触发
玩家进入世界,Fabric 的 JOIN 事件把玩家对象交给我们。
Fabric 1.20 entity spawn tutorial
这篇教程把一个很直观的效果做出来:玩家进入 Minecraft 世界后,服务器在玩家面前生成两排小鸡。学完以后,同一套思路可以换成牛、猪、狼、僵尸,也可以改成在前方摆出方块阵列。
Project result
在 Fabric MOD 里,“启动 Minecraft”时还没有具体玩家,所以真正适合写生成逻辑的时机是玩家进入世界之后。我们使用 `ServerPlayConnectionEvents.JOIN` 监听这个时刻,然后在服务器世界里创建实体。
玩家进入世界,Fabric 的 JOIN 事件把玩家对象交给我们。
读取玩家朝向,在前方 3 格找到中心点,再排成两行。
在服务器端创建实体,设置位置和方向,最后放进世界。
Mind map
小提示:如果你每次重新进入世界都会多出 10 只小鸡,这是正常的。这个版本演示的是“进入世界就触发”。如果只想第一次进入时触发,可以再加玩家数据记录。
Build flow
Code guide
package mls.maik;
import mls.maik.block.ModBlocks;
import mls.maik.event.ModPlayerEvents;
import mls.maik.item.ModItems;
import net.fabricmc.api.ModInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Maik implements ModInitializer {
public static final String MOD_ID = "maik";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
@Override
public void onInitialize() {
ModBlocks.registerModBlocks();
ModItems.registerModItems();
ModPlayerEvents.register();
LOGGER.info("Hello Fabric world!");
}
}
package mls.maik.event;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
public class ModPlayerEvents {
private static final int SPAWN_DISTANCE = 3;
private static final int ENTITIES_PER_ROW = 5;
public static void register() {
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
ServerPlayerEntity player = handler.getPlayer();
spawnEntityGrid(player, EntityType.CHICKEN, 10);
});
}
private static void spawnEntityGrid(ServerPlayerEntity player, EntityType<? extends Entity> entityType, int count) {
ServerWorld world = player.getServerWorld();
Direction forward = player.getHorizontalFacing();
Direction right = forward.rotateYClockwise();
BlockPos center = player.getBlockPos().offset(forward, SPAWN_DISTANCE);
for (int i = 0; i < count; i++) {
int sideOffset = (i % ENTITIES_PER_ROW) - (ENTITIES_PER_ROW / 2);
int forwardOffset = i / ENTITIES_PER_ROW;
BlockPos spawnPos = center.offset(right, sideOffset).offset(forward, forwardOffset);
Entity entity = entityType.create(world);
if (entity != null) {
BlockPos safePos = findSafeSpawnPos(world, spawnPos);
entity.refreshPositionAndAngles(
safePos.getX() + 0.5,
safePos.getY(),
safePos.getZ() + 0.5,
player.getYaw() + 180.0F,
0.0F
);
world.spawnEntity(entity);
}
}
}
private static BlockPos findSafeSpawnPos(ServerWorld world, BlockPos startPos) {
for (int yOffset = 2; yOffset >= -3; yOffset--) {
BlockPos pos = startPos.up(yOffset);
if (world.getBlockState(pos).isAir()
&& world.getBlockState(pos.up()).isAir()
&& !world.getBlockState(pos.down()).isAir()) {
return pos;
}
}
return startPos;
}
}
// 10 只小鸡
spawnEntityGrid(player, EntityType.CHICKEN, 10);
// 6 头牛
spawnEntityGrid(player, EntityType.COW, 6);
// 8 只猪
spawnEntityGrid(player, EntityType.PIG, 8);
// 4 只狼
spawnEntityGrid(player, EntityType.WOLF, 4);
// 5 只僵尸。注意:僵尸是敌对生物,生存模式里会攻击玩家。
spawnEntityGrid(player, EntityType.ZOMBIE, 5);
import mls.maik.block.ModBlocks;
import net.minecraft.block.Block;
import net.minecraft.block.Blocks;
// 在 register() 中可以这样调用:
placeBlocksInFront(player, Blocks.DIAMOND_BLOCK, 10);
placeBlocksInFront(player, ModBlocks.ggblock, 10);
private static void placeBlocksInFront(ServerPlayerEntity player, Block block, int count) {
ServerWorld world = player.getServerWorld();
Direction forward = player.getHorizontalFacing();
Direction right = forward.rotateYClockwise();
BlockPos center = player.getBlockPos().offset(forward, SPAWN_DISTANCE);
for (int i = 0; i < count; i++) {
int sideOffset = (i % ENTITIES_PER_ROW) - (ENTITIES_PER_ROW / 2);
int forwardOffset = i / ENTITIES_PER_ROW;
BlockPos pos = center.offset(right, sideOffset).offset(forward, forwardOffset);
if (world.getBlockState(pos).isAir()) {
world.setBlockState(pos, block.getDefaultState());
}
}
}
主类 `Maik` 负责注册所有模块,事件类负责玩家进入世界后的逻辑。以后想改成生成其它实体、加粒子、给玩家提示文字,都可以集中在这个文件里调整。
Key ideas
Minecraft 里真正改变世界的逻辑要放在服务器端。这里的 `player.getServerWorld()` 就是关键。
`getHorizontalFacing()` 会给出玩家面朝的方向,`rotateYClockwise()` 可以算出右边方向,用来排队。
实体位置用 `refreshPositionAndAngles` 设置,最后用 `world.spawnEntity(entity)` 放进世界。
如果脚下是空气,小鸡可能掉下去;如果身体位置不是空气,它可能卡在方块里。
`spawnEntityGrid(player, EntityType.CHICKEN, 10)` 的最后一个数字就是数量。
这个效果适合演示事件。正式玩法里可以加记录,只让每个玩家第一次进入时触发。
Creative extensions
这类项目很适合作为学生的第一个 Fabric 事件练习,因为它反馈非常快:进世界,看结果,改代码,再进世界。只要掌握触发时机和位置计算,就能做出很多变体。
适合刚开始学习实体生成,效果可爱、风险低。
把数量调大一点,就能做出玩家进入世界后的迷你牧场。
把实体换成方块,可以摆出数字、路径或自定义方块展示台。
What students learn
学生会明白代码不是随便运行的,而是在某个游戏事件发生后被调用。
从玩家位置、前方方向、横向偏移开始,建立 3D 世界里的空间感。
把小鸡换成其它实体或方块,就是从“照着做”走向“自己改”。