Fabric 1.20 entity spawn tutorial

进入世界,面前出现 10 只小鸡

这篇教程把一个很直观的效果做出来:玩家进入 Minecraft 世界后,服务器在玩家面前生成两排小鸡。学完以后,同一套思路可以换成牛、猪、狼、僵尸,也可以改成在前方摆出方块阵列。

Fabric 1.20 JOIN 事件 ServerWorld EntityType BlockPos 方块扩展

Project result

目标不是背代码,而是看懂“事件触发以后,游戏世界发生了什么”。

在 Fabric MOD 里,“启动 Minecraft”时还没有具体玩家,所以真正适合写生成逻辑的时机是玩家进入世界之后。我们使用 `ServerPlayConnectionEvents.JOIN` 监听这个时刻,然后在服务器世界里创建实体。

01

触发

玩家进入世界,Fabric 的 JOIN 事件把玩家对象交给我们。

02

定位

读取玩家朝向,在前方 3 格找到中心点,再排成两行。

03

生成

在服务器端创建实体,设置位置和方向,最后放进世界。

学生通过代码控制 Minecraft 风格世界里的小鸡、牛、猪和方块生成效果
从代码到游戏世界:同一个生成逻辑可以换成不同动物,也可以换成方块。
Fabric 玩家进入世界生成小鸡思维导图
思维导图:从触发时机、服务器世界、位置计算、实体创建、安全落点到扩展示例。

Mind map

把“生成小鸡”拆开,其实是六个小问题。

  • 什么时候执行:玩家进入世界以后执行,而不是 MOD 刚加载时执行。
  • 在哪一端执行:实体生成放在服务器端,避免客户端看到假实体或重复生成。
  • 生成在哪里:根据玩家朝向计算前方位置,再给每只小鸡一点横向偏移。
  • 怎么保证不尴尬:找一个上方是空气、脚下有方块的位置,减少卡进地面的情况。

小提示:如果你每次重新进入世界都会多出 10 只小鸡,这是正常的。这个版本演示的是“进入世界就触发”。如果只想第一次进入时触发,可以再加玩家数据记录。

Build flow

实现流程:先拿到玩家,再决定面前的位置,最后把实体放进世界。

Fabric 玩家进入世界生成实体流程图
流程图:JOIN 事件触发后,读取玩家朝向、计算安全位置、创建实体,再扩展成其它玩法。

Code guide

要添加的代码:一个入口注册,一个玩家事件类。

src/main/java/mls/maik/Maik.java
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!");
    }
}
src/main/java/mls/maik/event/ModPlayerEvents.java
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;
    }
}
替换 register() 里的这一行
// 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());
        }
    }
}

代码为什么写成独立的 `ModPlayerEvents`?

主类 `Maik` 负责注册所有模块,事件类负责玩家进入世界后的逻辑。以后想改成生成其它实体、加粒子、给玩家提示文字,都可以集中在这个文件里调整。

Key ideas

几个最容易混淆的点,先弄清楚。

01

不是客户端生成

Minecraft 里真正改变世界的逻辑要放在服务器端。这里的 `player.getServerWorld()` 就是关键。

02

不是随机乱放

`getHorizontalFacing()` 会给出玩家面朝的方向,`rotateYClockwise()` 可以算出右边方向,用来排队。

03

不是直接塞坐标

实体位置用 `refreshPositionAndAngles` 设置,最后用 `world.spawnEntity(entity)` 放进世界。

04

安全位置很重要

如果脚下是空气,小鸡可能掉下去;如果身体位置不是空气,它可能卡在方块里。

05

数量可以自己调

`spawnEntityGrid(player, EntityType.CHICKEN, 10)` 的最后一个数字就是数量。

06

重复进入会重复触发

这个效果适合演示事件。正式玩法里可以加记录,只让每个玩家第一次进入时触发。

Creative extensions

把小鸡换掉,马上就能变成新的玩法。

这类项目很适合作为学生的第一个 Fabric 事件练习,因为它反馈非常快:进世界,看结果,改代码,再进世界。只要掌握触发时机和位置计算,就能做出很多变体。

  • 动物欢迎队:小鸡、牛、猪各生成几只,做成进入世界的欢迎场景。
  • 奖励方块路:在玩家面前生成钻石块、金块或自定义方块,形成一条奖励路线。
  • 挑战开场:生成几只僵尸或骷髅,做成“进入世界立刻开始挑战”的模式。
  • 只触发一次:用玩家数据记录是否已经领取过欢迎礼,避免每次登录刷出一堆实体。

小鸡欢迎

适合刚开始学习实体生成,效果可爱、风险低。

牛羊牧场

把数量调大一点,就能做出玩家进入世界后的迷你牧场。

方块图案

把实体换成方块,可以摆出数字、路径或自定义方块展示台。

What students learn

这不是一个孤立的小效果,而是 Fabric 事件编程的入口。

读懂事件

学生会明白代码不是随便运行的,而是在某个游戏事件发生后被调用。

读懂坐标

从玩家位置、前方方向、横向偏移开始,建立 3D 世界里的空间感。

读懂扩展

把小鸡换成其它实体或方块,就是从“照着做”走向“自己改”。