☰
REPENTOGON GridEntityDoor 扩展指南:门对象 PreviousState/PreviousVariant 修复与新 API 实战
2026/10/16 2:26:31 网站建设 项目流程
  • 游戏开发
  • 插件系统

【免费下载链接】REPENTOGON

Script extender for The Binding of Isaac: Repentance

项目地址:https://gitcode.com/gh_mirrors/re/REPENTOGON
点击查看免费下载

本文面向 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),并且支持读写。

变量类型说明
PreviousStateint与默认行为相同,但现在返回正确的整数值而非 userdata
PreviousVariantint与默认行为相同,但现在返回正确的整数值而非 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名称参数返回
1446MC_PRE_GRID_ENTITY_DOOR_RENDER(GridEntityDoor Door, Vector Offset)Vector或boolean
1447MC_POST_GRID_ENTITY_DOOR_RENDER(GridEntityDoor Door)void
1402MC_PRE_GRID_ENTITY_DOOR_UPDATE(GridEntityDoor Door)boolean
1403MC_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 << 0LEFT01 << 4LEFT1
1 << 1UP01 << 5UP1
1 << 2RIGHT01 << 6RIGHT1
1 << 3DOWN01 << 7DOWN1

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正是在这类初始化与动画播放的路径上被刷新。

七、使用注意事项小结

  1. 类型语义:PreviousState/PreviousVariant现在是int,不要再对它们做 userdata 解包;setter 仅接受整数。
  2. 更新时机:这两个字段只在门子类初始化或播放动画时更新,跨帧持续读取时值可能不变,不应将其当作实时状态量。
  3. ExtraSprite 是共享引用:GetExtraSprite()返回的是门对象内部的ANM2 _extraSprite引用(偏移0x168),修改它会直接影响门自身的附加精灵渲染,谨慎用于全局替换。
  4. 回调与放置联动:门的渲染/更新回调(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

项目地址:https://gitcode.com/gh_mirrors/re/REPENTOGON
点击查看免费下载

相关推荐

上一篇:Sunshine:打造个人游戏串流服务器的终极指南
下一篇:Sunshine游戏串流完整指南:5分钟搭建免费个人云游戏平台

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询