三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity为啥不使用继承?——一场“血统枷锁“的技术反思

Unity为啥不使用继承?——一场“血统枷锁“的技术反思

引子:一个"程序员的噩梦"

想象一位游戏程序员,在项目的第一天,兴致勃勃地设计了一套优雅的类结构:

Character(角色) ├── Player(玩家) ├── Enemy(敌人) └── NPC(村民)

他抚摸着这套整洁的继承树,感到无比满足——“多么优雅!多么符合直觉!”

第二天,策划走进来:“我们需要一个’会飞的敌人’”。

他从容地加了一层:

Enemy ├── WalkingEnemy └── FlyingEnemy

第三天,策划又来:“我们要一个’会飞的NPC’”。

他皱了皱眉——"飞行"这个能力,属于Enemy的分支——NPC怎么获得?

他犹豫再三,决定把"飞行"提升到Character:

Character ├── FlyingCharacter │ ├── FlyingPlayer │ ├── FlyingEnemy │ └── FlyingNPC └── WalkingCharacter ├── WalkingPlayer ├── WalkingEnemy └── WalkingNPC

类的数量翻倍了——但结构还能维持

第四天,策划兴奋地跑来:“我们还要一个’会游泳’的能力!还有’会隐身’!还有’会喷火’!”

他呆坐在电脑前——继承树开始爆炸

FlyingSwimmingInvisibleFireBreathingEnemy WalkingSwimmingFireBreathingNPC FlyingInvisiblePlayer ...

类的数量呈"组合爆炸"式增长——代码变成了一团乱麻——每一个新需求,都像是在往这座摇摇欲坠的塔上再加一块砖

第五天——他崩溃了

这就是"继承的困境"——是每一个用过面向对象继承的程序员,都或多或少经历过的噩梦

而Unity——从一开始,就旗帜鲜明地拒绝了这条路

它选择了另一条更宽阔的道路——组合(Composition)

**今天,我们就来深入探讨——为什么Unity不使用继承?继承到底困在了哪里?

一、继承的甜蜜承诺

在讨论"继承的困境"之前——先回顾一下继承的"甜蜜承诺"

继承的初衷

面向对象编程中,继承的初衷是极其美好的

“复用代码——建立层次——表达’is-a’的关系。”

它有三大承诺

承诺1:代码复用

  • 父类写好通用逻辑——子类直接继承使用
  • 不用重复写相同的代码

承诺2:分类清晰

  • 狮子是猫科——猫科是哺乳动物——哺乳动物是脊椎动物
  • 层次分明——一目了然

承诺3:多态优雅

  • 一个"Animal"变量——可以指向Dog、Cat、Lion
  • 调用相同的方法——表现出不同的行为

这些承诺——听起来完美无缺

在大学的课堂上、在教科书的例子里——继承展现的都是它最美好的一面

**但——当继承走进"真实世界的游戏开发"——它开始暴露出深刻的问题

二、困境一:组合爆炸

继承最致命的问题——就是开头那个例子展示的——组合爆炸

问题的本质

当"能力"有多个维度时——继承树无法优雅地表达

假设我们有4个独立的能力维度

  • 移动方式走 / 飞 / 游(3种)
  • 攻击方式近战 / 远程 / 魔法(3种)
  • 智能类型敌对AI / 友好NPC / 玩家控制(3种)
  • 特殊能力隐身 / 无 / 分身(3种)

用继承来表达——需要多少个类?

3 × 3 × 3 × 3 = 81个类!

每增加一个维度——类的数量成倍增长

这就是"组合爆炸"——当维度增加,继承树的复杂度会以"指数级"膨胀

现实中的例子

想象一个"塔防游戏"

  • 单位类型兵、骑士、法师、弓箭手、飞行单位……(10种)
  • 元素属性火、水、雷、冰、土、光、暗(7种)
  • 等级1-10级(10种)

**如果用继承——需要 10 × 7 × 10 = 700 个类

这不是编程——这是灾难

继承树在"多维度组合"面前——彻底失效

三、困境二:僵化的血统

**继承的第二个问题——血统一旦确立,就难以改变

一个残酷的现实

假设你设计了一个类

classEnemy:Character{publicvoidAttack(){/* 敌人攻击玩家 */}publicvoidPatrolArea(){/* 敌人巡逻 */}}

几个月后,需求变了“这个敌人被主角说服,加入了玩家阵营。”

问题来了

  • 它需要停止"攻击玩家"——但攻击逻辑写在Enemy里
  • 它需要开始"跟随玩家"——但这是NPC的能力
  • 它的类型是"Enemy"——但它已经不是敌人了

你怎么办?

方案A:把它变成NPC

  • 需要重新实例化——原来的数据全部丢失
  • 原来的引用全部失效

方案B:给Enemy加上"跟随玩家"的方法

  • 代码越来越臃肿——Enemy承担了本不该有的职责
  • 违反"单一职责原则"

方案C:用一堆if判断

  • “如果我现在是朋友,就不攻击”
  • 代码充满特殊分支——难以维护

每一种方案——都有严重的副作用

这就是继承的"僵化"——一个对象一旦被赋予了"血统",就再也难以改变

现实世界不这样

但现实世界不是这样的

  • 一个人可以从"学生"变成"员工"
  • 一个盟友可以变成"敌人"
  • 一个物体可以从"活的"变成"死的"

存在是流动的——身份是可变的——但继承却把它们"焊死"

这就是继承的第二个大问题——它假设"分类是永恒的"——但现实是"变化是永恒的"

四、困境三:脆弱的基类

**继承的第三个问题——基类的每一次修改,都是一场地震

一个真实的场景

假设你的项目里,有一个基类Character

classCharacter{publicinthp=100;publicvoidTakeDamage(intdamage){hp-=damage;}}

继承它的类有20个Player、Enemy1、Enemy2、NPC1、Boss1、Boss2……

某天——你决定加个"护甲"的概念

classCharacter{publicinthp=100;publicintarmor=0;// 新增publicvoidTakeDamage(intdamage){damage=Mathf.Max(0,damage-armor);// 计算方式变了hp-=damage;}}

结果

  • 所有20个子类的行为都变了
  • 有些子类可能重写了TakeDamage——它们不受影响,但可能产生bug
  • 有些子类假设了旧的逻辑——它们默默地出错了

你不知道

  • 改这个基类,会影响哪些地方?
  • 有多少子类真的需要armor这个概念?
  • 有多少子类可能因此产生难以察觉的bug?

这就是软件工程中著名的"脆弱基类问题"(Fragile Base Class Problem):

**基类的每一次修改——都可能像多米诺骨牌一样——震动整个继承树

**在游戏项目中——基类往往是最重要、也最容易膨胀的——修改它带来的风险,让人望而却步

很多项目最终陷入"基类不敢动"的窘境——技术债务越积越深

五、困境四:横向共享的窘迫

**继承的第四个问题——只能纵向传递能力,无法横向共享

一个具体的例子

假设你的项目里有两个类,完全没有继承关系

Character └── Player(玩家) Environment └── Tree(树)

某天,需求来了“玩家和树,都可以被’燃烧’——都有火焰特效、都会掉血、都会最终消失。”

问题这个"燃烧"能力,应该放在哪里?

方案A:放在两个类里,各写一份

  • 代码重复——违反DRY原则
  • 改一处逻辑,要改两处

方案B:提升到公共基类

  • 但Character和Environment的公共基类只能是Object
  • 总不能把"燃烧"放到Object里——那所有东西都能燃烧了

方案C:用接口(Interface)

  • 接口只能定义"契约"——不能包含实际代码
  • 两个类还是要各自实现——代码依然重复

继承在"横向共享能力"上——非常无力

组合的优雅答案

而Unity的组合思想——一句话解决写一个BurnableComponent组件,挂到任何需要"燃烧能力"的物体上

classBurnableComponent:MonoBehaviour{publicfloatburnDamage=10;// ...燃烧的所有逻辑}

给玩家挂上BurnableComponent → 玩家能燃烧
给树挂上BurnableComponent → 树能燃烧
给汽车挂上BurnableComponent → 汽车能燃烧

无需任何继承关系——能力可以自由地"横向共享"

这就是组合相对于继承的"降维打击"——它突破了继承的"纵向枷锁",实现了"任意横向共享"

六、困境五:菱形继承与多重继承

**继承的第五个问题——多重继承的死局

菱形继承问题

假设我们想让一个类"同时继承两个父类"

Animal / \ Bird Fish \ / FlyingFish(飞鱼)

问题来了

  • Animal.eat() 应该继承谁的版本?
  • Bird.eat() 和 Fish.eat() 如果都被重写了——FlyingFish该用哪个?

这就是著名的"菱形继承问题"(Diamond Problem)——多重继承时,继承链会形成菱形,导致方法调用的歧义

C++支持多重继承——但需要复杂的"虚继承"来解决——极难理解、极易出错

Java、C#等语言——干脆禁止多重继承——只允许"多实现接口"(但接口无实现)

游戏中的困境

这在游戏开发中意味着什么?

假设我们有两个类

Weapon(武器):有Damage、AttackSpeed Container(容器):有Capacity、Contents

现在需求来了“魔法背包”——既是武器(能攻击),又是容器(能装东西)"

**你无法用继承——因为C#不允许多继承

只能选择

  • 让MagicBag继承Weapon——手动实现Container的功能
  • 让MagicBag继承Container——手动实现Weapon的功能
  • 或者两者都不继承——从Object开始,重写所有逻辑

**继承在这里——彻底失败

而在Unity的组合世界里——没有这个问题

  • 给MagicBag挂上Weapon组件 → 它就是武器
  • 给MagicBag挂上Container组件 → 它就是容器
  • 两个组件独立运作——互不冲突

一次搞定——干净利落

七、困境六:难以适应变化

**继承的第六个问题——它假设"设计能一次搞定"

一个残酷的真相

在游戏开发中——需求是永远变化的

产品经理说“这个玩家角色,我们希望改成第三人称视角。”
策划说“BOSS的AI我们要重做——从近战改成远程。”
主美说“这个NPC我们要加个飞行状态。”

**每一次需求变化——都可能撼动继承树的根基

如果继承树设计不当

  • 加一个能力——要改基类
  • 改一个能力——要改多个子类
  • 删一个能力——继承链断裂

每一次改动——都是一次冒险

而Unity的组合世界

  • 加一个能力 → 新增一个组件
  • 改一个能力 → 只改这一个组件
  • 删一个能力 → 从物体上移除这个组件

每一次改动——都是精确、独立、无副作用的

这就是组合对继承的"降维优势"——它天然拥抱变化

八、继承不是完全无用

**说了这么多继承的问题——我们不是要"全盘否定继承"

继承有它的价值

场景1:明确的"is-a"关系

  • 动物是生物——狗是动物——这种关系是永恒的
  • 在这种场景,继承是自然的选择

场景2:稳定的接口层次

  • UI控件:Button是Selectable是UIBehaviour
  • 在框架级别,继承可以提供清晰的结构

场景3:模板方法模式

  • 父类定义流程——子类填充细节
  • 是继承的经典有效应用

**继承的问题——不是"继承本身",而是"过度使用继承"

Unity的智慧——是识别出"游戏物体的能力组合"这一场景,天然不适合继承——所以用组合替代

九、Unity的答案

Unity的答案,就是"组件系统"(Component System):

  • GameObject是空壳——没有任何预设能力
  • Component是能力包——可以自由添加
  • 能力之间——是"平等的"、“独立的”、“可组合的”

用这套系统——上面所有的困境都被解决了

  • 组合爆炸 →每种能力一个组件——加起来就行
  • 僵化血统 →运行时可以添加或删除组件
  • 脆弱基类 →每个组件独立,改一个不影响其他
  • 横向共享 →同一个组件,可以挂到任何物体上
  • 多重能力 →一个物体可以挂任意多个组件
  • 适应变化 →需求变了,调整组件搭配即可

**继承的六大困境——Unity用组合,一次性解决

十、哲学思考:从"血统"到"能力"

Unity放弃继承——不只是一个技术选择——更是一种世界观的选择

血统世界观

继承代表的是"血统世界观"

  • 你是什么,取决于你的祖先是什么
  • 你的能力,来自你的血脉传承
  • 你的定位,一出生就决定了

这是一种"贵族式"的世界观——有等级、有身份、有难以改变的宿命

能力世界观

组合代表的是"能力世界观"

  • 你是什么,取决于你能做什么
  • 你的能力,来自你选择装配了什么
  • 你的定位,随时可以改变

这是一种"平等式"的世界观——每个物体都从"零"开始,用自己的选择定义自己

Unity选择了后者

**这不仅是技术上的选择——更是一种深刻的哲学表达

不问你从哪里来——
只问你想成为什么。

**每一个GameObject——都是一张白纸——它的命运,由它自己"装配的能力"决定

结语:一场"血统枷锁"的解放

从"程序员的噩梦",
到"继承的六大困境",
到"组合的降维打击",
到"世界观的哲学之选"——

Unity拒绝继承的选择——不是意气用事——而是一场深思熟虑的技术革命

  • 它拒绝了**"组合爆炸"的失控**
  • 它拒绝了**"僵化血统"的枷锁**
  • 它拒绝了**"脆弱基类"的隐患**
  • 它拒绝了**"横向共享"的窘迫**
  • 它拒绝了**"多重继承"的死局**
  • 它拒绝了**"难以变化"的宿命**

它选择了

  • 一种极简的世界观——GameObject + Component
  • 一种灵活的组合方式——自由拼装
  • 一种平等的哲学——没有血统的贵贱

它像一位智慧的建筑师

  • 不迷信"图纸的完美"
  • 而信奉"积木的自由"
  • 让每一次建造,都能应对新的挑战

**下次当你面对一个复杂的类继承树、感到无从下手时——请记得

继承不是唯一的答案——
组合是另一条更宽阔的路——
Unity用20年的实践证明了

放下"血统"——
拥抱"能力"——
才是通往灵活、自由、可变化的软件世界的正道

这就是Unity为什么不使用继承——因为它看清了继承的局限——并选择了一条更符合"变化本质"的道路

在这条路上——
每一个物体都是平等的
每一份能力都是自由的
每一次组合都是崭新的可能

这——就是"组合"对"继承"的胜利——是游戏引擎发展史上,最优雅的哲学之选。 🎮🧩✨

← 返回列表