跳到主要内容

4 篇博文 含有标签「Scripting API」

C# runtime API

查看所有标签

上线才发现的事件系统坑:内存泄漏、数据污染、递归陷阱

TinyGiants
GES Creator & Unity Games & Tools Developer

你一直在每次测试 5 分钟。跑得好好的。然后 QA 提了个 Bug:"30 分钟游玩过程中内存持续增长。加载 6 个场景后帧率从 60 降到 40。"你去 Profile。一个应该只有 12 个监听器的事件上注册了 847 个。每次场景加载都添加了新的订阅但从未移除旧的。对象被销毁了,但它们的委托引用还活着,把已经死掉的 MonoBehaviour 钉在内存里,垃圾回收器碰都碰不到。

或者这个:"第二次进入 Play Mode 后血量数值不对。第一次运行没问题。"你按 Play,测战斗,停止。再按 Play,玩家以 73 HP 开始而不是 100。上一个会话的 ScriptableObject 状态泄漏了,因为没人重置它。

再或者经典的:游戏卡了 3 秒,然后 Unity 崩溃。事件 A 的监听器触发了事件 B,事件 B 的监听器触发了事件 A。栈溢出。但有时候它不崩溃 —— 只是卡住,在一个不产生任何可见错误的死循环里吃 CPU。

这些不是假设。这些是我见过的上线游戏里的真实 Bug。根本原因都一样:事件系统的模式单独看没问题,但到了规模化的时候就崩了。

运行时构建事件流:当可视化编辑器不够用的时候

TinyGiants
GES Creator & Unity Games & Tools Developer

你的程序化地牢生成器刚刚造了一个有三块压力板和一个尖刺陷阱的房间。下一个房间是一个连接着锁门的拉杆谜题。再下一个是 Boss 竞技场,环境危害根据 Boss 的血量阶段激活。这些事件关系在编辑期一个都不存在。地牢布局取决于玩家 30 秒前输入的种子。

你怎么连接这些事件?

传统做法是写一个巨大的 switch 语句。每种房间类型手动订阅和取消订阅事件处理器。每种 AI 难度手动串联不同的攻击模式。每个 Mod 创建的内容手动解析配置文件并翻译成事件连接。"手动"就是问题所在 —— 每当拓扑在运行时改变,你就在重新实现事件连线逻辑。

可视化节点编辑器在处理设计期已知的流程时非常棒。但它们从根本上无法处理运行前根本不存在的流程。而越来越多最有趣的游戏系统恰恰就是事件图动态生成的那种。

执行顺序的隐患:'谁先响应'背后的 Bug

TinyGiants
GES Creator & Unity Games & Tools Developer

玩家受到 25 点伤害。血量系统扣血。UI 刷新血条。但血条显示的是 100 而不是 75。你盯着代码看了 20 分钟才反应过来:UI 的监听器比血量系统的监听器先执行了。UI 读到的是旧值,渲染完了,血量系统才完成扣血。等数据正确的时候,这一帧已经画完了。

你刚刚发现了"执行顺序 Bug"。如果你用事件驱动架构上过线,八成已经在不知情的情况下发布过好几个这样的 Bug。它们在测试环境下表现正常 —— 因为脚本恰好按正确顺序初始化了 —— 然后到了线上就炸,因为 Unity 换了个加载顺序。

这不是什么边缘情况,而是大多数事件系统的结构性缺陷 —— 包括 Unity 的 UnityEvent 和标准 C# event 委托。一旦搞明白为什么,你就再也回不去了。

时间驱动事件:为什么协程不适合做延迟和循环

TinyGiants
GES Creator & Unity Games & Tools Developer

你需要在手雷落地后延迟2秒引爆。挺简单的,你写了个协程。IEnumerator DelayedExplosion(),yield return new WaitForSeconds(2f),调用爆炸逻辑。整整齐齐大概10行。感觉还不错。

然后策划说:"玩家应该可以拆弹。"好了,现在你得存一个 Coroutine 引用才能调 StopCoroutine()。但等等——如果玩家在协程启动之前就拆了呢?需要空检查。如果 GameObject 在等待中途被销毁了呢?又一个空检查。如果玩家恰好在协程完成的那一帧拆弹呢?竞争条件。你的10行现在变成了25行,而你甚至还没处理"显示拆弹消息 vs 显示爆炸"的分支逻辑。

这就是 Unity 中每个时间驱动事件的故事。第一版实现很干净。第二个需求把代码翻了一倍。第三个需求让你开始怀疑人生。