5. Prototype
5. Prototype:用可复制对象或数据模板定义新种类,并区分深浅复制边界 ,通过可复位因果实验和反例证据验收。
5. Prototype
学习目标
- 能说明原型与生成器的关系
- 能区分深浅拷贝边界
- 能解释数据驱动的怪物生成
为什么"5. Prototype"从问题证据开始
你的游戏里有一群怪物:骷髅、哥布林、龙,每种怪物的生成逻辑都不同。最直觉的做法是给每种怪物建一个生成器类(SkeletonSpawner、GoblinSpawner、DragonSpawner)——每个生成器知道怎么造自己的怪物。代码今天能跑,但明天你发现:每个生成器几乎一样(都是"new 一个怪物、设些属性"),只是造的怪物不同;加一个新怪物就要写一个新生成器类;更糟的是,如果怪物种类由数据文件定义(策划想动态加怪物),生成器类这种硬编码方式根本做不到。
本页把"无模式基线"定义为"每种怪物一个生成器类"的代码,然后引入原型作为候选机制,验证它是否真的让"加新怪物种类"变成改数据而非写代码。通过条件是新增怪物种类不修改任何引擎代码——而是提供一个新的原型实例。
🔮 猜一猜:新增一种怪物,原型方案与生成器类方案各要改几处代码?
来源、版本与独立重写边界
本页用作者完整在线正文核对正式标题、设计分叉和时代语境,并以作者源码仓库交叉检查结构。仓库许可证明确正文、HTML与样式为 CC BY-NC-ND 4.0,示例程序等其他文件为 MIT;因此下列中文解释、图示、交互和代码均为独立教学重写,不翻译、拼接或改写受 ND 限制的原文表达。
本章机制与术语
理解 Prototype 需要四个核心概念:
- ↡:一个可复制的示例对象,包含某种怪物的默认状态,是"种类"的物化。
- ↡:从原型复制出新实例的方法——新怪物不再由类构造,而是由原型复制。
- ↡:持有原型并负责产出实例的对象——它不认识具体怪物类,只认识 clone()。
- ↡:怪物种类由数据(原型实例/模板)定义而非代码类定义——改种类=改数据。
四个概念共同约束"用可复制对象或数据模板定义新种类,并区分深浅复制边界":任何结论都必须回到"新增种类是否不动引擎代码、克隆是否完整独立"两个可观察量。
第 1 / 4 步 · ① Prototype 接口:声明 clone() 方法
每种怪物只需一个原型实例,运行时克隆并按需修改——新增怪物类型不再需要新子类。
💡 对着动画看:动画上方是
«interface» Prototype(声明 clone()),下方 Skeleton/Goblin/Dragon/Item 四个具体原型通过虚线继承箭头连向它。注意每个原型的属性(health=100, speed=2.5)——它们就是"种类"的物化。读"生成函数/模板/一等类型"三节时,对比看:这三个方案本质上都在回答"如何定义一个可复制的新种类"。
官方结构逐项深读
5. Prototype
Prototype 的主张是:用"复制示例"代替"从类构造"。每个怪物种类由一个原型对象代表,新怪物 = 原型.clone()。这样"种类"从代码类降级为数据对象——策划或数据文件就能定义新种类,引擎代码一行不改。代价是要处理克隆的深浅复制边界:克隆出的实例必须完全独立,修改它不能污染原型或其他克隆。
The Prototype Design Pattern
模式结构:原型接口声明 clone();具体原型实现克隆逻辑(返回类型正确的新实例);生成器持有原型引用,需要新实例时调 prototype->clone()。经典示例——怪物生成:spawner(spawn(goblinPrototype)),每次 spawner.spawnMonster() 都从原型复制一个哥布林。关键在于生成器不 new 任何具体类,它只认识 clone() 接口——这就是"加新怪物不动生成器代码"的原因。
How well does it work?
原型在实际中好不好用?作者的评价是**"有保留地好用"**:核心问题在 clone() 的实现——浅拷贝只复制指针,克隆与原型共享内部对象(改一个全变);深拷贝要递归复制一切(成本高、易漏)。游戏对象往往有网格、材质、行为脚本等复杂引用,深克隆很容易出错。因此原型更适合"状态简单、复制边界清晰"的对象;复杂对象用原型要非常小心。
Spawn functions
不用类的生成器:生成函数——每个怪物种类对应一个返回新实例的函数。Monster* spawnGoblin() { return new Goblin(); }。生成器持有函数指针,调用即得新怪物。相比原型,生成函数的 clone 天然是"构造新实例",不存在共享状态的坑;但代价是"种类"还是代码(函数),策划依然无法用数据定义新怪物。原型 vs 生成函数:数据驱动性 vs 复制安全性。
Templates
模板方案:C++ 模板类做生成器——Spawner<Goblin> 在编译期为每种怪物生成一个生成器实例。优点是类型安全、零运行时开销;缺点是模板实例在编译期固定,"新种类"还是要写代码(模板参数),策划无法动态加怪物。模板解决了"重复的生成器代码",但没解决"数据驱动"——它仍要求新种类 = 新代码。
First-class types
一等类型方案:把"类型"变成值——在支持一等类型的语言(如 C#/Java 的反射、Python 的类对象)中,直接持有类对象本身做生成器。Spawner<Goblin> 变成 Spawner(Goblin.class),类型作为数据传递。这是三者中最接近原型的方案——"种类"真正成为可传递、可存储、可由数据选择的值,只是复制机制由语言反射承担而非 clone()。
The Prototype Language Paradigm
原型的终极形态:整个语言都基于原型(Self、JavaScript)。在这种语言里没有"类",对象直接从一个原型对象继承——obj = prototype.clone() 然后 obj.method = ... 给它加自己的行为。类与实例的界限消失:每个对象都是另一个对象的克隆,行为通过"继承链"传播。游戏里的原型模式是这种语言范式在类语言里的局部应用。
Self
Self 语言是原型范式的原型(字面意义):对象不需要类,创建对象 = 从原型复制并修改。作者评价 Self 的教训是——原型的自由是双刃剑:没有类的约束,行为难以规范(两个"同种"对象可能不同步演化);而游戏需要的是"种类"的稳定性(所有哥布林行为一致)。因此游戏里的原型被限制在"克隆产生新实例"的层面,而不是让每个对象自由演化。
How did it go?
Self 项目的总结:原型语言学起来直观(没有类与实例的两层心智负担),但维护起来难(对象关系像蛛网,缺少类提供的结构约束)。作者从中的领悟:游戏里用原型要克制——把它当作"生成新实例的机制",而不是"组织整个代码的方式"。类负责结构稳定,原型负责运行时灵活,两者分工而非互斥。
What about JavaScript?
JavaScript 的继承就是原型式的:对象有 __proto__ 指向原型,属性查找沿原型链向上。但游戏里通常不用 JS 原型链做怪物生成——因为原型链的共享同样有"改一个影响一片"的坑,而且显式 clone 更可控。JS 给游戏开发的真正启示是:原型机制在语言层面是可行的,但工程上仍需要显式管理复制边界(如用工厂函数返回新对象)。
Prototypes for Data Modeling
原型的现代应用:数据建模——怪物种类、物品模板、技能配置都存成原型数据(JSON/脚本配置),运行时从原型克隆出实例。这正是数据驱动开发(DDD)在游戏里的落地:策划改数据文件 = 改"种类";引擎代码不认识任何具体怪物,只认识"给我这个原型的克隆"。原型从"代码技巧"升格为"内容生产工具",是它最有价值的使用方式。
可迁移实现或计算骨架
initial state -> 定义原型(含默认状态)-> 生成器持原型 -> spawn = 原型.clone() -> 按需修改新实例
fault injection -> clone 用浅拷贝共享内部对象
pass condition -> 新增怪物种类不改引擎代码;克隆实例完全独立
reset -> initial state对应实现要点:原型接口声明 clone() 返回自类型;具体原型实现深拷贝或明确复制边界;生成器只认识 clone();新增种类 = 注册一个新原型实例(数据),不写新类。该骨架只保存实验合同;真实项目还要固定复制边界清单(哪些字段深拷、哪些共享只读)、种类注册表与数据格式,并保留基线实现以便回退。
本章练习与节点验证矩阵
练习
问题 1:克隆独立验证。
操作:克隆一个怪物后修改克隆的血量,检查原型与其他克隆
问题 2:新增种类实验。
操作:在动画原型池中"注册"第 5 种怪物(只用数据,不写类)
问题 3:深浅拷贝对比。
操作:分别用浅拷贝与深拷贝实现 clone,各克隆一次并修改内部引用成员
问题 4:方案对比。
操作:对比生成函数/模板/一等类型/原型四种"定义新种类"的方案
术语复核
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 原型
- 克隆
- 数据驱动
练习答案参考
练习判据即答案:每个练习的"验证判据"列给出了通过标准——先自己动手,再对照判据核验。
术语复核与本章回顾
掌握"5. Prototype"意味着能从"每种怪物一个生成器类让新种类必须写代码"出发,解释原型、克隆、生成器与数据驱动四者的关系,再用"新增种类是否不动引擎代码、克隆是否完整独立"两个可观察量推翻或保留实现。若两个判据不能同时满足,本章仍未通过。
一句话回顾:怪物不再是"造出来的",而是"复制出来的"——原型对象就是种类,clone() 就是生产,策划改数据文件就能给游戏加新怪物,引擎代码纹丝不动。