- 游戏开发
- 插件系统
【免费下载链接】REPENTOGON
Script extender for The Binding of Isaac: Repentance
本篇技术指南基于 REPENTOGON 官方示例文档 Beams 示例,系统讲解如何用Beam类与Point点序列绘制会动态移动、拉伸的光束(如 Gello 脐带、Gehenna 球链敌人),并完整演示一个"生命吸取射线" Mod 的实现:从RegisterMod注册、逐帧伤害结算,到SpritesheetCoordinate纹理裁剪原理与屏幕坐标换算,读完即可独立编写基于 BeamRenderer 的自定义渲染效果。
Beam 是什么:只管渲染的底层类
Beam是对游戏内部BeamRenderer的高层封装,游戏用它绘制各种绳索/锁链类特效——Evis、Gello、Vis Fatty 等敌人的脐带都走这条渲染路径。正如文档 Beam 类参考 中强调的:
Beams are used internally for rendering cords... Note that thisonly handles renderingand not physics.
也就是说,Beam是一个纯渲染类,不处理任何物理计算:点之间的连线形态、拉伸与碰撞全部由你自己每帧喂入的点坐标决定。官方文档也明确说明,目前不提供带自动物理调点的扩展版本(原文称后续可能提供,属于未来计划而非现有能力)。
最适合理解 Beam 动态特性的场景是 Gehenna 的球链敌人(Ball and Chain):带刺铁球离锚点越远,锁链就越长,视觉上呈现动态拉伸——这正是通过每帧重新Add点 +Render实现的。
Beam由任意数量的 Point 按添加顺序组成,因此整个 API 的核心就是:构造 Beam → 每帧 Add 若干 Point → Render。
源码剖析:Beam 背后的 C++ 实现
从 BeamRenderer 的 ZHL 定义 可以看到全局单例与渲染三件套:
reference BeamRenderer *g_BeamRenderer; // 开始一束新的 Beam:绑定 ANM2 动画、Layer、overlay 标志 static void BeamRenderer::Begin(ANM2 *anm2, unsigned int layer, bool useOverlay, bool unk); // 追加一个点:pos + Color + SpritesheetCoordinate + Width static void BeamRenderer::Add(Vector *point, ColorMod * color, float spritesheetCoordinate<xmm2>, float width<xmm3>); // 结束并绘制 static void BeamRenderer::End();BeamRenderer结构体自身持有副本ANM2 _anm2(即 Sprite 拷贝)、_layer、_useOverlayData、_unkBool以及点队列deque_Point _points。这与文档中的警告完全对应:
- Sprite 会被拷贝进 Beam:构造函数执行
construct_from_copy,之后要改动画请用Beam:GetSprite拿到的副本(见 Lua 绑定实现 中toLua->_anm2.construct_from_copy(sprite))。 - 点队列使用 std::deque:
Add只做push_back,Render后默认清空,配合每帧重建即形成动态光束。
Point 结构定义 则揭示了每个点真正携带的五个字段:
struct Point depends (Vector, ColorMod) { Vector _pos : 0x0; float _width : 0x8, _spritesheetCoordinate : 0xc; ColorMod _color : 0x10; bool _worldSpace : 0x3c; } : 0x40;这意味着 Lua 侧Point除位置与纹理坐标外,还支持颜色插值(Color在点之间插值)与世界空间标记(IsWorldSpace,渲染时自动把Position从世界坐标换算为屏幕坐标)。
Render 的完整校验流程
Lua_BeamRender 展示了Render(ClearPoints = true)的底层执行逻辑:
if (beam->_points.size() < 2) { // 1. 至少需要两个点 error = 0; goto funcEnd; } if (beam->_useOverlayData) { if (beam->_anm2._overlayAnimState._animData == nullptr) error = 1; // 2. overlay 动画状态必须有效 } else if (beam->_anm2._animState._animData == nullptr) error = 2; // 3. 普通动画状态必须有效 if (!IsValidLayerID(beam->GetANM2(), beam->_layer)) error = 3; // 4. Layer ID 必须在动画范围内 g_BeamRenderer->Begin(beam->GetANM2(), beam->_layer, beam->_useOverlayData, beam->_unkBool); for (auto it = beam->_points.begin(); it != beam->_points.end(); ++it) { Vector posBuffer; if (it->_worldSpace) LuaEngine::Isaac_WorldToScreen(&posBuffer, &it->_pos); // 5. 世界坐标点自动换算屏幕坐标 else posBuffer = it->_pos; g_BeamRenderer->Add(&it->_pos, &it->_color, it->_width, it->_spritesheetCoordinate); } g_BeamRenderer->End(); funcEnd: if (clearPoints) beam->_points.clear(); // 6. 默认清空点队列对应地,errors 表 定义了四条可预期的报错,排查问题时可直接对照:
| 错误信息 | 触发条件 |
|---|---|
Must have at least two points | 点数量少于 2 |
Overlay AnimState is NULL! | 启用 UseOverlay 但动画没有 overlay 状态 |
AnimState is NULL! | 动画未Play或加载失败 |
Invalid layer id | Layer ID 超出动画的 Layer 数量 |
构造函数 Lua_CreateBeamDummy 还有一个关键细节:Layer 参数既接受字符串 LayerName 也接受整型 Layer ID,字符串会通过sprite->GetLayer(name)解析成 ID,解析失败则报Invalid layer name %s;同时会强制设置该 Layer 的_wrapSMode = 0; _wrapTMode = 1;,保证纹理按 Beam 的裁剪方式正确平铺/拉伸。
完整示例:生命吸取射线 Mod
原始示例文档 给出的完整 Mod 可 点此下载(含全部资源),官方演示视频为 beam_damage_ray.mp4(一颗激光"死亡射线"持续焚烧敌人)。以下是带注释的完整代码:
local mod = RegisterMod("Repentogon Beam Example", 1) -- Some constants. local DAMAGE_MULTIPLIER = 0.01 local ITEM_ID = Isaac.GetItemIdByName("Life-Drain Gem") -- First make a sprite. local sprite = Sprite() sprite:Load("gfx/chain_beam.anm2", true) sprite:Play("Idle", true) -- Handles the beam and damaging of the closest enemy to the player. function mod:HandleLifeSteam(player) local data = player:GetData() -- Check if the player has our item, first. if not player:HasCollectible(ITEM_ID) then return end -- Get the beam stored in player:GetData(), or create one if it doesn't exist. local beam = data.LifeStealBeam if not beam then -- Create the beam. We don't need to use the overlay here. data.LifeStealBeam = Beam(sprite, "chain", false, false) beam = data.LifeStealBeam end -- Get the closest enemy local closestDistance, closestEnemy for _, enemy in ipairs(Isaac.GetRoomEntities()) do if enemy:IsActiveEnemy() and enemy:IsVulnerableEnemy() then local distanceToPlayer = enemy.Position:Distance(player.Position) if not closestDistance or closestDistance > distanceToPlayer then closestEnemy = enemy:ToNPC() closestDistance = distanceToPlayer end end end -- If there's no enemy in the room, end the function early. if not closestEnemy then return end -- Deal damage every frame. closestEnemy:TakeDamage(player.Damage * DAMAGE_MULTIPLIER, 0, EntityRef(player), 0) -- Render the beam. The position is in screen coordinates. -- The second argument, SpritesheetCoordinate, is what the Y position of the spritesheet is by the time this Point is reached. -- So for example, two points of 0 and 64 SpritesheetCoordinate will render the sprite starting from y0 to 64. -- An additional Point with a SpritesheetCoordinate of 0 will draw it in reverse from y64 to y0. -- Width is an optional third argument that is a multiplier used to determine the width of the Beam. beam:Add(Isaac.WorldToScreen(player.Position), 0) beam:Add(Isaac.WorldToScreen(closestEnemy.Position), 64) -- 64 is the height of the spritesheet. -- Render the beam. By default, this will clear all points. beam:Render() end mod:AddCallback(ModCallbacks.MC_POST_PLAYER_RENDER, mod.HandleLifeSteam)逐步拆解关键实现点
构造 Beam 并缓存到 player data:
Beam(sprite, "chain", false, false)的四个参数依次为 Sprite、Layer 名称(此处是字符串"chain",底层解析为 Layer ID)、UseOverlay、UnkBool。Beam 实例缓存在player:GetData()的LifeStealBeam字段中,避免每帧重建——这也呼应了"Sprite 被拷贝进 Beam"的设计:Beam 一旦创建,动画状态就绑定在内部副本上。屏幕坐标必须手动换算:
beam:Add(Isaac.WorldToScreen(player.Position), 0)。Add的Position参数是屏幕坐标(默认),实体位置是世界坐标,因此要用Isaac.WorldToScreen转换。当然也可以利用Point的IsWorldSpace标志让引擎自动换算(见下文 API 表)。SpritesheetCoordinate 的本质是纹理 Y 裁切进度:它表示"到达该点时,精灵表应该绘制到哪个 Y 位置"。示例中首点为
0、末端为64(64 恰好是精灵表高度),效果就是沿光束方向把整张纹理从y0铺到y64;如果再追加一个SpritesheetCoordinate = 0的点,纹理会从y64反向画回y0——这一技巧可用于实现来回往复的纹理循环效果。逐帧伤害 + 逐帧重绘:
TakeDamage(player.Damage * 0.01, ...)在MC_POST_PLAYER_RENDER回调中每帧执行,同时beam:Render()默认清空旧点队列,下一帧重新Add——锁链/光束的"动态拉伸"就是这样逐帧合成的。
API 速查:Beam 与 Point
Beam 构造函数与函数(完整继承自 Beam 类文档)
-- 构造函数:Layer 可传 LayerID(int) 或 LayerName(string) Beam(Sprite Sprite, int Layer, boolean UseOverlay, boolean UnkBool) Beam(Sprite Sprite, string LayerName, boolean UseOverlay, boolean UnkBool)注意:构造函数中的
Sprite会被拷贝进 Beam 成为独立对象;要修改动画请使用Beam:GetSprite()返回的副本。
| 函数 | 签名 | 说明 |
|---|---|---|
Add | void Add(Vector Position, float SpritesheetCoordinate, float Width = 1.0) | 追加一个点,按添加顺序保存 |
Add | void Add(Point Point) | 直接追加预构造的Point对象 |
Render | void Render(boolean ClearPoints = true) | 开始→逐点提交→结束绘制;默认清空点队列 |
GetPoints/SetPoints | Point[] | 读取/整体替换当前点表(SetPoints要求至少 2 个点,见 源码校验) |
GetSprite/SetSprite | Sprite | 获取/替换内部 Sprite 副本(SetSprite还可同时更新 Layer 与 UseOverlay) |
GetLayer/SetLayer | int或string | 读取/设置渲染的 Layer;SetLayer会同步重置该 Layer 的 wrap 模式 |
GetUseOverlay/SetUseOverlay | boolean | 是否使用 overlay 动画通道 |
GetUnkBool/SetUnkBool | boolean | 传递给BeamRenderer::Begin的未公开标志位 |
Width参数是光束宽度的乘数,非零值会按比例缩放精灵表宽度,并且在相邻点之间插值,因此可以为 Beam 写出中间粗、两端细的渐变轮廓。
Point 构造函数与函数(完整继承自 Point 类文档)
Point(Vector Position, float SpritesheetCoordinate, float Width = 1.0, Color Color = Default, boolean IsWorldSpace = false)| 函数 | 说明 |
|---|---|
GetPosition/SetPosition | 渲染位置,默认按屏幕坐标解释 |
GetSpritesheetCoordinate/SetSpritesheetCoordinate | 到达该点时精灵表应绘制到的 Y 位置(别名GetHeight/SetHeight已废弃) |
GetWidth/SetWidth | 宽度乘数,点间插值 |
GetColor/SetColor | 该点的颜色(Color对象),点间插值——可实现光束渐变染色 |
GetIsWorldSpace/SetIsWorldSpace | 置为true后,渲染时引擎自动把Position从世界坐标换算为屏幕坐标,无需手动Isaac.WorldToScreen |
实战提示与注意事项
- 坐标系:
Add的Vector是屏幕坐标。示例中用Isaac.WorldToScreen手动转换;若点是用Point对象构造并设置了IsWorldSpace = true,渲染阶段会自动完成换算(见 Lua_BeamRender 中的 worldSpace 分支),两种方式取其一即可。 - 渲染时机:示例挂在
ModCallbacks.MC_POST_PLAYER_RENDER上,让光束渲染在玩家之后(覆盖在玩家之上);若希望光束藏在玩家后面,可换用MC_PRE_PLAYER_RENDER(Beam 类文档中的示例即采用后者)。 - 错误排查顺序:遇到
Render报错先对照四条错误信息——多数情况是"点少于 2 个""动画没 Play""Layer 名写错"这三类。 - 物理要自己算:Beam 不碰撞、不牵引,像锁链一样"拉着"目标移动的效果需要在自己的回调里用向量运算驱动点的位置。
延伸阅读
- Beam 类 API 参考
- Point 类 API 参考
- 官方示例包:repentogon-beam.zip、演示视频 beam_damage_ray.mp4
- Lua 绑定实现:LuaBeamRenderer.cpp、结构定义 BeamRenderer.zhl 与 Point.zhl
- 游戏开发
- 插件系统
【免费下载链接】REPENTOGON
Script extender for The Binding of Isaac: Repentance
相关推荐
Wotr-BD-LR魔能射手射线流Build:地狱烈焰与灼热射线伤害测试
Wotr BD LR魔能射手射线流Build:地狱烈焰与灼热射线伤害测试 Wotr BD LR是《开拓者:正义之怒》的主角Build搜集项目,专注于为玩家提供各
文档教程探索未来渲染:3D高斯散射实时光线场渲染——Splat
探索未来渲染:3D高斯散射实时光线场渲染——Splat 项目简介 在计算机图形学的广阔领域中,我们迎来了一项创新技术——Splat。这个开源项目是一个WebGL
图形学前端Remotion Starburst 放射光线效果实战指南:用 WebGL 在 React 视频中渲染复古星芒
Remotion Starburst 放射光线效果实战指南:用 WebGL 在 React 视频中渲染复古星芒 @remotion/starburst 是 Re
音视频视频AI 应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考