主人公"帅春"是一个猎人,他的技能是向敌人丢斧头,有一天,他击杀猎物被猎人之神赐福,进入了一种玄之又玄的顿悟状态,醒来后已经领悟了一种并不稳定的技能:
他丢出的斧子,有
15%的概率被附着上切割之力,命中敌人后,敌人会在接下来的10秒内,每2秒损失5点生命值。

好的,背景交待完了,身为一个成熟的程序员,我们开始尝试在 Godot 里编写代码实现逻辑。
1. 当然是选择 Buff 系统
面对这个需求,我们的第一反应通常是:
"这不就是一个典型的 Buff 吗?"。
Buff 系统是 RPG 和许多动作游戏中不可或缺的一部分,它用来管理角色身上临时或永久的状态变化。
因此,我们尝试构建如下的结构:

给 玩家、投射物、敌人 都绑定一个名为 buff容器 的节点。
Buff容器 节点的作用主要为:
管理其宿主的所有buff(包括负面buff),计算它们的时间,挂载,层数,移除等,并提供便捷的接口给给代码使用。
并且,我们需要设计3个 Buff:

ok,我们先编写一个名为 BaseBuff 的基类,它具备如何能力:
- 声明自己的基础属性:
- 是否永久的
- 持续时间
- 对层数的处理策略(可叠加,还是刷新等)
- 声明自己的基本类型
- 属性增强?
- 概率触发?
- 事件触发?
- 时间间隔触发?
- 实现触发时的专属业务逻辑(当然,作为 BaseBuff,它没有自己的专属业务逻辑,只是预留一个函数,方便继承者在声明基础信息后,直接覆写该方法)
因此,作为惯性,最本能的写法就是去继承,我们需要写如下三个代码(伪代码):
gdscript
# 切割射击buff
class ABuff extends BaseBuff:
永久 = true
类型 = 事件触发
触发事件 = 攻击
触发概率 = 0.15
执行方法(context):
context.子弹.add_buff(BBuff.new())
# 切割弹buff
class BBuff extends BaseBuff:
永久 = true
类型 = 事件触发
触发事件 = 命中
触发概率 = 1.0
执行方法(context):
context.目标.add_buff(CBuff.new())
# 流血buff
class CBuff extends BaseBuff:
永久 = false
持续时间 = 10.0
类型 = 间隔触发
触发间隔 = 2.0
触发概率 = 1.0
执行方法(context):
context.目标.take_damage(5.0)
ok,大功告成,代码看起来非常完美,我真是一个经验丰富的程序员!
2. "继承" 的代价是什么?
但是。世事就怕这个但是。
但是,第二天,策划找到了我,并掏出了一份策划方案,告诉我,他给"猎人之神"创建了20种+不同的赐福类型,方便每个玩家在每局游戏都能对"主角帅春"进行不同路线等等强化,例如:
- 每3次攻击,就会一定会让斧子被附魔,命中后,敌人在8秒内,每2秒损失10点生命值。
- 每3秒,猎人就会给自己恢复20点生命。
- 每5秒,猎人会随机给一个敌人释放A技能。
- 每4秒,猎人会随机给一个敌人释放B技能。
- 每3秒,猎人会随机给一个敌人释放C技能。
......等等
我了个!这意味着我不得不放下手里正在进行的其他重要工作,配合策划,开始编写如上几十个buff的代码,毕竟这每个buff都需要声明属性,并且实现它的实现放方法:
gdscript
执行方法(context):
本buff干了啥
更操蛋的是,万一未来策划要微调属性,也还是得找到我,我来修改,毕竟这实现和赋值都在继承体内部。
太爆炸了,未来如果技能上百个了,或者上千个了怎么办?
我们发现,继承是一种非常强的(IS-A)关系。BleedBuff 是一个 BaseBuff。这种关系一旦确定,就很难再改变。当一个事物需要同时具备多种不同维度的特性时,继承就会变得异常笨拙,导致类爆炸 或者形成庞大而复杂的继承树。
或者,我们应该换一个思路,正如人们常说的一句话:
正如人们常说的一句话。 ------ 人们常说
不好意思,开个玩笑,重新来。
正如人们常说的一句话:
组合大于继承。
那么,是如何大于的呢?它又应该如何工作呢?
答案是抽象。
3. "组合"是更狠的抽象
抽象是编程的基础思想。
其实"继承" 过程中我们已经进行了一定程度的抽象。
正如Java 或者 C++ 老师所讲的,所有动物都继承了Animal,但是:
Dog.say() 和 Cat.say() 的实现是不同的,因此,Dog 和 Cat 通过继承实现了多态。

我们思考了哪些属性是大部分Buff都必须考虑的共性,并且产生了一个思维惯性:
执行逻辑的也是
Buff,因此我们应该继承BaseBuff,利用多态的特性来满足不同的需求。
但"组合"必须跳开这个思路,产生更为细的抽象:

实现差异的可能并不是
动物不同,而是动物持有的发声器不同。
在这样的抽象细粒度里,动物的特性可以通过茫茫多不同的组合来实现。

再引入一些配置化的手段,以上图中就可以产生 3^4 = 81 种不同形态的动物了,这要是让你写继承,不得写崩溃?
因此,组合的抽象步骤一般分为两步:
- 拆分成更细粒度的功能,并抽象成接口。
- 实现不同的功能单元。
后续使用者可以不必再是代码,而是配置,哪怕是完全不懂代码的人也可以胜任。
4. 重构Buff成组合模式
实现
Buff业务逻辑的不一定是Buff本身。
让我们按这个逻辑来进行抽象,现在我们有了一个"实现Buff具体逻辑"的工具接口"效果器":
gdscript
class_name BaseEffect:
func execute(context):
pass
每当Buff需要执行逻辑时,只需要找到Buff实例上挂载的"效果器"并执行 execute 方法即可。

让我们尝试来抽象一下,以上例子里有哪些效果器呢?
- 效果器A:给目标施加一个新Buff。
- 效果器B:对目标造成伤害。
咦?竟然这么简单?是的。接下来就只用配置属性和组合。
gdscript
# 切割射击buff
class ABuff extends BaseBuff:
永久 = true
类型 = 事件触发
触发事件 = 攻击
触发概率 = 0.15
效果器 = 效果器A
施加的Buff = BBuff
# 切割弹buff
class BBuff extends BaseBuff:
永久 = true
类型 = 事件触发
触发事件 = 命中
触发概率 = 1.0
效果器 = 效果器A
施加的Buff = CBuff
# 流血buff
class CBuff extends BaseBuff:
永久 = false
持续时间 = 10.0
类型 = 间隔触发
触发间隔 = 2.0
触发概率 = 1.0
效果器 = 效果器B
伤害 = 5.0
现在我们看到,整个步骤已经无需写代码了,只需要配置,配置,配置。
让我们把这些配置抽取成 godot 里的 Resource 资源,或者弄一个 Json 文件,或者弄一个标准化的 Excel 表格,便可以交给策划来进行日常维护了。
5.结论:为什么组合比继承更"高级"?
回到最初的问题。
说组合比继承"高级",并不是说继承一无是处。继承在建立明确、稳定、层次分明的类型关系时非常有用(例如,Button 继承自 Control,Control 继承自 Node)。
但在构建复杂且多变的游戏系统(如技能、Buff、道具系统)时,组合的优势是压倒性的:
-
高度灵活性 (Flexibility):你可以动态地组合行为,而不是在编译时就用继承关系写死。想创造一个新Buff?只需要在编辑器里拖拽几个效果资源,调整一下参数即可,完全无需编写新的类。
-
高内聚,低耦合 (High Cohesion, Low Coupling):每个 BuffEffect 只关心自己的逻辑(高内聚)。BuffComponent 不知道也不关心效果的具体内容,它只负责管理Buff的生命周期(低耦合)。这使得代码更容易维护和测试。
-
避免类爆炸 (Avoids Class Explosion):对于 N 个效果,如果想实现它们之间的任意组合,继承可能需要创建 2^N - 1 个子类。而使用组合,你只需要 N 个效果类。
-
更符合数据驱动的设计:使用 Godot 的 Resource,游戏设计师(甚至是不会编程的策划)可以直接在编辑器里创建和调整各种Buff,极大地提高了开发效率和迭代速度。
总而言之,继承告诉我们一个对象"是什么",而组合则定义了一个对象"能做什么"。在需要灵活插拔、组合功能的游戏开发领域,优先考虑组合,而不是继承,这会让你在面对不断变化的需求时,游刃有余。