为什么写这篇
给 Boss 加「召唤物」听起来很酷,但真正动手会立刻撞上一连串问题:调用哪个函数?实体 id 从哪来?为什么召唤出来的东西看不见?为什么它没有伤害?
这篇把我们给 RBO7 双 Boss 和 RBO4 弗兰肯斯坦加招式的经验,整理成一套可复用的方法论。
一、先分清三种「实体」
| 类型 | 例子 | 有伤害吗 | 说明 |
|---|---|---|---|
| 纯特效粒子 | RBO4 的 THE_CREATURE_FLAMES(id 0x18) | 否 | hitboxState=0、attack=0,只播动画 |
| 有判定的弹幕 / 召唤物 | RBO7 的 UNK_1C 毒雾、UNK_1D 毒焰 | 是 | EInit / enemyDef 带 hitbox |
| 完整小怪 / Boss 部件 | RBO7 的木乃伊本体、Boss 门 | 是 | 有自己的状态机、动画、AI |
最关键的一条:CreateEntityFromEntity(id) 用的是「当前 Boss 覆盖层自己的实体表」。
如果目标实体不在这个覆盖层里,它的更新函数、EInit、动画表、图库都不在当前内存中。直接写一个 id,拿到的只会是错的函数指针或者空指针。
想跨 Boss 复用实体,等于把那段更新函数 + EInit + 动画 + 图库一起搬过来——那是另一个量级的工程。
二、三个必须认识的函数
AllocEntity(start, end)
在当前实体数组里找 entityId == 0 的空槽,找到就先 DestroyEntity 再返回指针;没有空槽返回 0。
常用区间:
- [160, 192):玩家武器 / 副武器 / 部分弹幕(RBO7 毒雾用它)
- [192, 256):关卡粒子 / 召唤物(RBO4 火焰、RBO7 毒焰用它)
调用后必须判空,槽位满了会返回 0,直接写内存就是踩空。
CreateEntityFromEntity(id, parent, child)
它其实只做四件事:
- DestroyEntity(child) 清零;
- child->entityId = id;
- child->pfnUpdate = 覆盖层实体表[id];
- child->posX / posY = parent 的坐标。
它不会复制 params、rotate、zPriority、hitbox、animSet。 这些要么自己写,要么靠 child 的 step 0 去初始化。
还有个 MIPS 层面的细节:a2 必须放在 jal 的延迟槽里。
ori a0, zero, 0x1C ; 实体 id
addu a1, s2, zero ; parent = self
jal CreateEntityFromEntity
addu a2, s0, zero ; child(在延迟槽里)
InitializeEntity(g_EInitX)
child 的 step 0 通常会调用它,从 EInit + enemyDef 设置动画、调色板、zPriority、hitbox 等等。
它会覆盖你在创建时写的 zPriority 和 hitbox。 所以顺序很重要:
- 只改位置 / 朝向 / params:创建后直接写,没问题;
- 想改 zPriority / hitbox:必须在 InitializeEntity 之后写——可以在 child 的 step 0 里挂钩子,或者干脆改 EInit / enemyDef 数据。
三、覆盖层实体表在哪
CreateEntityFromEntity 里有一句 lw v0, imm(at),其中 at 等于 0x80180000 加上 id 乘 4。那么 0x80180000 + imm 就是这个覆盖层的实体表:
| 覆盖层 | 实体表 | 表偏移 | 已确认的 id |
|---|---|---|---|
| RBO7 | 0x80180338 | 0x338 | 0x15 烟雾、0x1C 毒雾、0x1D 毒焰 |
| RBO4 | 0x801803EC | 0x3EC | 0x16 本体、0x17 头、0x18 火焰、0x1A / 0x1B 门 |
每个覆盖层都不一样,换一个 Boss 就要重新反汇编确认一次,不能照抄。
四、标准流程(以「砸地召唤」为例)
- 选实体:在该 Boss 覆盖层的 e_init.c 里找现成实体,看 EInit 有没有 hitbox。纯特效选粒子类;要伤害就选 enemyDef 带判定的。
- 定时机:在 Boss 源码里找节点——动画结束、某一帧、计时到点,或者新增一个 step。
- 写生成序列:AllocEntity → CreateEntityFromEntity → 补参数。
- 放代码:指令少且原处有空洞就原地替换;否则放到扩容区,用 j / jal 跳过去。
- 改 DRA / XML(扩容时):长度、LBA、XML 文件项。
- 验证:仿真 → 镜像回读 → 游戏内存档扫描。
生成序列的骨架(纯特效):
; child = AllocEntity(0x8007A960, 0x8007C0E0)
lui a0, 0x8008
addiu a0, a0, -0x56A0
lui a1, 0x8008
addiu a1, a1, -0x3F20
jal AllocEntity
nop
beq v0, zero, skip
addu s0, v0, zero
; CreateEntityFromEntity(id, self, child)
ori a0, zero, 0x1C
addu a1, s2, zero
jal CreateEntityFromEntity
addu a2, s0, zero
; 子实体参数
ori v0, zero, 2
sh v0, 0x30(s0) ; params
lhu v0, 0x24(s2)
addiu v0, v0, 1
sh v0, 0x24(s0) ; zPriority = self + 1
五、要伤害怎么办
优先找 EInit / enemyDef 本来就把 hitbox 配好的实体(敌人的弹幕、Boss 召唤物)。
如果手上只有纯特效实体,就在 child 的 step 0、InitializeEntity 之后补:
ori v0, zero, 1
sh v0, 0x3C(s0) ; hitboxState = 1
ori v0, zero, 8
sh v0, 0x40(s0) ; attack
sh v0, 0x46(s0) ; width
sh v0, 0x47(s0) ; height
六、看不见是另一回事
实体能不能被画出来,取决于三件事:
- animSet 对应的 sprite bank 是否已在本房间加载;
- palette / blendMode / drawFlags 是否正确;
- anim 指针是否指向本覆盖层的动画表。
RBO4 的火焰能用,是因为它的 animSet 0x8004 恰好在房间的 bank 表里。换成别的 Boss 的图库就未必了——跨 Boss 复用实体,图库往往要一起搬,也就是「导入 VRAM」那件事。
七、坑清单
| 坑 | 说明 |
|---|---|
| 实体表偏移每个覆盖层不同 | RBO7 是 0x338,RBO4 是 0x3EC,必须自己反汇编确认 |
| CreateEntityFromEntity 只复制坐标 | params / rotate / z / hitbox 全都要自己写 |
| a2 必须在 jal 延迟槽 | 否则传进去的是垃圾 |
| InitializeEntity 覆盖 z 和 hitbox | 要改就在它之后挂钩子,或改 EInit |
| AllocEntity 区间选错 | 只能用 [160,192) 或 [192,256) |
| 槽位满 | 返回 0,必须判空 |
| 覆盖层尾部是 BSS | RBO4 的 0x246E4 到 0x2539F,盲写会覆盖全局变量 |
| 图库没加载 | 实体在跑但画不出来 |
| params 高位 | params 的 0xFF00 常被当成 zPriority / palette,写之前先看目标实体 step 0 |
八、验证三件套
- 仿真:用桩函数替换 AllocEntity / CreateEntityFromEntity,跑一遍钩子,检查子实体的 id、params、坐标、z、rotate 是否符合预期;顺便验证 AllocEntity 返回 0 时确实不写内存。
- 镜像回读:校验光盘上的字节和本地文件一致,DRA 的 LBA 和长度正确。
- 存档扫描:在模拟器里触发招式后存档,扫 g_Entities[192..255],看子实体在不在、step 和 hitbox 对不对。
这套流程走下来,才能在实机前把大部分问题挡掉。
系列文章
