跳到主要内容

9 篇博文 含有标签「Architecture」

Game architecture and design patterns

查看所有标签

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

你一直在每次测试 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

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

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

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

当你的项目有 200 个事件:为什么组织管理会崩溃

TinyGiants
Unity Games & Tools Developer

你开了一个新 Unity 项目,创建了十个事件。OnPlayerDeathOnScoreChangedOnLevelComplete。命名合理,扔进一个文件夹,继续干活。日子很美好。你脑子里装得下整个事件结构。

快进六个月。你有了 200 个事件。Project 窗口变成了一面 ScriptableObject 文件墙。你需要 OnPlayerHealthDepleted——还是叫 OnPlayerHPLow?还是 OnPlayerHealthZero?你在列表里上下滚动,眯着眼看一堆全以 OnPlayer 开头的名字。三分钟后你放弃了,直接创建了一个新的,因为你甚至不确定你要的那个事件是不是已经存在了。

每个事件驱动的 Unity 项目最终都会走到这一步。不是因为事件模式本身有问题,而是因为没人构建过大规模管理事件的工具。Unity 给了你 Animation 窗口、Shader Graph、Timeline、Input System 调试器。事件呢……只有 Project 窗口。

零反射、零 GC:"高性能"事件系统到底意味着什么

TinyGiants
Unity Games & Tools Developer

Unity Asset Store 上每一个事件系统插件的描述里都写着"高性能"。就夹在"易于使用"和"完整文档"中间。但问题是——1ms 和 0.001ms 对人来说都很快,可一个比另一个慢了一千倍。当一个插件说"高性能"时,到底在说什么?跟什么比?怎么测的?

我以前也不在意这些。大多数人都不在意。接几个事件,游戏在开发机上跑得好好的,发布就完了。但后来我做了一个移动端项目,几百个实体各自监听多个事件,突然"高性能"就不再是营销打勾项了——而是 60 FPS 和幻灯片之间的区别。

这篇文章讲的是"高性能"对于事件系统到底应该意味着什么、为什么大多数实现达不到、以及 GES 如何通过 Expression Tree 编译实现接近零开销。用真实数据说话,不打太极。

Unity 泛型序列化之墙:类型安全的事件不该有样板代码税

TinyGiants
Unity Games & Tools Developer

你写了一个 GameEvent<T>。干净、类型安全、优雅。你创建了一个 GameEvent<float> 字段用来广播血量变化,打上 [SerializeField]。切到 Inspector 一看——字段消失了。就像你让 Unity 除以零一样,它用一片空白面板回敬你。

这是 Unity 最古老的架构痛点。序列化系统不懂泛型,从来没懂过。每一个试图构建类型安全、数据驱动事件系统的开发者都一头撞上了这堵墙。

这不是什么小麻烦,而是那种会毒化整个架构的限制。你要么放弃类型安全,要么淹没在样板代码里,要么接受你那漂亮的泛型设计永远无法触及 Inspector。多年来社区的标准答案一直是"手写具体类就行了"。但问题来了——如果样板代码是 100% 可预测的,为什么要人来写?

告别隐形意大利面:为什么你的事件系统正在拖垮项目

TinyGiants
Unity Games & Tools Developer

你改了一个方法名。就一个——把 OnPlayerDied 改成了 OnPlayerDefeated,因为策划觉得措辞需要柔和一点。点击 Play,什么都没发生。没有编译报错,没有警告。场景里十个通过 Inspector 用 UnityEvent 绑定的对象就这么……哑了。悄无声息。你可能三天后才从 QA 那里听到,更惨的情况是玩家先发现。

如果你觉得这场景似曾相识,恭喜——你已经亲身领教过"隐形意大利面代码"了。这种技术债不会出现在 IDE 里,不会触发编译器警告,也不会出现在任何依赖关系图上。它就这么潜伏着,等着在最要命的时候给你一刀。

这不是水平问题,是架构问题。而且比大多数 Unity 开发者愿意承认的要普遍得多。