Forge 1.20.1 Street Fighter style brawl

Minecraft 街霸大乱斗实现文档

本页记录一次完整的 MOD 开发过程:把原来的 drache_block 做成库巴大王 Boss,让它和 Mario、MyZombie、BloodMoonReaper 在自动生成的竞技台上进行街霸风格混战,并补上动画、粒子、玩家保护和构建验证。

DracheBoss Mario Fireball MyZombie Buff BloodMoonReaper StreetFighter Arena Gradle Build

Project overview

目标不是简单刷四个怪,而是做出一场可重复触发、可观察、可讲解的 Boss 混战。

这个功能的核心想法来自街霸式对战:舞台有边界,角色有入场位置,技能有视觉反馈,玩家可以站在旁边观看战局,但不会因为某个 Boss 的控制技能而失去观战体验。Minecraft Forge 的实现方式不是做一套独立游戏,而是在现有实体系统、AI 目标系统、渲染系统和指令系统上组合出一个战斗场景。

项目最终增加了四个层面的内容。第一层是实体层,把 drache_block 活化为库巴大王 Boss,并注册刷怪蛋、属性和渲染器。第二层是场景层,新增 /streetfighter brawl 指令,一次性生成竞技台、围栏、灯光和四名战士。第三层是体验层,调整 MyZombie 血量,让 Mario 和库巴的火球更绚丽,让库巴走动和跳跃时出现明显动画。第四层是保护层,禁止大乱斗中的 BloodMoonReaper 对玩家施加黑暗和反胃眩晕,避免学生靠近观察时被技能打断。

从教学角度看,这个案例很适合讲“需求如何落到代码”。一句话需求里包含了 Boss 设计、实体注册、AI 目标、场景生成、粒子特效、动画表现、玩家体验和测试流程。把这些内容拆开后,学生能看到一个真实 MOD 功能并不是靠一个大文件完成,而是由多个小模块合作:实体负责行为,指令负责生成,渲染器负责表现,资源文件负责命名,构建工具负责检查。

Minecraft 街霸大乱斗竞技台插图
插图:库巴、Mario、MyZombie 和 BloodMoonReaper 在 Minecraft 竞技台上混战,舞台风格参考街霸对战场景。

Gameplay demo

先看一次真实战斗,再回头拆代码,学生更容易理解每个模块的作用。

这段视频展示了 /streetfighter brawl 生成后的实机效果:竞技台已经搭好,Mario、库巴大王、MyZombie 和 BloodMoonReaper 在同一个舞台上持续交战。观察时可以重点看三件事:第一,MyZombie 的血量更厚,所以它不会很快退场;第二,库巴移动和跳跃时有明显的身体摆动,Boss 感更强;第三,Mario 和库巴的火球带有更醒目的粒子反馈,战斗画面更接近街霸式大招乱飞的观感。

视频也适合用来核对需求是否落实:玩家靠近 BloodMoonReaper 时不会再被大乱斗专用的发昏技能影响,战斗能被稳定观看;四名角色没有被随便堆在同一点,而是在竞技台中形成来回拉扯;火焰、爆闪和场地灯光共同强化了“Boss 大乱斗”的演示效果。

实机演示:Minecraft Forge 1.20.1 街霸大乱斗,四名角色在竞技台上持续混战。
街霸大乱斗实现思维导图
思维导图:中心是街霸大乱斗,向外拆成库巴 Boss、竞技台、四方目标、玩家保护、动画粒子和构建验证。

Mind map

先看全图,再写代码。这样每个文件为什么存在就很清楚。

思维导图的作用是帮学生建立模块感。比如“库巴变成 Boss”并不只是一句描述,它至少包含实体类、属性、AI、技能、Boss 血条、渲染器、刷怪蛋和方块触发逻辑。“大乱斗”也不是把四个实体随便放在一起,它需要出生点、血量调平、目标锁定、场地边界和观战保护。把这些模块先画出来,代码就不会变成一团难以维护的临时补丁。

  • 实体模块:新增 DracheBoss,复用 drache_block 的模型视觉,给它火焰吐息、火球连发、壳冲撞、震地和狂暴。
  • 场景模块:新增 StreetFighterCommands,通过命令自动构建竞技台,减少手动摆放成本。
  • 体验模块:给库巴走动和跳跃加动画,给 Mario 与库巴火球加拖尾、爆闪和电火花。
  • 安全模块:大乱斗中的 BloodMoonReaper 不再让玩家黑暗和反胃,但它在普通战斗中仍保持原有 Boss 设定。

Implementation steps

详细实现步骤:从方块、实体、指令到动画粒子,一步一步搭起来。

01

梳理已有工程

先搜索 drache_blockMarioMyZombieBloodMoonReaper。这一步确认已有方块模型、Mario 实体、僵尸模型和血月收割者 Boss,避免重新造一套平行系统。

02

设计库巴 Boss

新增 DracheBoss,继承 Monster。它拥有 720 血、强护甲、击退抗性、Boss 血条和多段技能,让它能像大 Boss 一样坚持战斗。

03

复用 drache_block 外观

drache_block 原本是 Blockbench 方块模型,已经有头、身体和背甲结构。渲染器直接调用方块渲染,让 Boss 看起来就是原来的库巴模型“活过来”。

04

让方块放下即变身

新增 DracheBlock,重写 setPlacedBy。玩家放下方块后,服务端生成 DracheBoss,然后删除原方块,形成“方块变生物”的效果。

05

注册实体和刷怪蛋

ExampleMod 中注册 DRACHE_BOSSDRACHE_BOSS_SPAWN_EGG,绑定实体属性和客户端渲染器,并把刷怪蛋放入创造模式标签。

06

实现竞技台指令

新增 /streetfighter brawl。指令以玩家前方为中心,清空空间、铺竞技台地板、放围栏和灯光,再生成四名战士。

07

调节角色血量

大乱斗中临时把库巴设为 720 血,Mario 设为 120 血,MyZombie 设为 360 血,BloodMoonReaper 设为 520 血。这样战斗不会几秒结束。

08

设置互相攻击目标

指令生成后先让库巴盯 Mario,Mario 盯库巴,MyZombie 盯收割者,收割者盯 MyZombie。AI 后续也能根据受伤和目标规则切换。

09

禁止玩家发昏

大乱斗里的 BloodMoonReaper 会被写入一个持久化标记。它仍能打架,但它的凝视和血月领域不会对玩家施加黑暗、反胃眩晕。

10

增强火球和动画

Mario 火球增加彩色拖尾、星火、爆闪;库巴火球和吐息增加火焰核心、电火花、熔岩粒子。库巴渲染器根据速度和离地状态做走动摆动与跳跃拉伸。

11

本地化与提示

补充中文和英文语言文件,让库巴刷怪蛋、实体名称和街霸竞技场提示不再显示裸 key,学生测试时能更容易理解当前发生了什么。

12

构建验证

先跑 compileJava 抓 API 名称错误,再跑 build 检查资源处理、打包和重混淆。构建通过后再进入游戏执行命令观察效果。

Flow chart

流程图把命令执行过程讲清楚:先造舞台,再放角色,最后启动战斗。

学生第一次写命令系统时,经常把“命令能执行”和“命令能稳定生成场景”混在一起。这里的流程图强调服务端执行顺序:先拿到玩家,计算玩家前方的安全中心点,再构建竞技台。只有场地准备好以后,才生成四名实体并设置血量、名字、持久化和目标。这样做的好处是,如果任何一个实体创建失败,命令可以返回失败提示,而不是留下一个半成品舞台。

流程图最后一步是构建验证。Forge MOD 中很多错误不是写代码时立刻出现,而是在资源处理、重混淆、客户端渲染或游戏内实体生成时才暴露。因此文档把 gradlew build 和游戏内重新执行命令作为最终步骤,帮助学生形成“写完就验证”的工程习惯。

街霸大乱斗实现流程图
流程图:从输入命令到生成新版竞技场,展示服务端指令的完整执行路径。
街霸大乱斗竞技台布局图
竞技台布局图:红蓝出生区、中线、边界围栏和灯光共同制造街霸风格舞台感。

Arena layout

竞技台不是装饰,它控制战斗节奏。

竞技台使用长方形结构,左右两侧有红色和蓝色区域,中间用黄色线条形成视觉焦点。围栏和铁栏杆的作用是减少 Boss 跑出场地的概率,海晶灯让场地更容易被玩家观察。地板使用深板岩和抛光深板岩交替,既保持 Minecraft 的方块感,又能让火焰、粒子和发光效果更明显。

出生点也经过设计:库巴和 Mario 在左右两侧面对面,MyZombie 和 BloodMoonReaper 在斜向位置形成第二条战线。这样开局不会所有实体挤在一个点上,也不会完全分散。它们先形成两组对抗,随后因为范围伤害、受伤仇恨和移动 AI 进入更混乱的四方乱斗。

重新测试新版效果时,要重新执行 /streetfighter brawl。旧场景中已经生成的 BloodMoonReaper 不会自动带上“禁止玩家眩晕”的新标记。

Code blocks

关键代码块:文档保留最能说明思路的片段。

1. 注册库巴 Boss 实体和刷怪蛋
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()));
2. drache_block 放下后变成库巴 Boss
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);
    }
}
3. /streetfighter brawl 指令的主流程
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;
}
4. 大乱斗中禁止 BloodMoonReaper 让玩家发昏
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 仍然保留凝视技能。
}
5. 库巴走动和跳跃动画
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);
6. Mario 和库巴火球的绚丽粒子
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 和视觉中心

库巴负责制造压迫感。它血量最高,有 Boss 血条,有火焰吐息、火球连发、壳冲撞、震地和狂暴爆发。它的动作比较重,所以渲染器用走动摆动和跳跃压缩来表现重量。

Mario

灵活的远近混合角色

Mario 有跳踩、火球和星星状态。大乱斗中临时提升血量,让他不是一碰就倒。火球拖尾和发射爆闪让学生能清楚看见技能从哪里释放、飞向哪里。

MyZombie

耐打的近战搅局者

MyZombie 基础血量被提高,大乱斗中临时血量更多。它攻击时会击退目标并产生暴击粒子,角色定位像一个越打越碍事的重型斗士。

收割者

危险技能型 Boss

BloodMoonReaper 原本会用血月领域和凝视干扰玩家。大乱斗版本保留怪物战斗能力,但对玩家禁用发昏效果,这样学生能走进竞技台边缘观察。

竞技台

让战斗被看见

竞技台的作用是控制空间。边界让战斗集中,中线和出生区让战斗一开始就有“对战舞台”的仪式感,灯光则让粒子效果更清楚。

玩家

观战者和调试者

玩家不是这场混战的主角,而是测试者。玩家可以靠近观察技能、动画和 AI,但不应该被黑暗和反胃阻止学习。这就是禁用玩家眩晕的原因。

Test checklist

测试清单:每次改完都要看这些点。

A

构建是否通过

运行 .\gradlew.bat compileJava.\gradlew.bat build。如果编译不过,先处理 Java API、导入和类型错误,不要急着进游戏。

B

命令是否出现

进入游戏后输入 /streetfighter brawl。如果命令没有补全,检查 RegisterCommandsEvent 是否调用了 StreetFighterCommands.register(event)

C

竞技台是否完整

执行命令后检查地板、边界、灯光、红蓝出生区和中线。如果场地被山体遮挡,可以换一个平坦位置重新执行。

D

四名角色是否生成

确认库巴、Mario、MyZombie 和 BloodMoonReaper 都出现,并且名字可见。如果有角色缺失,优先检查实体注册和属性绑定。

E

玩家是否不会发昏

靠近大乱斗中的 BloodMoonReaper,确认不会反胃或黑暗。注意普通刷出的 BloodMoonReaper 仍保留原技能,这是预期行为。

F

动画和粒子是否明显

观察库巴移动、跳跃、壳冲撞和火焰技能。观察 Mario 火球拖尾、闪光和星火。如果效果太密集,可以再降低粒子数量。

课堂总结

这个功能的学习价值在于它把许多 MOD 技术点放进同一个可见作品里。实体注册解决“游戏认识谁”,属性和 AI 解决“它会怎么行动”,指令解决“一键生成场景”,渲染器和粒子解决“玩家看到了什么”,测试清单解决“怎么确认它真的完成”。当学生能从这篇文档顺着图、步骤和代码复现功能时,就已经跨过了从“写一个物品”到“组织一个完整玩法系统”的重要门槛。

后续可以继续扩展:给竞技台加入倒计时和胜负判定,给每个角色添加血条 UI,把战斗结果写入聊天栏,或者让玩家选择队伍加入混战。也可以进一步改造库巴模型,把方块渲染升级为可分部位动画的实体模型,让头、手、脚和背甲都有独立动作。现在的版本已经可以作为课堂演示和下一轮扩展的稳定起点。