- 游戏开发
- 插件系统
【免费下载链接】REPENTOGON
Script extender for The Binding of Isaac: Repentance
本文面向 The Binding of Isaac: Repentance 的模组开发者,系统讲解 REPENTOGON 脚本扩展器对GridEntityDoor(网格实体:门)这一 Lua 类所做的全部改动。读完你将掌握:PreviousState与PreviousVariant两个"由 userdata 修正为整数"的读写变量、GetExtraSprite()与PlayAnimation()两个新增函数,以及它们背后的源码实现、相关门渲染/更新回调与门槽位(DoorSlot/DoorMask)联动用法。
一、GridEntityDoor 是什么
GridEntityDoor是 GridEntity 的派生类,对应游戏网格中的门实体(房间入口、出口、隐藏门等)。在 REPENTOGON 中,它被挂接到独立的 Lua metatableGRID_ENTITY_DOOR,并在 LuaClasses.h 中通过LuaGridEntityDoor = LuabridgeType<GridEntity_Door, ...>导出到 Lua 环境。
原版 API 对门的控制能力有限:门的关键内部状态("变化前的状态")暴露给 Lua 时是不透明的 userdata,模组无法直接读取或写回;同时缺少对附加精灵(ExtraSprite)与动画播放的原生访问接口。REPENTOGON 在 docs/docs/GridEntityDoor.md 中记录了两类修复:Modified Variables(修改变量)与Functions(新增函数)。
二、PreviousState / PreviousVariant:从 userdata 修正为整数
这是本文档最重要的改动。原版中GridEntityDoor.PreviousState与PreviousVariant返回的是 userdata(指向内部结构),模组拿不到可用的数值;REPENTOGON 将其修复为返回真正的整数(int),并且支持读写。
| 变量 | 类型 | 说明 |
|---|---|---|
PreviousState | int | 与默认行为相同,但现在返回正确的整数值而非 userdata |
PreviousVariant | int | 与默认行为相同,但现在返回正确的整数值而非 userdata |
重要更新时机:根据 docs/docs/GridEntityDoor.md 的注记,这两个字段只在初始化门的子类(initializing a door subclass)或播放动画(playing an animation)时才会被更新。也就是说,门的"前一个 State / 前一个 Variant"并不在每次状态变更时都刷新,模组若依赖这两个字段做状态机判断,需要结合门的动画播放时机来理解其语义。
底层实现验证
修复由 LuaGridEntityDoor.cpp 完成:两个RegisterVariable注册了 getter/setter,读操作直接lua_pushinteger返回gridEnt->_previousState/gridEnt->_previousVariant(均为unsigned int),写操作通过luaL_checkinteger接收整数并回写:
lua::RegisterVariable(_state, lua::Metatables::GRID_ENTITY_DOOR, "PreviousState", Lua_GridEntityDoor_GetPreviousState, Lua_GridEntityDoor_SetPreviousState); lua::RegisterVariable(_state, lua::Metatables::GRID_ENTITY_DOOR, "PreviousVariant", Lua_GridEntityDoor_GetPreviousVarinat, Lua_GridEntityDoor_SetPreviousVariant);对应的内存布局在 GridEntityDoor.zhl 中定义:_previousState位于偏移0x284、_previousVariant位于偏移0x288(uint32_t)。因为 getter/setter 直通原生字段,Lua 侧读写即原生侧读写,可用于检测门的类型/变体切换(例如普通门与隐藏门之间的形态变化)。
三、GetExtraSprite():以函数形式访问附加精灵
原版门对象持有变量ExtraSprite(附加精灵,例如门框装饰、谜题门特效),但 REPENTOGON 以getter 函数GetExtraSprite()的方式暴露它:
---@return Sprite local sprite = door:GetExtraSprite()返回类型为 Sprite。实现上,LuaGridEntityDoor.cpp 直接取出结构体偏移0x168处的ANM2 _extraSprite并作为 Sprite userdata 压栈返回,因此你可以对返回的 Sprite 继续调用SetFrame、Play、ReplaceSpritesheet等常规 Sprite API,实现自定义门的附加贴图与动画。
四、PlayAnimation():播放门的动画
door:PlayAnimation()该函数没有参数、没有返回值(void),用于主动播放门对象的动画。LuaGridEntityDoor.cpp 中的实现直接调用原生方法gridEnt->play_animation():
LUA_FUNCTION(Lua_GridEntityDoor_PlayAnimation) { GridEntity_Door* gridEnt = lua::GetLuabridgeUserdata<GridEntity_Door*>(L, 1, lua::Metatables::GRID_ENTITY_DOOR, "GridEntityDoor"); gridEnt->play_animation(); return 0; }play_animation()是游戏原生函数(在 GridEntityDoor.zhl 中通过__thiscall签名定位)。注意:前面提到的PreviousState/PreviousVariant刷新时机之一就是"播放动画",所以调用PlayAnimation()后这两个字段会被重新计算,模组可以在播放前后分别读取,借此捕获门动画切换带来的形态变化。
五、与门相关的回调(GridEntity Door callbacks)
门是 REPENTOGON 网格渲染/更新回调体系的一等公民。在 CustomCallbacks.cpp 中,GridEntity_Door的Render被挂接了HOOK_GRIDRENDER_CALLBACKS,对应两个回调 ID;在 CustomCallbacks.cpp 中,Update被挂接了HOOK_GRIDUPDATE_CALLBACKS:
| 回调 ID | 名称 | 参数 | 返回 |
|---|---|---|---|
| 1446 | MC_PRE_GRID_ENTITY_DOOR_RENDER | (GridEntityDoor Door, Vector Offset) | Vector或boolean |
| 1447 | MC_POST_GRID_ENTITY_DOOR_RENDER | (GridEntityDoor Door) | void |
| 1402 | MC_PRE_GRID_ENTITY_DOOR_UPDATE | (GridEntityDoor Door) | boolean |
| 1403 | MC_POST_GRID_ENTITY_DOOR_UPDATE | (GridEntityDoor Door) | void |
以上参数/返回信息来自 ModCallbacks 枚举文档。PRE 回调返回Vector可改写渲染偏移,返回boolean(false)可取消默认渲染;UPDATE 的 PRE 回调返回boolean可决定是否跳过原生更新逻辑。示例:
mod:AddCallback(ModCallbacks.MC_PRE_GRID_ENTITY_DOOR_UPDATE, function(_, door) if door.Variant == SomeDoorVariant then return false -- 跳过原生日志,执行自定义逻辑 end end)六、结合 DoorSlot / DoorMask 与门放置逻辑的实际应用
要写出门相关的模组逻辑,通常还需要理解门的"槽位"概念。门的槽位(DoorSlot)取值为 0~7(左/上/右/下,加上第二轮LEFT1/UP1/RIGHT1/DOWN1),每个槽位对应一个位掩码,定义在 DoorMask 枚举文档中:
| 值 | 枚举 | 值 | 枚举 |
|---|---|---|---|
1 << 0 | LEFT0 | 1 << 4 | LEFT1 |
1 << 1 | UP0 | 1 << 5 | UP1 |
1 << 2 | RIGHT0 | 1 << 6 | RIGHT1 |
1 << 3 | DOWN0 | 1 << 7 | DOWN1 |
Lua 侧的同名枚举在 enums_ex.lua 中以1 << DoorSlot.LEFT0的形式定义。C++ 侧 RoomPlacement.h 提供了DoorSlotToMask(doorSlot) = 1 << doorSlot的等价转换。
实战:把门补进现有房间
Level 文档 给出了一段完整可用的示例:遍历当前房间 0~7 的每个门槽位,用位掩码判断该槽位是否允许放门(roomDesc.Data.Doors & (1 << doorSlot) ~= 0),若当前没有门(not room:GetDoor(doorSlot))则随机生成一个 1x1 房间并通过TryPlaceRoomAtDoor把连接门补上:
mod:AddCallback(ModCallbacks.MC_USE_ITEM, function(_, id, rng, player, flags, slot) local level = game:GetLevel() local room = game:GetRoom() local roomDesc = level:GetCurrentRoomDesc() local success = false for doorSlot = 0, 7 do local doorSlotAllowed = roomDesc.Data.Doors & (1 << doorSlot) ~= 0 if doorSlotAllowed and not room:GetDoor(doorSlot) then local seed = rng:Next() local requiredDoors = 15 -- 要求新房间 4 个槽位都有门,确保放得下 local roomConfig = RoomConfig.GetRandomRoom(seed, true, Isaac.GetCurrentStageConfigId(), RoomType.ROOM_DEFAULT, RoomShape.ROOMSHAPE_1x1, nil, nil, nil, nil, requiredDoors) if roomConfig then local newRoom = level:TryPlaceRoomAtDoor(roomConfig, roomDesc, doorSlot, seed, true, false) if newRoom then success = true end end end end if success then return {ShowAnim = true} else return {Discharge = false} end end, MY_ACTIVE_ITEM_ID)这段代码在门生成成功后返回{ShowAnim=true}(播放门的动画)——恰好对应本文第四节PlayAnimation()所触发的原生动画路径。
底层:门是如何被实体化的
从源码结构可以推断,门的生成由房间放置系统驱动。在 RoomPlacement.cpp 中,当新房间与玩家当前房间相邻时,通过currentRoom->MakeDoor(returnDoorSlot)创建门实体,随后写入目标房间索引door->_targetRoomIdx、调用SetRoomTypes(CurrentRoomType, TargetRoomType)设置两侧房间类型,并在房间帧数大于 0 时调用door->play_animation()播放开门动画:
currentRoom->MakeDoor(returnDoorSlot); GridEntity_Door* door = currentRoom->GetDoor(returnDoorSlot); if (door) { door->_targetRoomIdx = roomDesc->SafeGridIndex; door->SetRoomTypes(neighborDesc->Data->Type, roomDesc->Data->Type); if (currentRoom->GetFrameCount() > 0) { door->play_animation(); } }GetDoor(slot)的原生签名(__thiscall GridEntity_Door* Room::GetDoor(int slot))定义在 Room.zhl 中,这与上面 Lua 示例里的room:GetDoor(doorSlot)一一对应。从这段实现可以看出,SetRoomTypes与play_animation是门实例化过程中的关键步骤,而_previousState/_previousVariant正是在这类初始化与动画播放的路径上被刷新。
七、使用注意事项小结
- 类型语义:
PreviousState/PreviousVariant现在是int,不要再对它们做 userdata 解包;setter 仅接受整数。 - 更新时机:这两个字段只在门子类初始化或播放动画时更新,跨帧持续读取时值可能不变,不应将其当作实时状态量。
- ExtraSprite 是共享引用:
GetExtraSprite()返回的是门对象内部的ANM2 _extraSprite引用(偏移0x168),修改它会直接影响门自身的附加精灵渲染,谨慎用于全局替换。 - 回调与放置联动:门的渲染/更新回调(1446/1447、1402/1403)与
DoorMask位掩码、TryPlaceRoomAtDoor等放置 API 配合使用,可以做出"动态开新门""自定义门动画""门不可见/不可通行"等玩法效果。
参考路径汇总
- 类文档:docs/docs/GridEntityDoor.md
- 门枚举:docs/docs/enums/DoorMask.md
- 回调枚举:docs/docs/enums/ModCallbacks.md
- 门 Lua 绑定实现:repentogon/LuaInterfaces/GridEntities/LuaGridEntityDoor.cpp
- 门原生结构体与函数定位:libzhl/functions/GridEntityDoor.zhl
- 门渲染/更新回调挂接:repentogon/LuaInterfaces/CustomCallbacks.cpp
- 房间门生成逻辑:repentogon/LuaInterfaces/Room/RoomPlacement.cpp
- Lua 侧 DoorMask 枚举:repentogon/resources/scripts/enums_ex.lua
- 游戏开发
- 插件系统
【免费下载链接】REPENTOGON
Script extender for The Binding of Isaac: Repentance
相关推荐
REPENTOGON 扩展 API 实战指南:EntityBomb 炸弹实体类完整解析
REPENTOGON 扩展 API 实战指南:EntityBomb 炸弹实体类完整解析 导读 本文聚焦 REPENTOGON 为《以撒的结合:忏悔》Lua Mo
游戏开发插件系统REPENTOGON Lua API 扩展全览:800+ 新函数、200+ 新回调与 60+ 新枚举的文档导航与实践指南
REPENTOGON Lua API 扩展全览:800+ 新函数、200+ 新回调与 60+ 新枚举的文档导航与实践指南 本文面向《以撒的结合:忏悔+》(The
游戏开发插件系统如何 5 分钟给小爱音箱免费装上无限曲库:XiaoMusic Docker 部署与语音点播指南
如何 5 分钟给小爱音箱免费装上无限曲库:XiaoMusic Docker 部署与语音点播指南 XiaoMusic 是一款开源音乐工具:它接管小爱音箱的语音点播
游戏开发插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考