梳理已有工程
先搜索 drache_block、Mario、MyZombie、BloodMoonReaper。这一步确认已有方块模型、Mario 实体、僵尸模型和血月收割者 Boss,避免重新造一套平行系统。
Forge 1.20.1 Street Fighter style brawl
本页记录一次完整的 MOD 开发过程:把原来的 drache_block 做成库巴大王 Boss,让它和 Mario、MyZombie、BloodMoonReaper 在自动生成的竞技台上进行街霸风格混战,并补上动画、粒子、玩家保护和构建验证。
Project overview
这个功能的核心想法来自街霸式对战:舞台有边界,角色有入场位置,技能有视觉反馈,玩家可以站在旁边观看战局,但不会因为某个 Boss 的控制技能而失去观战体验。Minecraft Forge 的实现方式不是做一套独立游戏,而是在现有实体系统、AI 目标系统、渲染系统和指令系统上组合出一个战斗场景。
项目最终增加了四个层面的内容。第一层是实体层,把 drache_block 活化为库巴大王 Boss,并注册刷怪蛋、属性和渲染器。第二层是场景层,新增 /streetfighter brawl 指令,一次性生成竞技台、围栏、灯光和四名战士。第三层是体验层,调整 MyZombie 血量,让 Mario 和库巴的火球更绚丽,让库巴走动和跳跃时出现明显动画。第四层是保护层,禁止大乱斗中的 BloodMoonReaper 对玩家施加黑暗和反胃眩晕,避免学生靠近观察时被技能打断。
从教学角度看,这个案例很适合讲“需求如何落到代码”。一句话需求里包含了 Boss 设计、实体注册、AI 目标、场景生成、粒子特效、动画表现、玩家体验和测试流程。把这些内容拆开后,学生能看到一个真实 MOD 功能并不是靠一个大文件完成,而是由多个小模块合作:实体负责行为,指令负责生成,渲染器负责表现,资源文件负责命名,构建工具负责检查。
Gameplay demo
这段视频展示了 /streetfighter brawl 生成后的实机效果:竞技台已经搭好,Mario、库巴大王、MyZombie 和 BloodMoonReaper 在同一个舞台上持续交战。观察时可以重点看三件事:第一,MyZombie 的血量更厚,所以它不会很快退场;第二,库巴移动和跳跃时有明显的身体摆动,Boss 感更强;第三,Mario 和库巴的火球带有更醒目的粒子反馈,战斗画面更接近街霸式大招乱飞的观感。
视频也适合用来核对需求是否落实:玩家靠近 BloodMoonReaper 时不会再被大乱斗专用的发昏技能影响,战斗能被稳定观看;四名角色没有被随便堆在同一点,而是在竞技台中形成来回拉扯;火焰、爆闪和场地灯光共同强化了“Boss 大乱斗”的演示效果。
Mind map
思维导图的作用是帮学生建立模块感。比如“库巴变成 Boss”并不只是一句描述,它至少包含实体类、属性、AI、技能、Boss 血条、渲染器、刷怪蛋和方块触发逻辑。“大乱斗”也不是把四个实体随便放在一起,它需要出生点、血量调平、目标锁定、场地边界和观战保护。把这些模块先画出来,代码就不会变成一团难以维护的临时补丁。
DracheBoss,复用 drache_block 的模型视觉,给它火焰吐息、火球连发、壳冲撞、震地和狂暴。StreetFighterCommands,通过命令自动构建竞技台,减少手动摆放成本。Implementation steps
先搜索 drache_block、Mario、MyZombie、BloodMoonReaper。这一步确认已有方块模型、Mario 实体、僵尸模型和血月收割者 Boss,避免重新造一套平行系统。
新增 DracheBoss,继承 Monster。它拥有 720 血、强护甲、击退抗性、Boss 血条和多段技能,让它能像大 Boss 一样坚持战斗。
drache_block 原本是 Blockbench 方块模型,已经有头、身体和背甲结构。渲染器直接调用方块渲染,让 Boss 看起来就是原来的库巴模型“活过来”。
新增 DracheBlock,重写 setPlacedBy。玩家放下方块后,服务端生成 DracheBoss,然后删除原方块,形成“方块变生物”的效果。
在 ExampleMod 中注册 DRACHE_BOSS 和 DRACHE_BOSS_SPAWN_EGG,绑定实体属性和客户端渲染器,并把刷怪蛋放入创造模式标签。
新增 /streetfighter brawl。指令以玩家前方为中心,清空空间、铺竞技台地板、放围栏和灯光,再生成四名战士。
大乱斗中临时把库巴设为 720 血,Mario 设为 120 血,MyZombie 设为 360 血,BloodMoonReaper 设为 520 血。这样战斗不会几秒结束。
指令生成后先让库巴盯 Mario,Mario 盯库巴,MyZombie 盯收割者,收割者盯 MyZombie。AI 后续也能根据受伤和目标规则切换。
大乱斗里的 BloodMoonReaper 会被写入一个持久化标记。它仍能打架,但它的凝视和血月领域不会对玩家施加黑暗、反胃眩晕。
Mario 火球增加彩色拖尾、星火、爆闪;库巴火球和吐息增加火焰核心、电火花、熔岩粒子。库巴渲染器根据速度和离地状态做走动摆动与跳跃拉伸。
补充中文和英文语言文件,让库巴刷怪蛋、实体名称和街霸竞技场提示不再显示裸 key,学生测试时能更容易理解当前发生了什么。
先跑 compileJava 抓 API 名称错误,再跑 build 检查资源处理、打包和重混淆。构建通过后再进入游戏执行命令观察效果。
Flow chart
学生第一次写命令系统时,经常把“命令能执行”和“命令能稳定生成场景”混在一起。这里的流程图强调服务端执行顺序:先拿到玩家,计算玩家前方的安全中心点,再构建竞技台。只有场地准备好以后,才生成四名实体并设置血量、名字、持久化和目标。这样做的好处是,如果任何一个实体创建失败,命令可以返回失败提示,而不是留下一个半成品舞台。
流程图最后一步是构建验证。Forge MOD 中很多错误不是写代码时立刻出现,而是在资源处理、重混淆、客户端渲染或游戏内实体生成时才暴露。因此文档把 gradlew build 和游戏内重新执行命令作为最终步骤,帮助学生形成“写完就验证”的工程习惯。
Arena layout
竞技台使用长方形结构,左右两侧有红色和蓝色区域,中间用黄色线条形成视觉焦点。围栏和铁栏杆的作用是减少 Boss 跑出场地的概率,海晶灯让场地更容易被玩家观察。地板使用深板岩和抛光深板岩交替,既保持 Minecraft 的方块感,又能让火焰、粒子和发光效果更明显。
出生点也经过设计:库巴和 Mario 在左右两侧面对面,MyZombie 和 BloodMoonReaper 在斜向位置形成第二条战线。这样开局不会所有实体挤在一个点上,也不会完全分散。它们先形成两组对抗,随后因为范围伤害、受伤仇恨和移动 AI 进入更混乱的四方乱斗。
/streetfighter brawl。旧场景中已经生成的 BloodMoonReaper 不会自动带上“禁止玩家眩晕”的新标记。Code blocks
public static final RegistryObject<EntityType<DracheBoss>> DRACHE_BOSS =
ENTITY_TYPES.register("drache_block", () ->
EntityType.Builder.of(DracheBoss::new, MobCategory.MONSTER)
.sized(2.4F, 3.35F)
.clientTrackingRange(12)
.fireImmune()
.build(new ResourceLocation(MODID, "drache_block").toString()));
public static final RegistryObject<Item> DRACHE_BOSS_SPAWN_EGG =
ITEMS.register("drache_block_spawn_egg",
() -> new ForgeSpawnEggItem(DRACHE_BOSS, 0xF0A018, 0x2F8F2D, new Item.Properties()));
public class DracheBlock extends Block {
@Override
public void setPlacedBy(Level level, BlockPos pos, BlockState state,
LivingEntity placer, ItemStack stack) {
super.setPlacedBy(level, pos, state, placer, stack);
if (!(level instanceof ServerLevel serverLevel)) {
return;
}
DracheBoss boss = ExampleMod.DRACHE_BOSS.get().create(serverLevel);
if (boss == null) {
return;
}
float yaw = placer == null ? 0.0F : placer.getYRot() + 180.0F;
boss.moveTo(pos.getX() + 0.5D, pos.getY(), pos.getZ() + 0.5D, yaw, 0.0F);
serverLevel.addFreshEntity(boss);
serverLevel.removeBlock(pos, false);
}
}
private static int startBrawl(CommandSourceStack source) {
ServerPlayer player = source.getPlayerOrException();
ServerLevel level = player.serverLevel();
BlockPos center = arenaCenter(player);
buildArena(level, center);
DracheBoss drache = spawnMob(ExampleMod.DRACHE_BOSS.get().create(level), level, center.offset(-10, 1, 0), 90.0F, "KOOPA KING");
Mario mario = spawnMob(ExampleMod.MARIO.get().create(level), level, center.offset(10, 1, 0), -90.0F, "MARIO");
MyZombie myZombie = spawnMob(ExampleMod.MY_ZOMBIE.get().create(level), level, center.offset(-3, 1, 5), 25.0F, "MYZOMBIE");
BloodMoonReaper reaper = spawnMob(ExampleMod.BLOOD_MOON_REAPER.get().create(level), level, center.offset(4, 1, -5), -150.0F, "BLOOD MOON REAPER");
tuneArenaFighter(drache, 720.0D);
tuneArenaFighter(mario, 120.0D);
tuneArenaFighter(myZombie, 360.0D);
tuneArenaFighter(reaper, 520.0D);
reaper.setStreetFighterNoPlayerDizzy(true);
drache.setTarget(mario);
mario.setTarget(drache);
myZombie.setTarget(reaper);
reaper.setTarget(myZombie);
return 1;
}
private static final String STREET_FIGHTER_NO_PLAYER_DIZZY_TAG =
"StreetFighterNoPlayerDizzy";
public void setStreetFighterNoPlayerDizzy(boolean value) {
this.getPersistentData().putBoolean(STREET_FIGHTER_NO_PLAYER_DIZZY_TAG, value);
}
private boolean suppressesPlayerDizzy() {
return this.getPersistentData().getBoolean(STREET_FIGHTER_NO_PLAYER_DIZZY_TAG);
}
private void tickGaze() {
if (this.suppressesPlayerDizzy() || !(this.level() instanceof ServerLevel serverLevel)) {
return;
}
// 普通 BloodMoonReaper 仍然保留凝视技能。
}
float speed = Mth.clamp((float) entity.getDeltaMovement().horizontalDistance() * 5.0F, 0.0F, 1.0F);
float walkTime = entity.tickCount + partialTick;
float walkSwing = Mth.sin(walkTime * (0.36F + speed * 0.38F)) * speed;
float walkBounce = Mth.abs(Mth.sin(walkTime * (0.72F + speed * 0.5F))) * 0.06F * speed;
float verticalMotion = (float) entity.getDeltaMovement().y;
boolean airborne = !entity.onGround();
float jumpStretch = airborne ? Mth.clamp(verticalMotion * 0.18F, -0.1F, 0.13F) : 0.0F;
poseStack.translate(0.0D, 0.02D + walkBounce, 0.0D);
poseStack.mulPose(Axis.ZP.rotationDegrees(walkSwing * 4.5F));
poseStack.scale(scale * squashXz, scale * stretchY, scale * squashXz);
private void paintFireballTrail(ServerLevel serverLevel, Vec3 origin,
Vec3 direction, boolean starPowered) {
DustParticleOptions dust = starPowered ? STAR_FIREBALL_DUST : FIREBALL_DUST;
for (int i = 0; i < 18; i++) {
double distance = 0.35D + i * 0.42D;
double swirl = (this.tickCount + i) * 0.72D;
Vec3 ring = side.scale(Mth.cos((float) swirl) * 0.18D)
.add(0.0D, Mth.sin((float) swirl) * 0.18D, 0.0D);
Vec3 point = origin.add(direction.scale(distance)).add(ring);
serverLevel.sendParticles(dust, point.x, point.y, point.z, 2, 0.035D, 0.035D, 0.035D, 0.0D);
serverLevel.sendParticles(ParticleTypes.END_ROD, point.x, point.y, point.z, 1, 0.02D, 0.02D, 0.02D, 0.01D);
}
serverLevel.sendParticles(ParticleTypes.FLASH, origin.x, origin.y, origin.z, 1, 0, 0, 0, 0);
}
Fighter roles
库巴负责制造压迫感。它血量最高,有 Boss 血条,有火焰吐息、火球连发、壳冲撞、震地和狂暴爆发。它的动作比较重,所以渲染器用走动摆动和跳跃压缩来表现重量。
Mario 有跳踩、火球和星星状态。大乱斗中临时提升血量,让他不是一碰就倒。火球拖尾和发射爆闪让学生能清楚看见技能从哪里释放、飞向哪里。
MyZombie 基础血量被提高,大乱斗中临时血量更多。它攻击时会击退目标并产生暴击粒子,角色定位像一个越打越碍事的重型斗士。
BloodMoonReaper 原本会用血月领域和凝视干扰玩家。大乱斗版本保留怪物战斗能力,但对玩家禁用发昏效果,这样学生能走进竞技台边缘观察。
竞技台的作用是控制空间。边界让战斗集中,中线和出生区让战斗一开始就有“对战舞台”的仪式感,灯光则让粒子效果更清楚。
玩家不是这场混战的主角,而是测试者。玩家可以靠近观察技能、动画和 AI,但不应该被黑暗和反胃阻止学习。这就是禁用玩家眩晕的原因。
Test checklist
运行 .\gradlew.bat compileJava 和 .\gradlew.bat build。如果编译不过,先处理 Java API、导入和类型错误,不要急着进游戏。
进入游戏后输入 /streetfighter brawl。如果命令没有补全,检查 RegisterCommandsEvent 是否调用了 StreetFighterCommands.register(event)。
执行命令后检查地板、边界、灯光、红蓝出生区和中线。如果场地被山体遮挡,可以换一个平坦位置重新执行。
确认库巴、Mario、MyZombie 和 BloodMoonReaper 都出现,并且名字可见。如果有角色缺失,优先检查实体注册和属性绑定。
靠近大乱斗中的 BloodMoonReaper,确认不会反胃或黑暗。注意普通刷出的 BloodMoonReaper 仍保留原技能,这是预期行为。
观察库巴移动、跳跃、壳冲撞和火焰技能。观察 Mario 火球拖尾、闪光和星火。如果效果太密集,可以再降低粒子数量。
这个功能的学习价值在于它把许多 MOD 技术点放进同一个可见作品里。实体注册解决“游戏认识谁”,属性和 AI 解决“它会怎么行动”,指令解决“一键生成场景”,渲染器和粒子解决“玩家看到了什么”,测试清单解决“怎么确认它真的完成”。当学生能从这篇文档顺着图、步骤和代码复现功能时,就已经跨过了从“写一个物品”到“组织一个完整玩法系统”的重要门槛。
后续可以继续扩展:给竞技台加入倒计时和胜负判定,给每个角色添加血条 UI,把战斗结果写入聊天栏,或者让玩家选择队伍加入混战。也可以进一步改造库巴模型,把方块渲染升级为可分部位动画的实体模型,让头、手、脚和背甲都有独立动作。现在的版本已经可以作为课堂演示和下一轮扩展的稳定起点。