文章 15
评论 3
浏览 65385
PS1 月下夜想曲 Best 版本研究(三):让 Boss 学会召唤

PS1 月下夜想曲 Best 版本研究(三):让 Boss 学会召唤

为什么写这篇

给 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)

它其实只做四件事:

  1. DestroyEntity(child) 清零;
  2. child->entityId = id;
  3. child->pfnUpdate = 覆盖层实体表[id];
  4. 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
RBO70x801803380x3380x15 烟雾、0x1C 毒雾、0x1D 毒焰
RBO40x801803EC0x3EC0x16 本体、0x17 头、0x18 火焰、0x1A / 0x1B 门

每个覆盖层都不一样,换一个 Boss 就要重新反汇编确认一次,不能照抄。

四、标准流程(以「砸地召唤」为例)

  1. 选实体:在该 Boss 覆盖层的 e_init.c 里找现成实体,看 EInit 有没有 hitbox。纯特效选粒子类;要伤害就选 enemyDef 带判定的。
  2. 定时机:在 Boss 源码里找节点——动画结束、某一帧、计时到点,或者新增一个 step。
  3. 写生成序列:AllocEntity → CreateEntityFromEntity → 补参数。
  4. 放代码:指令少且原处有空洞就原地替换;否则放到扩容区,用 j / jal 跳过去。
  5. 改 DRA / XML(扩容时):长度、LBA、XML 文件项。
  6. 验证:仿真 → 镜像回读 → 游戏内存档扫描。

生成序列的骨架(纯特效):

; 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,必须判空
覆盖层尾部是 BSSRBO4 的 0x246E4 到 0x2539F,盲写会覆盖全局变量
图库没加载实体在跑但画不出来
params 高位params 的 0xFF00 常被当成 zPriority / palette,写之前先看目标实体 step 0

八、验证三件套

  1. 仿真:用桩函数替换 AllocEntity / CreateEntityFromEntity,跑一遍钩子,检查子实体的 id、params、坐标、z、rotate 是否符合预期;顺便验证 AllocEntity 返回 0 时确实不写内存。
  2. 镜像回读:校验光盘上的字节和本地文件一致,DRA 的 LBA 和长度正确。
  3. 存档扫描:在模拟器里触发招式后存档,扫 g_Entities[192..255],看子实体在不在、step 和 hitbox 对不对。

这套流程走下来,才能在实机前把大部分问题挡掉。


系列文章


标题:PS1 月下夜想曲 Best 版本研究(三):让 Boss 学会召唤
作者:Miaocat
地址:https://small7g.cn/articles/2026/11/04/1793783136724.html

Lazy Cat Days. 什么都有,又好像什么都没有.

取消