13. Type Object
13. Type Object:用运行时类型对象表示种类,让实例共享数据并可由内容扩展,通过可复位因果实验和反例证据验收。
13. Type Object
学习目标
- 能区分类型对象与实例
- 能解释共享定义的威力
- 能设计类型继承与注册表
为什么"13. Type Object"从问题证据开始
你的游戏里有一百种怪物,每种有各自的属性:血量、速度、技能。最直觉的 OOP 做法是每种怪物一个类:class Goblin { ... }、class Dragon { ... }——用继承表达种类差异。代码今天能跑,但两个问题浮现:类爆炸(一百种怪物 = 一百个类,新种类要写代码重编译)、策划无法参与(加怪物必须等程序员)。
本页把"无模式基线"定义为"每种怪物一个类"的代码,然后引入类型对象作为候选机制,验证它是否真的让"新增种类不动代码"。通过条件是新怪物由数据(类型对象)定义,引擎只提供读取机制——而不是种类数量决定类数量。
来源、版本与独立重写边界
本页用作者完整在线正文核对正式标题、设计分叉和时代语境,并以作者源码仓库交叉检查结构。仓库许可证明确正文、HTML与样式为 CC BY-NC-ND 4.0,示例程序等其他文件为 MIT;因此下列中文解释、图示、交互和代码均为独立教学重写,不翻译、拼接或改写受 ND 限制的原文表达。
本章机制与术语
理解 Type Object 需要四个核心概念:
- ↡:用运行时对象表示一种"种类"——GoblinType 存血量/速度/技能,是共享定义。
- ↡:具体怪物,只存自身状态(位置/当前血量),引用一个类型对象。
- ↡:实例从类型对象读取属性——改类型对象 = 改所有该种实例。
- ↡:类型对象由数据定义(文件/配置)——新种类 = 新数据,不写新类。
四个概念共同约束"用运行时类型对象表示种类,让实例共享数据并可由内容扩展":任何结论都必须回到"新增种类是否不动代码、实例是否共享定义、类型是否可动态变更"三个可观察量。
Type Object — 种类是数据,实例是引用
第 1 / 4 步 · ① 类型对象:定义怪物的共享属性(hp/speed/技能)——一份定义
类型从「类」降级为「数据对象」——种类由内容定义,实例按需引用。
💡 对着动画看:动画上方是 GoblinType 类型对象(hp=60, speed=3, 技能:投掷——一份共享定义),下方三个哥布林实例各自只存位置和当前血量,虚线指向类型对象。注意"改 GoblinType.hp 所有哥布林同时变强"——这就是共享的威力与陷阱。读"典型 OOP 回答"一节时对比:类方案把定义固化进代码,类型对象把它变成数据。
官方结构逐项深读
13. Type Object
Type Object 的主张:把"种类"从"类"降级为"对象"。传统 OOP 用类表达种类(Goblin 类),类型对象用运行时对象表达(GoblinType 实例)。种类差异从"继承关系"变成"数据差异"——种类是数据,可以存储、可以运行时创建、可以由内容文件定义。实例不再由类区分,而是由"引用哪个类型对象"区分。
Intent
意图:让"定义新种类"不写新代码。用类型对象代替类层次,种类数量从"编译期类数"变成"运行期数据条数"。策划或工具可以创建新类型对象(改数据文件),引擎的实例生成逻辑完全通用("给我这个类型,生成一个实例")。这继承了 Prototype 的"数据驱动"思想,但用"共享类型"而非"复制原型"。
Motivation
一百种怪物的问题:写一百个类,绝大部分只有属性不同(血量、速度、掉落物),行为相同(都是"攻击-被攻击-死亡")。类的差异主要是数据而非行为。动机:把"种类的数据"从代码里拿出来——一个 GoblinType 对象就够了,不必有 Goblin 类;一百个类型对象 = 一百条数据。
The typical OOP answer
传统 OOP 答案:继承树——Monster 基类,Goblin : Monster、Dragon : Monster……每个子类在构造函数里设属性。问题:继承表达"种类"很笨重——种类差异是数据(数值不同),继承表达的是行为差异(方法不同);用继承表达数据差异,等于把数据固化进类,新种类必须写新类。
A class for a class
"为类建一个类":GoblinType 类描述哥布林(属性数据),Monster 实例持 GoblinType* 引用。这就把"种类"对象化了——GoblinType 是一个普通对象,可以被创建、存储、替换。Monster 类只有一个(不再有子类),所有怪物共用它,区别只在 type_ 指针指向哪个类型对象。这是类型对象的本质:实例类只有一个,种类全靠数据。
The Pattern
模式结构:Type 类(存种类属性:hp/speed/skill)→ TypedObject 类(持 Type* 引用 + 自身状态)→ 读取属性时 type_->hp。Monster 实例的 hp 读取走类型对象,位置等自身状态存实例。类型对象可以组织成层次(GoblinType 继承 MonsterType)表达"亚种"。引擎生成怪物 = 指定一个 Type,new 一个 Monster 挂上引用。
When to Use It
何时用类型对象:种类众多且由数据定义(怪物、道具、技能模板)、需要运行时动态创建种类(编辑器里调数值)、策划/内容团队要独立加种类。何时不用:种类很少且稳定(两个类能搞定就别建类型系统)、行为差异远大于数据差异(用继承或 Component 更合适)。判断标准:"种类数量会频繁增长吗"——会,就用类型对象。
Keep in Mind
两个注意点:类型对象要手工追踪(它也是对象——要管理生命周期、注册表、引用计数,不能裸 new 裸 delete);按类型定义行为更难(类型对象存数据容易,存"该类型怎么做"的行为难——需要函数指针或委托,复杂度上升)。类型对象擅长数据差异,行为差异要另想办法。
The type objects have to be tracked manually
类型对象是运行时对象,意味着要管理它们的生命周期:谁创建、谁持有、何时销毁。通常需要一个类型注册表(map<name, Type*>):加载数据时注册所有类型,实例创建时按名字查表。销毁顺序要小心(类型对象被实例引用时不能提前销毁)。这是数据驱动系统的常见成本——资源管理从"编译期自动"变成"运行期手动"。
It's harder to define behavior for each type
类型对象存数据很容易,存行为很难:GoblinType 想定义"哥布林的攻击方式"——C++ 里类型对象不能直接挂虚函数(它本身不是类层次)。解法:函数指针(类型对象持行为回调)、行为 ID(switch 分发)、组合组件(类型对象引用行为对象)。作者提醒:如果行为差异真的很大,类型对象可能不是最佳工具——继承/Component 更适合行为多态。
Sample Code
示例代码:Breed 类(血量/速度/攻击力 + 可选"父 Breed")+ Monster 类(持 Breed* + 当前 hp)。Monster::currentHealth() 返回实例字段,Monster::maxHealth() 返回 breed_->health——静态属性走类型,动态状态走实例。创建:new Monster(goblinBreed);改类型:monster->breed_ = dragonBreed(变身!)。
Making type objects more like types: constructors
让类型对象更"像类":给类型对象加构造能力——goblinBreed->newMonster() 返回新哥布林实例(类型对象兼任工厂)。这融合了 Prototype(生成器)+ Type Object(共享类型):实例由类型创建,类型定义属性。这样"生成某类怪物"的语义完整落在类型对象上:breed.newMonster() 比裸 new Monster(breed) 更清晰。
Sharing data through inheritance
类型对象之间的继承:GoblinBreed : MonsterBreed(亚种继承基础属性)——类型对象可以形成"类型层次"表达种类间的共性。实例读取属性时沿类型链查找(breed->health,没有就查父类型)。这与类继承表达的是同一件事(种类共性),但发生在运行时数据层而非编译期类层——改父类型属性,所有亚种同时更新。
Design Decisions
四个关键设计决策:类型对象封装还是暴露、类型化对象如何创建、类型能否改变、支持什么继承。前两者决定 API 形态,后两者决定运行时行为——下面四节展开。
Is the type object encapsulated or exposed?
封装方式:暴露(Monster 直接暴露 type_ 字段,外部可读可改——简单,但外部可以随意改类型破坏不变式)与封装(Monster 只暴露读取方法 maxHealth(),内部引用类型对象——安全,类型切换走受控接口)。作者建议:读取走封装方法(外部只问"这个怪物的最大血量",不问"它的类型是谁"),需要变身时提供 changeBreed() 受控方法。
How are typed objects created?
创建方式:类型对象兼任工厂(breed.newMonster()——语义完整,实例与类型绑定清晰)与独立工厂函数(createMonster(breed)——类型对象不承担创建职责,职责单一)。作者倾向前者:类型对象"知道自己怎么造实例"最自然,也方便子类型覆盖创建逻辑。但要注意:工厂方法意味着类型对象持有一份"实例构造逻辑",类型对象不再纯数据。
Can the type change?
类型能否中途改变:可变(实例可换类型——支持"变身"玩法,但引入"实例与类型不同步"的状态风险)与不可变(实例创建时定死类型——简单安全,但玩法受限)。作者建议:默认不可变(类型是实例的"出生设定"),变身玩法是特殊需求,用受控的 changeBreed() 显式实现——别让类型字段随处可改。
What kind of inheritance is supported?
类型继承程度:无继承(每个类型独立存全部属性——简单,但亚种要重复数据)与单继承(类型对象有父类型,属性沿链查找——表达亚种高效,但查找链要管理)与多继承(多个父类型——灵活,但冲突规则复杂)。作者建议:单继承够用(哥布林→食人魔这种"基础型→亚种"是最常见形态),多继承很少值得它的复杂度。
See Also
- Prototype:原型用"复制原型"定义种类,类型对象用"共享类型"定义种类——一个复制、一个共享。
- Flyweight:类型对象天然是享元的"固有状态"——多个实例共享一份类型数据。
- Bytecode:类型对象定义"种类是什么",字节码定义"种类做什么"——数据与行为互补。
可迁移实现或计算骨架
initial state -> 定义 Type 类(属性数据)-> 注册类型对象(数据加载)-> 实例创建时挂类型引用 -> 属性沿类型读取
fault injection -> 每种怪物一个类
pass condition -> 新增种类只加数据(类型对象),引擎零改动
reset -> initial state对应实现要点:Type 类存属性(可含父类型支持单继承);注册表按名管理类型对象生命周期;实例持 Type* 并只存动态状态;读取静态属性走类型、动态状态走实例。该骨架只保存实验合同;真实项目还要固定类型注册表、继承深度上限与类型热更新规则,并保留基线实现以便回退。
本章练习与节点验证矩阵
练习
问题 1:新增种类。
操作:在动画类型池中"新增"一个"食人魔"类型(只改数据)
问题 2:共享验证。
操作:修改 GoblinType.hp 为 80,观察所有哥布林实例
问题 3:类型可变实验。
操作:给一个实例调用 changeBreed(DragonType),观察属性变化
问题 4:继承实验。
操作:让 GoblinType 继承 MonsterType 基属性,改基属性观察亚种
术语复核
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 类型对象
- 实例
- 共享
术语复核与本章回顾
掌握"13. Type Object"意味着能从"每种怪物一个类让新种类必须写代码"出发,解释类型对象、实例、共享与数据驱动四者的关系,再用"新增种类是否不动代码、实例是否共享定义、类型是否可动态变更"三个可观察量推翻或保留实现。若三个判据不能同时满足,本章仍未通过。
一句话回顾:种类从"类"降级为"对象"——GoblinType 是一份共享数据,Monster 是挂类型引用的通用实例,改数据文件就能给游戏加新怪物。
阅读导航
← 上一页:12. Subclass Sandbox · 下一页:V. Decoupling Patterns → ← 上一页:12. Subclass Sandbox · 下一页:V. Decoupling Patterns →