跳到主要内容

15 篇博文 含有标签「Unity」

Unity game development

查看所有标签

跨场景事件:没人聊但人人踩的持久化问题

TinyGiants
GES Creator & Unity Games & Tools Developer

你的 AudioManager 播放背景音乐。它订阅了 OnLevelStart,在玩家进入新区域时切换曲目。你把 AudioManager 放在 DontDestroyOnLoad 对象上保持跨场景存活。开发期间一切正常,因为你一直在同一个场景里测试。

然后有人第一次从关卡 1 加载到关卡 2。音乐不切了。AudioManager 还活着 —— DontDestroyOnLoad 尽职了 —— 但事件订阅没有存活下来。或者更糟:旧的订阅还在,指向关卡 1 里已经被销毁的事件触发方,下次有东西尝试调用它时你会在游戏中途收到一个 MissingReferenceException

这就是持久化问题,每个有多个场景的 Unity 项目迟早都会撞上。

调试不可见的东西:为什么事件系统需要自己的可观测层

TinyGiants
GES Creator & Unity Games & Tools Developer

QA 提了个 Bug:"玩家捡起钥匙后门没开。"

简单吧?大概是缺了个引用或者条件写错了。你打开工程,捡起钥匙,然后……门开了。在你机器上没问题。于是你问测试员复现步骤,他说"大约 30% 的概率出现,通常在存档/读档之后。"

现在你进入了调试地狱。在钥匙拾取事件、背包更新、任务进度检查、和门的解锁条件之间的链路某处,有东西间歇性地失败。但是哪一环?事件没触发?触发了但监听器没订阅?订阅了但条件评估为 false?条件是对的但门的状态在加载后是过期的?

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

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 中每个时间驱动事件的故事。第一版实现很干净。第二个需求把代码翻了一倍。第三个需求让你开始怀疑人生。

并行还是顺序:每个事件系统都需要的两种执行模式

TinyGiants
GES Creator & Unity Games & Tools Developer

玩家死了。死亡音效和死亡粒子应该同一瞬间开始——没必要等一个完了再开始另一个。但屏幕淡出绝对必须在重生点加载之前完成。重生加载必须在传送之前完成。传送必须在屏幕淡入之前完成。

这就是并行和顺序执行同时存在于一个流程中,由一个事件触发。而尴尬的现实是:大多数 Unity 事件系统只给你一种模式。触发事件,所有监听器响应,完事。至于这些响应应该同时发生还是严格按顺序来?你自己想办法。

于是你就去解决了。用协程。用回调。用名为 _hasFadeFinished 的布尔值。不知不觉间,你搭了一个散落在六个文件里的临时状态机,包括未来的你在内没人能看懂。

看不见的事件链:你无法调试看不见的东西

TinyGiants
GES Creator & Unity Games & Tools Developer

玩家死了。死亡音效响了。布娃娃激活了。UI 弹出"你死了"。游戏自动存档。数据分析事件发出去了。重生倒计时开始了。六个不同的系统,全部在响应一个事件:OnPlayerDeath。但我的问题是——这些关系记录在哪?

不在你的代码里。不在项目管理工具里。不在任何图表里。它存在于一个地方:最初配置它的那个人的脑子里。如果那个人半年前离职了,那它哪儿都不存在。

这就是事件驱动架构的脏秘密。我们采用它是因为它解耦了系统。我们庆祝 AudioManager 不需要引用 UIManager 了。但我们从不谈代价:执行流变得不可见了。而不可见的东西,从定义上来说,就是没法可视化调试的。

告别 if-else 地狱:可视化条件逻辑的正确打开方式

TinyGiants
GES Creator & Unity Games & Tools Developer

每个游戏说到底就是一大堆条件判断。"只在敌人没有免疫火焰伤害、且玩家有火焰 buff、且暴击判定通过的时候才造成火焰伤害。"在原型阶段,你随手在回调里写个 if 就继续了。三十秒搞定,能跑,感觉效率很高。

然后原型进入正式开发。那些三十秒写的 if 语句开始疯狂繁殖。一个变五个,五个变五十个,五十个变成"第二关 Boss 的掉落概率到底在哪个鬼条件里控制的?"然后你的策划站在你身后问能不能把一个伤害阈值从0.3改成0.25,你在解释这得重新编译。

欢迎来到 if-else 地狱。常住人口:每一个活过三个月的 Unity 项目。

策划不写代码也能配事件:设计师与程序员的协作问题

TinyGiants
GES Creator & Unity Games & Tools Developer

周二下午三点,你的策划凑过来说:"诶,玩家受到50点以上伤害的时候,屏幕震动能不能再猛一点?打击音效延迟半秒再播?还有中毒的跳伤改成1.5秒一次吧,2秒太慢了。"

三个改动。从策划的角度来看,可能也就十五秒的思考量。但实际上会发生什么呢:你关掉 Scene 视图,打开 IDE,等它加载,搜索伤害处理的代码,找到屏幕震动强度那个值——埋在某个方法里面。改掉。然后找音效延迟——在另一个类里。改掉。再找中毒协程——又在另一个类里,而且 tick 频率藏在 WaitForSeconds 的参数里。改掉。保存三个文件。切回 Unity。等重新编译。测试。

八分钟后,策划说:"算了,震动还是原来的好,中毒能不能试试1.8秒?"