跳到主要内容
TinyGiants
Unity Games & Tools Developer
查看所有作者

GCS 如何用可视化内容创作与 FlowGraph,重新定义卡牌肉鸽游戏的创作方式

TinyGiants
Unity Games & Tools Developer

一条从内容创作、玩法编排到运行时验证的端到端创作链路

GCS 从内容编辑、FlowGraph 到战斗运行与 Monitor 验证的完整工作流

上图展示了使用 GCS 创作一款卡牌肉鸽游戏时的完整工作流,先在左侧的 Editor 中完成卡牌、卡组、角色、敌人、状态与遭遇战的内容创作,再用下方的 FlowGraph 对卡牌效果、状态规则与敌人的意图行为进行玩法编排,随后进入 Play Mode 运行战斗,根据右侧的 Monitor 记录战斗状态与流图行为对创作内容进行优化,由此形成一条从内容设计到玩法验证的端到端卡牌肉鸽游戏的完整闭环

整个 GCS 的核心是 FlowGraph,它是这条工作流的核心骨架,而 Editor 则是六类内容创作的基础,负责定义表现层数据、美术资产、数值与资源引用关系,FlowGraph 继续决定卡牌、状态与敌人在战斗中的触发时机、数据来源、规则变化和表现反馈,为了让这些逻辑能够直接阅读、自由组合并由运行时执行,我把卡牌肉鸽常用的触发、数据、运算、流程、动作、决策和表现能力抽象成了 145 个核心节点

一切从可视化内容创作开始

Game Card Editor 的 Card 模式,包含卡牌列表、字段、Behavior 与卡面预览

Editor 提供了 Card、Deck、Player、Enemy、Status、Encounter 六种内容创作模式,每种模式都包含3个可视化区域,左侧的 List 管理数据库条目,中间的 Inspector 完成内容创作与配置,右侧的 Preview 则同步展示卡面与角色敌人的视觉表现,所有配置被聚合在一个窗口内完成,创作者无需在 Project 窗口里逐个寻找 ScriptableObject,也不必在多个 Inspector 间来回切换

模式负责创作的内容
Card卡牌信息、费用、目标、标签、升级、卡面资源与 Behavior
Deck初始牌组、奖励卡池、卡牌份数与牌组构成
Player角色属性、基础能量、初始牌组、战斗 Prefab 与能量显示
Enemy敌人属性、生命范围、战斗 Prefab、Behavior 与 Intent 摘要
Status状态图标、类型、叠层、衰减规则、层数限制与 Behavior
Encounter参战角色、敌人阵容、奖励牌组、Intent 显示与战斗规则

这六种模式覆盖了一场卡牌战斗所需的主要内容,项目可以替换自己的卡牌 Artwork、状态图标、角色和敌人 Prefab,也可以修改名称、描述、数值等战斗参数

GCS 提供的是内容结构和运行规则,它并不会将项目锁进 Demo 的美术风格、职业设计或数值体系内

可视化内容创作到这里完成的是“游戏里有什么”,而当卡牌需要造成伤害或恢复能量,状态需要在回合开始时结算效果,敌人需要根据生命值来切换行动时,创作就进入了“这些内容该怎样运行”的阶段

Card、Status 与 Enemy 的 Behavior 会从 Editor 直接打开同一套 FlowGraph,玩法创作也从这里真正开始

FlowGraph 是 GCS 构建内容玩法的核心骨架

我没有把 FlowGraph 设计成若干个固定效果的选择器,也没有用少量的万能节点去覆盖所有的逻辑行为,要知道万能节点看起来省事,但当内容量增加以后,节点内就会塞满互相影响的选项,最终难以被复用,GCS 采用的是一整套被精心设计的原子化节点,每个节点只承担一个清晰的职责,这样可以极大的提高节点灵活性,再复杂的逻辑机制也可以通过动态参数端口与连线逐步形成

把十类节点各取一个放进同一张画布后,FlowGraph 的职责划分会变得非常直观,颜色用于区分节点类别,标题直接说明具体用途,Group 正面还保留了从子图暴露出来的 VolumeClipLifetimePrefab 等常用参数

FlowGraph 十类核心节点与可暴露参数的 Group 节点

当前内置的核心节点总数为 145,其中包括 144 个运行时注册节点和 1 个用于组织子图的 Group 节点

类别数量负责的工作
Entry33从出牌、回合、战斗、状态、单位与事件等时机启动规则
Hook18在伤害、护甲、费用、抽牌、状态等结果提交前读取并改写候选值
Get19读取单位、卡牌、状态、牌堆、能量、变量与事件数据
Operator22完成数学、比较、布尔、随机、集合与数据转换
Flow10组织分支、序列、循环、选择、等待、延迟与 Gate
Action27修改生命、护甲、能量、卡牌、牌堆、状态、回合与单位
Intent6构造敌人行动、权重随机、生命阈值、固定序列与次数限制
Event2发出 GCS 内部事件或可选的 GES 事件
FX7请求 SFX、VFX、动画、飘字、位移与镜头反馈
Group1将多个节点封装成可命名、可进入、可继续嵌套的子图
合计145覆盖从触发到反馈的完整卡牌肉鸽玩法语言

这 145 个节点不是 145 种预制卡牌效果,Entry 和 Hook 决定规则何时发生,Get 与 Operator 准备数据,Flow 安排执行路径,Action、Intent、Event 和 FX 交付结果,Group 再为复杂流图构建嵌套层级,得益于节点的原子化设计,同一个 Change HP 可以用于治疗、生命消耗或生命交换等复核场景的使用,同一个 Change Energy 也可以用于回能、扣能或回合资源的规则编排,而无需再为每种卡牌效果再做一个专用节点

一条玩法规则是怎样通过 FlowGraph 实现的

最基础的卡牌 Behavior 可以很短,以随包附带的一张卡牌 Posion Arrow 为例,从 On Card Played 开始,先用 Deal Damage 造成伤害,再播放命中反馈,随后通过 Change Status 施加 Poison,并播放对应的中毒效果,控制线从左到右给出结算顺序,伤害数值、目标、Status 与表现资源都会被保存在相应的节点中

Poison Arrow 依次完成伤害、命中反馈、施加 Poison 与状态表现

这个图把逻辑和表现放在了同一个可读路径中,修改伤害不需要进入中毒代码,替换 VFX 或 SFX 也不会影响数值结算,你可以随意添加和调整节点的编排顺序,这会让创作变得特别有趣且灵活

值得注意的是,这张流图中的 Deal Damage 负责的是标准伤害管线,而 Poison 是一个毒状态,它的周期伤害则属于 Status 自己的 Behavior, Card 流图中控制的仅仅是状态的施加动作本身;这样设计的好处是其他卡牌只要施加同一 Status,便可以直接复用这个状态的持续规则

On Card Played 连接 Change HP 的固定自我治疗 FlowGraph

再比如上面这个流图,恢复生命与能量使用的仍然是这组节点,最短的治疗图只需把 On Card Played 接到 Change HP,将目标设为自身并填入正数即可,同一节点换成负数时也能表达生命消耗

抽牌并获得能量的 FlowGraph,由 Draw Cards 与 Change Energy 顺序组成

同样的,资源组合沿用相同方法,Change Energy 调整当前能量,Draw CardsChange Energy 连在同一条控制路径上,就能表达“抽两张牌并获得一点能量”,节点无需知道这张卡最终叫补给、冥想还是战术准备,它只需要清晰的描述实际发生的动作即可

当效果开始读取战斗状态,固定动作便会转化为动态机制,创作者可以灵活读取当前生命、护甲、能量,或者状态层数、牌堆数量、卡牌标签与战斗变量等数据,经过 Operator 计算、比较或筛选,再将结果接入 Action;也可以用 Branch 按条件选择路径、用 Foreach 逐个结算目标集合、用 Choice 将候选项交给玩家选择,或是通过 Delayed Trigger 把后续动作安排到指定回合等

读取 Marked 层数并通过 Condition 与 Branch 选择抽牌或获得护甲

上图先用 Unit Status Stacks 读取自身的 Marked 状态层数,再由 Condition 与常量 0 比较,并把布尔结果交给 Branch,条件成立时执行 Draw Cards,条件不成立时执行 Change Armor,Get、Operator、Flow 与 Action 在同一张图中分别负责读取数据、完成判断、选择路径和提交结果,这就是固定动作转化为状态驱动机制的原理

一条机制因此可以从“造成 6 点伤害”逐步增长为“筛选带有指定状态的敌人,按状态层数计算伤害,再对其余敌人执行另一档结算,命中后再触发状态与表现”,画布中每增加一步,都有一个能够单独查看和调整的节点,也正是基于这种流图驱动的模式,创作者可以拖入任意的新节点、改变连接、替换数据来源或把新的分支插入现有路径,而不用推翻整张卡的具体实现

按照 Marked 状态拆分敌人并执行两档伤害结算

上图从 All Enemies 取得全部敌人,Filter 选出带有 Marked 状态的目标,Exclude 则得到其余敌人,两组目标分别接入 12 点与 8 点伤害,原本单一的群体伤害由此扩展为差异化结算,如果还要按状态层数计算伤害或在命中后追加状态、VFX 与 SFX,只需继续接入对应的数据与动作节点,而已有的目标筛选和结算路径则可以继续保留

Hook 与 Intent 扩展了 FlowGraph 的能力边界

普通的 Entry 适合表达“某件事情发生以后会做什么”,而对于力量、易伤、费用修改、手牌上限以及状态免疫等行为则是发生在结果提交之前,为了此,GCS 专门设计了 18 个特殊的 Hook 节点来表达这些效果:运行时先给出候选值,通过 Hook 来读取当前值和上下文,经过运算后再使用 Write Hook 来回写(当有特殊规则需要阻止本次处理时,也可以显式取消)

Strength 在伤害提交前读取状态层数并写回候选值

Strength 就是一个典型的 Hook 状态,Damage Dealt Hook 取得本次候选伤害,Status Info 读取 Strength 层数,Math Binary 将两者相加,最后由 Write Hook 回写,所有经过标准伤害管线的卡牌和敌人的意图行动都会得到相同的结果,而无需为每张卡牌主动查询 Strength

聊完了 Hook 我们再来看看 Intent,它解决的是敌人怎样选择、预告并执行行动,6 个 Intent 节点可以组合出各种意图行为,如固定行动、加权随机、生命阈值、行动序列、每场N次和周期行为等

Mire Reaper 使用生命阈值、加权随机与命名 Group 组织敌人意图

上图所示的 Mire Reaper 外层图会先按 40% 的生命阈值切换阶段,再在不同阶段进入各自的加权选择,一般涉及有多段 Boss 战的流图会这么实现,通过将每个候选行动整理成一个 Group 组节点,让外层画布只保留决策结构,使得外层流图的逻辑表达变得更加清晰

Mire Reaper 的 Poison Scythe Group 内层执行子图

双击 Poison Scythe 进入 Group 组节点后,完整的子图行动会继续展开:Leaf Intent 定义玩家看到的攻击意图:造成 14 点伤害、请求命中反馈、施加 2 层 Poison 状态,再播放中毒效果,这种嵌套流图的设计可以极大的提高流图的可读性与可维护性

FX 节点赋予了游戏的战斗表现

当卡牌造成伤害、恢复生命、施加状态后,玩家可能还希望通过动画、特效、音效或是飘字来丰富它们的战斗表现,为此,FX 家族提供了 Play SFXPlay VFXPlay AnimationShow Floating TextFlash UnitSlide UnitShake Camera等节点,它们与伤害、治疗、状态和 Intent 使用同一套控制路径,确保创作者可以灵活控制音效在何时播放、粒子附着在哪个单位、飘字显示哪个结算结果,以及镜头震动需要持续多久等

由 SFX、VFX、飘字与镜头震动组成的战斗表现流图

用 145 个内置节点组合数以亿计的玩法机制

节点的价值来自组合,145 个核心节点仅在 4 个有序位置中的理论排列数就已超过 4.4 亿种,实际 FlowGraph 还会继续引入参数、数据来源、目标、分支、循环、Hook、Intent 和 Group 层级,系统会自动排除端口类型或拥有者上下文不匹配的连接,因此,4.4 亿只是用来说明节点组合规模的理论值,并不等同于实际可用的玩法总数,不过这个数字已经足以说明,原子节点进入组合体系后,能够表达的机制远超任何预制效果清单

绝大多数常见的战斗规则都可以直接通过内置的节点组合完成,而无需编写新的节点,创作者可以访问 GCS 的在线文档,我在那里提供了 103 份可直接查看的 FlowGraph 配方,其中包括 52 份卡牌配方、30 份状态配方和 21 份敌人配方,它们覆盖了从选牌、抽弃牌,到费用花费、状态联动、伤害修改、额外回合、复活,以及Boss 阶段、召唤、牌堆干扰与跨图事件等一系列的经典机制,给出了《杀戮尖塔》、《月圆之夜》一类经典卡牌肉鸽玩法的具体流图参考

流图配方

每一份配方中都列出了流图效果、节点连接与参数配置,创作者可以直接理解它们是怎样工作的,再替换目标、数值、状态、资源和分支,逐步构建属于自己的机制体系,不过坦率地讲,绝大多数项目的玩法机制均可通过内置的 145 个节点来实现,确需专属规则时,GCS 仍允许开发者注册自定义的 FlowGraph 节点,并继续使用同一套端口、执行上下文与编辑器工作流来参与创作

对 FlowGraph 创作细节与操作体验的极致打磨

节点字段适合保存内容创作时已经确定的值,例如 6 点伤害、2 层 Poison 或指定 VFX Prefab,需要让数值在运行时计算时,支持动态输入的字段可以点击右侧端口开关,将固定参数直接暴露为输入端口

Deal Damage 与 Play Effect 中的固定字段和动态输入端口切换

上图所示,字段未连接时继续使用节点中保存的值,当连接上游数据后,本次执行会读取端口结果,例如 Deal Damage.Amount 可以接入力量层数、当前护甲、已消耗能量或牌堆数量等,固定伤害与动态伤害仍然使用同一个节点,区别只在数值从哪里获取,这项设计让参数来源从静态配置扩展到了实时战斗数据,又不会让基础卡牌多出不必要的连线,这便是动态参数端口的设计理念

同一个 Deal Damage 节点在默认状态与自定义名称、端口顺序下的对照

随着流图规模的不断扩大,节点连线开始变得复杂,很容易形成交叉连线,从而影响视觉观感与逻辑梳理,因此,我对节点、字段以及端口的显示名称做了特殊设计,允许创作者通过双击的方式来重命名,创作者完全可以将节点名称改成当前机制更容易理解的表达,端口顺序也支持调整,通过上下拖动改变同侧端口的排列顺序,就能显著减少数据连线交叉的情况

选中 Deal Damage 后展开的节点参考抽屉

创作者不必先记住每个节点的输入、输出和参数细节,可以通过按下 Tab 来展开右侧节点参考栏;当你选中任意节点后,参考栏便会完整显示节点的具体功能、输入与输出端口、参数含义、默认值和相关运行时机等,当你第一次使用 Deal Damage、Hook 或 Intent 等节点时,可以在画布内直接核对,而无需离开当前 Behavior 窗口去查询在线文档

FlowGraph Editor 的 Keyboard Shortcuts and Help 面板

工具栏右侧的 ? 会展示有关流图操作的所有快捷键,以便创作者能够更好、更快的实现流图设计

Group 让复杂流图保持清晰可读

节点越丰富,越需要一种可靠的层级组织方式,在 GCS 中,选中任意两个以上的节点便可将他们封装成一个 Group 组节点,跨越选区的输入和输出会自动转为 Group 边界端口,原有的执行关系也将会被保留,而无需在创建 Group 节点后重新手动布线

前面的 Mire Reaper ,以及用于组织战斗表现的 Play Effect(Shake) 都是一个 Group,它把 Play SFX、两段 Play VFXShow Floating TextShake Camera 收进子图,父图只用于外部表现及其必要的输入展示,父子图之间可以来回穿梭,已有 Group 还能继续和其他节点组成更高一层的 Group,我没有设置可嵌套的层数上限,因此,对于大型的 Behavior ,是可以按照“战斗阶段、机制模块、具体动作”来逐层进行整理的

group

Group 外层仍然可以直接调整,内部没有继续连接的端口会形成边界端口,创作者还可以选择需要暴露在 Group 正面的参数,经常调整的伤害、状态层数、VFX、SFX、Prefab 或持续时间可以留在外层,而对于那些只服务于内部结构的系统参数则可以继续隐藏在子图中,这样既能减少父图的噪声,又不必为了修改一个常用数值而反复进入内部,这对于想要灵活控制流图嵌套的创作者来说会是一个福音

Monitor 让游戏的运行状态可被追溯

FlowGraph 配置完成后,创作者可以进入 Play Mode,通过 Monitor 来观测这些规则在真实战斗中的运行状态,Monitor 提供了 Battle、Pile、Unit、Phase、Gate、Flow 与 Event 七个页面,分别用于查看战斗快照、牌堆变化、单位状态、阶段推进、等待项、Behavior 执行和事件记录等

Game Card Monitor 的 Battle 页面,集中显示当前战斗状态与数值变化

上图以 Battle 页面为例,当前阶段、回合、Encounter、已打出的卡牌数量、玩家生命与能量、敌人生命、状态以及本次攻击和治疗结果都集中在了同一个页面中,创作者可以直接确认一张卡实际造成了多少伤害、一项状态持续了几个回合、敌人的行动强度是否符合预期,以及整场战斗的资源消耗是否合理

当结果偏离设计预期时,则可以继续切换到 Pile、Unit、Phase、Gate、Flow 或 Event,确认卡牌移动、状态变化、阶段等待、Behavior 入口与事件触发是否正确,再回到 Editor 和 FlowGraph 调整卡牌费用、伤害、治疗、护甲、状态层数、敌人生命和行动强度,然后重新运行战斗并对比结果,这个循环让游戏的数值、战斗节奏与难度曲线的设计建立在游戏实际的运行结果之上,极大的缩短机制玩法的数值平衡与调试周期

这就是 GCS 重新定义的创作方式

Editor 让卡牌、卡组、角色、敌人、状态与遭遇战拥有统一的可视化内容创作入口;FlowGraph 用 145 个内置节点将玩法机制拆解成可阅读和组合的内置语言;内置运行时执行同一份 Behavior;Monitor 则集中呈现游戏的运行状态、数值变化和执行记录,这四部分共同构成了 GCS 的端到端卡牌肉鸽游戏的创作体验

FlowGraph 是我在 GCS 中投入最多设计与打磨的系统,它把卡牌肉鸽这类游戏最重要的策略机制与规则组合交还给了创作者:节点可以自由连接,参数可以接入动态数据,名称和端口可以按项目语义整理,复杂逻辑可以进入可嵌套的 Group,完成后的机制还可以通过 Monitor 持续观测和调整

当你亲手用它构建几张卡牌、一个持续状态或一名多阶段的敌人后,对这套创作方式的理解也会更加深入,你可以从最简单的伤害和治疗开始,逐步加入状态联动、Hook、敌人决策、表现反馈和属于项目自己的玩法体系,并将这些内容持续维护到一款能够商业化发行的卡牌肉鸽游戏中,让整个创作、验证与调整的过程始终在同一套工作流中完成

更进一步,GCS + AI-Agent的开发模式或将会成为一种全新的创作体验,GCS 中的内容与流图本身就是结构化、可执行并能够被验证的创作资产,因此你可以让 AI-Agent 在明确的边界内参与其中,以加速内容创作生成、规则编排与玩法迭代的速度,如果你对这种创作模式感兴趣,或是想要了解更多有关GCS的设计细节,欢迎加入 TinyGiants 官方 Discord 社区参与讨论~!


TG官方矩阵

Official Website: https://tinygiants.tech

Documentation: https://tinygiants.tech/docs/gcs

Asset Store: https://tinygiants.tech/gcs

Discord: https://tinygiants.tech/discord/gcs

Youtube: https://tinygiants.tech/youtube/gcs

Unity Forum: https://tinygiants.tech/forum/gcs

可视化与 API 事件分层:可扩展项目的最佳实践指南

TinyGiants
Unity Games & Tools Developer

随着项目规模增长,我最常听到的问题之一是:"我应该用可视化工具还是脚本 API?" 答案是两者都用——但知道什么场景下各自最有效,才是区分整洁可扩展事件架构与最终不堪重负架构的关键。

在观察了各种规模团队使用 GES 的方式后,我想分享一套分层方法,无论你有 50 个事件还是 500 个,都能保持项目的可维护性。

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

TinyGiants
Unity Games & Tools Developer

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

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

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

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

TinyGiants
Unity Games & Tools Developer

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

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

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

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

TinyGiants
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
Unity Games & Tools Developer

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

你怎么连接这些事件?

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

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

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

TinyGiants
Unity Games & Tools Developer

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

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

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

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

TinyGiants
Unity Games & Tools Developer

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

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

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

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

TinyGiants
Unity Games & Tools Developer

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

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

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

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

TinyGiants
Unity Games & Tools Developer

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

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

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