跳转到内容

能力系统 review

专家们普遍认同的 5 个核心思维模式

Section titled “专家们普遍认同的 5 个核心思维模式”

所有资深技能系统架构师都认同:战斗逻辑应由数据表/配置驱动,而非散落在代码各处的 if-else。这使得策划能独立迭代,减少程序员参与每个技能的实现。GAS、《Dota2》的 KV 系统、《英雄联盟》后期重构都走向了这个方向。

用层级化标签(而非枚举/硬编码 ID)作为系统间的通信协议,是被广泛验证的模式。标签的松耦合特性允许新机制在不修改已有代码的前提下接入系统。从 GAS 的 GameplayTag 到 Unity 的 Tag 系统,这一理念已成为行业标准。

“瞬时效果”与”持续修改器”的正交分离是公认的正确抽象。将一次性动作(扣血、位移)与持续状态(加攻 buff、减速 debuff)在架构上隔开,避免了生命周期管理的混乱。这对应文档中 Effect(无状态原子指令)与 Status+Behavior(有生命周期的状态容器)的分离。

伤害/治疗等核心流程应该是可注入的管线(Pipeline),而非单一函数调用。Pre/Post 阶段的拦截点允许任意 buff 以解耦方式篡改或响应结算过程。这是 GAS 的 GameplayEffectExecutionCalculation、《暗黑3》的伤害管线、以及几乎所有现代 ARPG 的共识。

技能逻辑层不应直接引用任何视觉/音频资源。通过间接标识符(Cue)让客户端自行映射资源,这在网络同步、跨平台、性能分级等场景下是刚需。没有任何资深架构师会主张逻辑和表现耦合。


专家们存在根本分歧的 3 个方面

Section titled “专家们存在根本分歧的 3 个方面”

分歧 1:声明式配置 vs. 脚本式灵活性

Section titled “分歧 1:声明式配置 vs. 脚本式灵活性”

核心争论:系统应该是一个”配置引擎”还是一个”脚本虚拟机”?

声明式阵营(本文档的立场)的论点:

  • 配置可验证、可序列化、可工具化。策划在编辑器中组装预定义节点,编译期即可发现错误。
  • 约束就是安全——不允许策划写出死循环、访问非法内存或产生竞态条件。
  • 网络同步简单:配置 ID + 参数即可重建,不需要同步脚本执行状态。

脚本式阵营的论点:

  • 任何足够复杂的声明式系统最终都会重新发明一门编程语言——但比真正的语言更差(无调试器、无IDE支持、无堆栈追踪)。文档中的 FloatValue.MathCondition.And/Or/NotEffect.Conditional/Repeat/Sequence 已经在构建一个表达式语言和控制流引擎了。
  • 当策划需要”如果目标在3秒内受到了来自两个不同施法者的火焰伤害,则触发爆燃”这类跨时序多条件逻辑时,纯声明式会导致配置极度嵌套且难以理解。
  • Lua/Blueprint 等成熟脚本方案有完整的调试生态,而自建 DSL 的工具链投入往往被低估。

对本文档的质疑:

“你的 Effect 已经有 ConditionalRepeatSequenceWithLocalVar——这就是一个没有循环变量、没有函数定义、没有调试器的图灵不完备语言。为什么不直接嵌入 Lua,把 Effect 退化为一组’安全 API’供脚本调用?你节省了复杂度还是创造了复杂度?“


分歧 2:“万能统一模型” vs. “专用子系统”

Section titled “分歧 2:“万能统一模型” vs. “专用子系统””

核心争论:所有游戏机制是否都应该通过同一套 Status/Effect/Trigger 管线来表达?

统一模型阵营(本文档的立场)的论点:

  • 正交组合的表达力是指数级的。一套原子指令 + 组合规则可以覆盖 buff、debuff、被动、光环、DOT、护盾等所有机制。
  • 维护一套系统的成本远低于维护 N 套专用系统。Bug 修复和优化只需做一次。
  • 策划理解一套规则就能配置所有内容,学习曲线仅在入门时陡峭。

专用子系统阵营的论点:

  • “护盾”不是”buff”——它有吸收量、优先级、破盾事件、多护盾叠加策略等独有语义。在统一模型中表达这些需要大量约定俗成的 Tag 组合和 Trigger 链,而专用护盾组件可以用 3 个字段清晰表达。
  • 性能热点集中于少数机制(如伤害结算、AOE扫描)。专用系统可以针对性优化数据布局(SoA vs AoS)、缓存策略、并行化,而万能管线的通用遍历路径很难做到这一点。
  • 统一模型的调试噩梦:当一个 bug 出现在”5层嵌套的 Trigger 链在特定堆叠策略下的溢出回调中触发了一个内联 Status 的 Periodic 行为”时,排查路径极长。专用系统的调用栈更短更可预测。

对本文档的质疑:

“你的 resolution_pipeline 里的 AllocationLayer 本质上是一个硬编码的护盾系统——先扣护盾再扣血。但如果我需要’按比例分摊到护盾和血量’、‘只吸收魔法伤害的护盾’、‘受到伤害时增厚而非减薄的反转护盾’呢?你最终要么不断扩展 AllocationLayer 直到它变成又一个微型脚本引擎,要么承认这里需要一个专用护盾子系统。AllocationLayerconversionRate: float 这个单一标量字段,能支撑多少种护盾语义?“


分歧 3:编译期安全 vs. 运行时灵活

Section titled “分歧 3:编译期安全 vs. 运行时灵活”

核心争论:系统应该在多大程度上允许”运行时才能发现的错误”?

严格静态验证阵营的论点:

  • 所有配置引用(Tag、Stat、Event)都应在加载时验证。->gameplaytag 这类外键引用是好的开始,但远远不够。
  • FloatValue.PayloadMagnitude 在没有 Payload 上下文时使用是静默的运行时错误。系统应在配置加载时就能通过类型分析检测出”这个 Effect 不在任何 Trigger 下,但使用了 Payload 相关节点”。
  • 策划配错了 TargetSelector.PayloadInstigator 用在 OnApply 里(没有 Payload),应该是编译错误,不是运行时空指针。

运行时容错阵营的论点:

  • 过度的静态验证会让配置系统变成类型体操。策划不是程序员,他们需要的是”填错了弹个对话框告诉我哪里错了”,而不是”类型系统不让我保存”。
  • 游戏开发是迭代式的。今天的”非法配置”可能是明天的”合法用法”。过早收紧约束会阻碍创新。
  • 运行时空值检查 + 详细日志 + 回退默认值,在实践中比编译期类型系统更经济。

对本文档的质疑:

“你在文档中用加粗红色警告写了’策划配置红线:带有 Payload 的节点只能在带有事件上下文的 Effect 执行链中使用’。但你的类型系统无法表达这个约束——Effect 接口在结构上不区分’有 Payload 的 Effect’和’无 Payload 的 Effect’。OnApplyeffect: EffectTriggereffect: Effect 类型签名完全相同。你把一个结构性安全问题降格为文档中的口头约定,这在任何有 10+ 策划的团队中都会在第一周就被违反。”

ArgCapture 的存在本身就是对类型系统不足的补丁——如果系统能在类型层面区分’有 Payload 上下文’和’无 Payload 上下文’,ArgCapture 的一半用例就不需要存在。策划需要理解’Payload 的传递边界在 ApplyStatus 处终止’这一隐含的作用域规则——这不是配置,这是编程。”

“更根本地说:FloatValue 是多态联合类型,可以是 ConstStatValuePayloadMagnitudeContextVar 中的任何一种。但不是所有变体在所有位置都合法。你的 Ability.cooldown 声明为 ActorFloatValue 而非 FloatValue——说明你意识到了这个问题并创建了一个受限子类型。但你只做了这一处。StatusCore.duration 仍然是 FloatValue,意味着策划可以填 PayloadMagnitude,而这在 Periodic tick 重新求值时将指向一个不存在的 Payload。你的类型边界划分是不完整的。“


维度共识分歧焦点
数据驱动✅ 一致同意驱动到什么程度——配置 vs. 脚本?
标签系统✅ 一致同意标签应承担多少语义——纯标记 vs. 行为载体?
Effect/Status分离✅ 一致同意是否所有机制都应通过统一管线表达?
管线化结算✅ 一致同意管线的扩展点应该是配置节点 vs. 脚本钩子?
逻辑/表现隔离✅ 一致同意(此处无显著分歧)
配置 vs. 脚本声明式节点树 vs. 嵌入式脚本语言
统一 vs. 专用万能组合模型 vs. 专用子系统集合
静态 vs. 动态安全编译期类型约束 vs. 运行时容错+日志

分歧 1:声明式配置 vs. 脚本式灵活性

Section titled “分歧 1:声明式配置 vs. 脚本式灵活性”

我的判断:本文档选择了正确的起点,但需要承认它的天花板

声明式是对的,但质疑也是真实的。 关键在于认清这不是二选一,而是分层问题。

本文档实际上已经在构建一门领域特定语言(DSL)。ConditionalRepeatSequenceWithLocalVar 就是控制流原语。但它与 Lua 之间有一个被忽略的根本性区别:

声明式节点树是可序列化、可遍历、可静态分析的结构化数据。
脚本是不透明的执行流。

这个区别的实际后果是:

能力声明式节点树嵌入脚本
编辑器可视化拖拽✅ 天然支持❌ 需要额外抽象层
网络同步✅ 配置ID+参数⚠️ 需同步执行状态或结果
静态依赖分析(“哪些Buff影响了火系伤害”)✅ 可遍历AST❌ 停机问题
热更/配置下发✅ 数据替换⚠️ 安全沙箱成本高
表达跨时序复杂逻辑❌ 极度嵌套✅ 自然
调试体验⚠️ 取决于工具投入✅ 成熟生态

核心洞察: 文档中 95% 的技能配置不会超出当前节点树的表达力。真正需要脚本的是那 5% 的 Boss 机制和跨系统联动。正确的策略不是在两者中选一个,而是:

  1. 保持当前声明式架构作为主干——它覆盖了绝大多数需求
  2. 当你发现策划需要在节点树中”模拟编程”时,不要给他们编程能力——给他们一个新的、封装了该模式的专用节点。

分歧 2:“万能统一模型” vs. “专用子系统”

Section titled “分歧 2:“万能统一模型” vs. “专用子系统””

统一模型的价值是真实的。 当策划问”我能不能做一个随时间衰减的护盾,衰减速度受施法者智力影响,破碎时对周围敌人造成伤害”时,统一模型可以用现有原语直接组合出来,不需要程序员写一行代码。这是专用子系统做不到的。

护盾系统AllocationLayer 当前的 conversionRate: float 确实太简陋。但正确的做法不是做一个完整的护盾子系统,而是让 AllocationLayer 支持条件过滤:

struct AllocationLayer {
targetStat: str ->stat_definition;
conversionRate: FloatValue; // 从 float 升级为 FloatValue,支持动态计算
allowOverflow: bool;
// 新增:该层只吸收满足条件的伤害
filter: TagQuery; // 如:只吸收 Damage.Element.Magic
onHitCue: list<str> ->cue_key;
onDepletedCue: list<str> ->cue_key;
}

分歧 3:编译期安全 vs. 运行时灵活

Section titled “分歧 3:编译期安全 vs. 运行时灵活”

我的判断:文档有一个结构性的类型安全漏洞,但解决方案不是更多的类型——而是更好的上下文标注

质疑者说得完全正确:OnApplyeffect: EffectTriggereffect: Effect 类型签名相同,但运行时语义根本不同。 这不是小问题——这是系统可维护性的定时炸弹。

但质疑者提出的隐含方案——“创建 EffectWithPayloadEffectWithoutPayload 两种类型”——也行不通。因为 Effect 是递归树结构:

Sequence [
ModifyStat { ... }, // 不需要 Payload
WithTarget {
target: PayloadInstigator, // 需要 Payload!
effect: ResolveCombat { ... }
},
Conditional {
condition: PayloadHasTag { ... }, // 需要 Payload!
thenEffect: ModifyStat { ... } // 不需要 Payload
}
]

Payload 依赖不是在 Effect 级别——而是在 Effect 内部的叶节点级别。要做完整的类型安全,需要对整棵树做类型推导——这实质上是在构建一个类型检查器/编译器。

务实的解决方案:三层防线

第一层:上下文枚举标注(配置时)

不改变 Effect 的类型定义,而是在”挂载点”上标注可用上下文:

interface Behavior {
struct Trigger {
// 引擎知道:这个 effect 的执行环境有 Payload
// 编辑器据此放开 PayloadMagnitude 等节点的使用
effect: Effect; // @context: HAS_PAYLOAD
}
struct OnApply {
// 引擎知道:这个 effect 的执行环境没有 Payload
// 编辑器据此禁用 PayloadMagnitude 等节点
effect: Effect; // @context: NO_PAYLOAD
}
}

这不是类型系统层面的约束,而是编辑器层面的约束。策划在编辑器中配置 OnApply 的 Effect 时,PayloadMagnitudePayloadInstigator 等节点根本不会出现在可选列表中。

第二层:加载时静态扫描(构建管线)

在配置加载/构建阶段,对 Effect 树做一次完整的 DFS 扫描:

class EffectValidator {
enum ContextCapability { HAS_PAYLOAD, NO_PAYLOAD }
List<ValidationError> validate(Effect effect, ContextCapability cap) {
// 递归遍历 Effect 树
// 当 cap == NO_PAYLOAD 时,遇到任何 Payload* 节点 → 报错
// 当遇到 ApplyStatus → 子 Status 的 Behavior 中的 OnApply
// 递归校验时 cap 切换为 NO_PAYLOAD
// 当遇到 ApplyStatus → 子 Status 的 Behavior 中的 Trigger
// 递归校验时 cap 切换为 HAS_PAYLOAD
}
}

这不需要修改任何配置表结构——纯粹是工具链的一部分。它能在策划点”保存”或 CI 流水线中捕获 100% 的 Payload 作用域违规。

第三层:运行时防御性编程(最后兜底)

class FloatValues {
static float evaluate(FloatValue cfg, Context ctx, Payload payload) {
if (cfg instanceof PayloadMagnitude) {
if (payload == null) {
LOG.error("PayloadMagnitude evaluated without Payload! " +
"Status={}, Behavior={}", ctx.debugSource(), cfg.debugPath());
return 0f; // 安全回退,不崩溃
}
return payload.magnitude;
}
// ...
}
}

著名游戏在三大分歧上的实际抉择

Section titled “著名游戏在三大分歧上的实际抉择”
游戏分歧1:配置 vs. 脚本分歧2:统一 vs. 专用分歧3:静态安全 vs. 运行时灵活
UE GAS混合:蓝图+配置高度统一运行时为主
Dota 2混合:KV配置+Lua统一框架+专用逃逸运行时容错
英雄联盟脚本为主(早期)→ 数据驱动重构(后期)早期专用→后期统一逐步收紧
暗黑破坏神3/4高度数据驱动统一管线+专用子系统强静态验证
魔兽世界混合演化深度专用运行时为主
Path of Exile高度数据驱动极致统一运行时+大量测试
原神配置为主+硬编码逃逸统一框架+元素专用编辑器约束
守望先锋脚本化(Statescript)专用优先强类型脚本

分歧 1:声明式配置 vs. 脚本式灵活性

Section titled “分歧 1:声明式配置 vs. 脚本式灵活性”

Dota 2 的技能系统是业界讨论最多的混合案例。

配置层(KV 文件):所有技能的基础属性、数值、标准行为都在 KeyValue 文件中声明式定义。这套系统覆盖了大量标准模式——线性弹道、范围伤害、Buff 挂载、属性修改:

"fireball"
{
"AbilityBehavior" "DOTA_ABILITY_BEHAVIOR_UNIT_TARGET"
"AbilityUnitDamageType" "DAMAGE_TYPE_MAGICAL"
"AbilityDamage" "100 175 250 325"
"AbilityCooldown" "10"
"AbilityManaCost" "100 120 140 160"
}

脚本层(Lua):当 KV 系统的标准行为无法表达需求时,技能可以挂载 Lua 脚本。Lua 拥有完整的 API 访问权限——读写属性、创建单位、操作物理引擎、自定义伤害流程。

实践中的比例:Dota 2 的约 120+ 英雄、500+ 技能中,估计 60-70% 的技能逻辑可以纯 KV 表达,30-40% 需要不同程度的 Lua 介入。复杂英雄如 Invoker、Rubick(偷技能)、Morphling(变身复制)几乎完全依赖 Lua。

Valve 学到的教训:Dota 2 自定义游戏(Custom Games)社区大量使用 Lua,产生了丰富的实战数据。社区反馈的最大痛点不是”Lua 太灵活”,而是”KV 系统和 Lua 之间的边界不清晰”——有些事情两边都能做,但做法不一样,文档也不统一。

关键洞察:Dota 2 证明了混合模型是可行的,但也证明了边界管理的成本往往被低估。

暗黑破坏神 3/4:数据驱动的极致

Section titled “暗黑破坏神 3/4:数据驱动的极致”

暴雪在 GDC 的多次演讲中详细分享了暗黑3的技能系统架构。

暗黑3采用了一套叫做 PowerScript 的内部数据驱动系统(注意:它叫 “Script” 但本质是声明式配置,不是通用脚本语言)。它是一套专用的领域语言,拥有有限的控制流原语,但不是图灵完备的。

这套系统的覆盖率极高——暗黑3中几乎所有技能、符文变体、传奇词缀都通过 PowerScript 配置。极少数情况需要程序员介入写 C++ 代码。

暴雪能做到这一点的关键是:他们投入了大量人力建设工具链——可视化编辑器、实时预览、自动化测试框架。声明式系统的可用性严重依赖工具质量,暴雪有资源做到这一点。

关键洞察:暗黑3证明了声明式系统可以走得极远,但前提是匹配的工具链投入。没有工具的声明式系统比脚本更难用。

守望先锋:Statescript——强类型脚本的第三条路

Section titled “守望先锋:Statescript——强类型脚本的第三条路”

守望先锋走了一条独特的路线。他们开发了 Statescript——一种为游戏逻辑定制的、强类型的、可视化的状态机脚本语言。

Statescript 既不是纯配置也不是通用脚本,而是一种受约束的可视化编程环境

  • 强类型:变量必须声明类型,连线有类型检查
  • 状态机模型:每个英雄的技能逻辑被组织为状态机,状态间的转换有明确的触发条件
  • 确定性:没有闭包、没有动态类型、没有副作用隐藏,便于网络同步和回放

关键洞察:守望先锋证明了存在第三条路——不是”配置 vs. 脚本”的二选一,而是”设计一门受约束的领域语言”。但这条路的开发成本极高,只有暴雪这个量级的团队才能负担为单一项目从头构建一门语言及其工具链。

英雄联盟:从脚本到数据驱动的痛苦迁移

Section titled “英雄联盟:从脚本到数据驱动的痛苦迁移”

LoL 的技能系统经历了显著的架构演化:

早期(2009-2015):每个英雄的技能几乎都是独立的 C++/Lua 代码。这让早期开发极度灵活——设计师可以为每个英雄写完全定制的逻辑。但随着英雄数量增长到 130+,维护成本爆炸。“改一个底层系统可能破坏 20 个英雄”成为常态。

中后期重构:Riot 逐步引入了更多数据驱动的基础设施——统一的 Buff 系统、标准化的伤害管线、配置化的弹道系统。但由于历史包袱,这个迁移是渐进的,新老系统长期共存。

关键洞察:LoL 是”脚本优先”策略长期后果的活教材。早期的灵活性换来了后期的技术债。但反过来说,LoL 的早期成功也部分得益于这种灵活性——在产品验证阶段,快速迭代比架构优雅更重要。

纯声明式 ◄──────────────────────────────────────► 纯脚本
│ │
D3/PoE 本文档 Dota2 OW LoL早期
(PowerScript) (节点树) (KV+Lua) (Statescript) (C++/Lua)

本文档处于光谱偏左的位置,接近暗黑3的路线。这个定位对于中等规模团队、明确的游戏类型是合适的。但需要警惕暗黑3模式的隐含前提——充足的工具链投入。


分歧 2:统一模型 vs. 专用子系统

Section titled “分歧 2:统一模型 vs. 专用子系统”

PoE 的技能系统是”万物皆 Modifier”哲学的极致实践。

PoE 的几乎所有游戏机制——技能、天赋、装备词缀、药剂、光环、诅咒——都通过统一的 Modifier 系统表达。一个”增加 50% 火焰伤害”的效果,无论它来自天赋树、装备词缀还是 Buff,在底层都是同一种 Modifier 对象,用相同的 Tag 系统(PoE 叫 “stat”,但概念等同于本文档的 Tag+Stat)进行分类和交互。

这种极致统一带来的好处是惊人的涌现性——PoE 玩家发现的无数 Build 组合,很多连设计师都没预料到。系统的表达力来自组合爆炸,而非逐个设计。

但代价同样显著:

  • 性能:PoE 的”伤害计算卡顿”是社区长期抱怨的问题。当一个技能触发几十个 Modifier、每个 Modifier 又触发连锁反应时,单帧计算量可以非常大
  • 可理解性:PoE 的 Modifier 交互规则极度复杂,催生了 Path of Building 等第三方计算工具——玩家需要外部工具才能理解自己的 Build 到底做了什么
  • Bug 排查:当所有东西都是 Modifier 时,Bug 的表现形式是”某个数值不对”,但根因可能藏在 50 层 Modifier 链的任何一环

关键洞察:PoE 证明了极致统一模型可以产生惊人的设计空间,但需要接受性能和可调试性上的代价。PoE 的成功有一个重要前提——它是服务器权威的 ARPG,客户端表现的偶尔卡顿是可接受的。如果是格斗游戏或 FPS,这种性能特征是致命的。

WoW 走了与 PoE 相反的路线。经过近 20 年的演化,WoW 拥有大量专用子系统

  • 光环(Aura)系统:独立的 Buff/Debuff 管理器,有专用的驱散分类(魔法、疾病、诅咒、毒药)、优先级、互斥组
  • 法术学校(Spell School)系统:火焰、冰霜、暗影等有硬编码的交互规则
  • 护甲/减伤系统:独立的减伤公式,不走通用 Modifier
  • PvP 韧性/适应系统:完全独立的伤害缩放层
  • 仇恨系统:独立的仇恨表管理器
  • 神器/特质/精华/灵魂绑定/…:每个版本引入新的专用子系统

这种架构的好处是每个子系统可以针对性地深度优化——200 人团本中仇恨系统的性能是经过极致优化的,不可能用通用 Modifier 管线达到同样的效率。

代价是系统间的交互变得脆弱。WoW 的历史上充满了”这个 Buff 和那个系统没有正确交互”的 Bug。每次版本引入新子系统,都需要逐一检查它与所有已有系统的交互。

关键洞察:WoW 的专用路线是在 20 年持续迭代中有机演化出来的,不是预先设计的。每个专用子系统都是为了解决前一个版本的具体问题而引入的。这是”没有人在第一天就能预见所有需求”这一现实的直接体现。

暗黑3/4:统一管线 + 关键路径专用

Section titled “暗黑3/4:统一管线 + 关键路径专用”

暗黑3取了中间路线。核心战斗管线(伤害计算、属性修改、Buff 管理)是统一的,但在几个关键点做了专用优化:

  • 伤害管线是配置化的多阶段管线(与本文档的 resolution_pipeline 思路非常接近)
  • 冷却系统是独立的——不走 Buff 管线
  • 资源系统(怒气、法力等)有专用组件
  • 套装效果有专用的触发框架

暗黑4在此基础上进一步走向数据驱动,将更多逻辑从 C++ 迁移到配置层,但保留了关键路径的专用优化。

关键洞察:暗黑3的策略是”默认统一,按需特化”——只有当性能 profiling 或复杂度分析明确指出某个子系统是瓶颈时,才将其拆出为专用实现。这是最务实的策略。

原神:统一框架 + 元素反应专用

Section titled “原神:统一框架 + 元素反应专用”

原神的技能系统在底层是相对统一的(基于类 GAS 的 Ability/Effect 框架),但在元素反应系统上做了显著的专用化处理。

元素反应(蒸发、融化、超载等)涉及:

  • 元素附着量的衰减模型(独立的浮点衰减系统)
  • 反应优先级矩阵(哪个元素先附着影响反应类型)
  • 反应系数(独立的乘区设计)
  • 内置冷却(ICD)系统(限制元素附着频率)

这些如果用通用 Status + Trigger 表达,配置复杂度和运行时性能都不可接受。原神选择在底层引擎中硬编码元素反应的核心逻辑,上层通过配置调整数值。

关键洞察:当一个子系统有自己独特的”物理规则”(如元素附着衰减)而非简单的数值修改时,专用实现几乎是必然选择。统一模型擅长处理”同质化的数值修改”,不擅长处理”异质化的规则交互”。

极致统一 ◄──────────────────────────────────────► 极致专用
│ │
PoE D3 本文档 原神 WoW
(万物Modifier) (管线统一 (Status/ (统一+元素 (深度专用)
+关键路径 Effect统一 专用)
专用) +管线配置)

本文档处于中间偏统一的位置,接近暗黑3。这是一个合理的起点——可以根据实际游戏的需求向两端调整。


分歧 3:编译期安全 vs. 运行时灵活

Section titled “分歧 3:编译期安全 vs. 运行时灵活”

守望先锋 Statescript:强类型的标杆

Section titled “守望先锋 Statescript:强类型的标杆”

Statescript 在类型安全上做到了游戏行业的最高标准:

  • 所有变量有明确类型声明
  • 节点间的连线有编译期类型检查
  • 状态机的转换条件在编译期验证可达性
  • 网络同步属性有专门的标注和验证

这使得守望先锋的技能 Bug 大多是逻辑错误(数值调错、时序不对),而很少是类型错误(空指针、类型转换失败)。

代价:开发 Statescript 及其编辑器、编译器、调试器的工程投入巨大。这是一个只有暴雪规模的团队才能承受的前期投入。

Dota 2 的 KV 系统几乎没有编译期验证。配置错误(引用不存在的 Ability、类型不匹配的参数)在运行时才会暴露,通常表现为技能不触发或服务器日志中的错误。

自定义游戏(Custom Games)社区对此深有体会——新手制作者最常见的问题就是”技能配了但没效果”,原因是 KV 文件中的某个键名拼写错误,系统默默忽略了它。

Valve 的应对策略:

  • 丰富的运行时日志
  • 社区驱动的验证工具(如 ModDota 社区的 linter)
  • 容错性设计(错误不崩溃,只是功能缺失)

关键洞察:Dota 2 的运行时容错策略在开放生态中是合理的——Custom Games 的制作者水平参差不齐,系统必须健壮到”怎么配都不崩溃”。但对于内部开发团队,这种策略会降低效率,因为 Bug 的反馈周期太长。

暗黑3 PowerScript:编辑器约束 + 加载验证

Section titled “暗黑3 PowerScript:编辑器约束 + 加载验证”

暗黑3的策略值得本文档重点参考:

  1. 编辑器层面:PowerScript 有专用的可视化编辑器,编辑器中只展示当前上下文合法的选项。策划不会看到”不应该用”的节点。

  2. 加载验证层面:游戏启动时对所有 PowerScript 配置做完整性检查——引用验证、类型验证、可达性分析。错误以醒目方式报告,开发版本中可以选择阻断启动。

  3. 运行时防御:通过验证的配置在运行时仍有空值检查和边界保护,但这是最后一道防线,不是主要的错误发现途径。

关键洞察:暗黑3证明了”编辑器约束 + 加载验证”的双层策略是最具性价比的——它不需要在配置 Schema 层面做复杂的类型系统设计,但仍然能在策划犯错的第一时间给出反馈。

PoE:运行时为主 + 海量自动化测试

Section titled “PoE:运行时为主 + 海量自动化测试”

GGG(PoE 开发商)在多次访谈中提到,他们的 Modifier 系统的正确性主要靠大量自动化测试保证:

  • 每个 Modifier 有预期输出的单元测试
  • 关键的 Modifier 交互组合有集成测试
  • 每次改动后跑回归测试套件

这种策略在 PoE 的极致统一模型下是合理的——因为 Modifier 的交互空间太大,编译期的静态分析不可能覆盖所有组合。测试才是唯一可靠的验证手段。

关键洞察:当系统的涌现性是核心价值时(如 PoE),过度的编译期约束会抑制组合空间。此时运行时验证 + 测试覆盖是更合适的策略。

强编译期 ◄──────────────────────────────────────► 纯运行时
│ │
OW D3 本文档 PoE Dota2
(Statescript (编辑器约束 (外键引用 (测试驱动) (运行时容错
强类型) +加载验证) +文档警告) +社区工具)

本文档当前处于中间偏右的位置——有外键引用验证,但 Payload 作用域等关键约束只停留在文档层面的”红线警告”。最值得参考的目标是暗黑3的位置——向左移动到”加载验证”这个层次,不需要走到守望先锋的强类型编译器。


无论哪个游戏,在三个分歧上的选择都遵循以下规律:

  1. 团队规模决定能走多远:守望先锋的 Statescript、暗黑3的 PowerScript 工具链,都是大团队才能负担的投入。中小团队应该选择投入产出比最高的点进行投资,而非全面铺开。

  2. 游戏类型决定偏向哪边

    • 强对抗/电竞(OW、LoL)→ 倾向确定性、强类型、专用优化
    • 开放构建/涌现玩法(PoE、Dota2 Custom)→ 倾向灵活性、统一模型、运行时容错
    • 内容驱动/PvE(D3、原神)→ 中间路线,按需特化
  3. 所有成功游戏都经历过架构演化:没有任何一个游戏在第一天就做对了所有选择。LoL 从脚本迁向数据驱动,WoW 从简单系统演化出大量专用子系统,PoE 持续重构其 Modifier 管线。本文档作为架构基准,最重要的品质不是”一开始就完美”,而是为演化留出空间

基于以上分析,本文档应明确自己的设计立场:

本系统采用声明式节点树 + 定向扩展节点的表达策略(接近暗黑3 PowerScript),默认统一、按需特化的架构策略(接近暗黑3),以及编辑器约束 + 加载期验证的安全策略(暗黑3层次,非守望先锋层次)。通用脚本能力作为最后手段预留扩展点,但不作为推荐路径。