0x0e.原型模式
如果说前面的工厂和建造者模式是“根据图纸从零组装一台新机器”,那么原型模式就简单粗暴多了:它是一台“克隆机”。直接拿一个现有的对象(原型),复制出一模一样的新对象。
在 Unity 开发中,原型模式的地位非常特殊,因为你其实每天都在用它,只是你可能不知道它的名字。
💡 为什么要用原型模式?
假设你在写一个关卡编辑器,玩家可以在地图上疯狂摆放一种叫“魔暴龙”的怪物。这只魔暴龙非常复杂,它身上挂着 AI 组件、3D 模型、动画控制器、音效源,还有一堆初始属性。
如果是纯代码实例化:
你得先 new 一个怪物,然后加载模型、绑定动画、加载音效、配置属性……如果玩家要放 100 只,你的游戏可能会直接卡死在频繁的“从零初始化”中。
原型模式的优雅解法:
在内存里先放好一只已经完全初始化好的魔暴龙(原型)。每当需要新怪物时,直接对它进行克隆(Clone)。克隆出来的新怪物立刻就拥有了和原型一模一样的状态,连配置都省了!
一、 🤯 震撼 Unity 玩家的真相:Prefabs 与 Instantiate
在传统的 C# 或 Java 书籍里,原型模式需要你实现 ICloneable 接口或写一个 Clone() 方法。
但在 Unity 中,官方把原型模式直接做成了整个引擎的核心基石,那就是:Prefab(预制体) 与 Object.Instantiate()。
- 原型(Prototype): 你在 Project 窗口里做好的那个 Prefab 资产,或者场景里隐藏的某个魔暴龙样本。
- 克隆(Clone):
Instantiate(monsterPrefab);
Unity 的底层是用 C++ 写的,当你调用 Instantiate 时,它会在内存中进行极其高效的二进制数据块复制。它比你用纯 C# 代码去 new 一个复杂对象并挨个赋值要快得多。这就是为什么在 Unity 中,我们极少需要自己去手写 GameObject 的克隆逻辑。
二、 纯 C# 数据层的原型模式(真正需要手写的情况)
虽然 Unity 帮我们搞定了 GameObject 的克隆,但在处理纯 C# 数据类(比如怪物的数值属性、技能数据)时,我们还是需要亲自上阵实现原型模式。
假设怪物在游戏运行中吃到了 Buff,属性改变了,我们想复制当前的怪物数据,就需要用到它。
1. 定义可克隆的怪物数据类
在 C# 中,我们可以利用 object 自带的 MemberwiseClone() 方法来实现快速复制。
using UnityEngine;
// 怪物数值类
public class MonsterData
{
public string monsterName;
public int hp;
public int attack;
// 引用类型属性(注意:这里埋了个巨大的坑,稍后讲解)
public WeaponInfo weapon;
public MonsterData(string name, int hp, int attack, WeaponInfo weapon)
{
this.monsterName = name;
this.hp = hp;
this.attack = attack;
this.weapon = weapon;
}
// 核心:克隆方法(原型模式的灵魂)
public MonsterData Clone()
{
// MemberwiseClone 是 C# 内置的“浅拷贝”方法,非常高效
return (MonsterData)this.MemberwiseClone();
}
public void PrintInfo()
{
Debug.Log($"[怪物]: {monsterName} | 血量: {hp} | 攻击: {attack} | 武器: {weapon.weaponName}");
}
}
// 武器信息类(引用类型)
public class WeaponInfo
{
public string weaponName;
public WeaponInfo(string name) { weaponName = name; }
}
2. 在 Unity 中使用克隆
using UnityEngine;
public class PrototypeTest : MonoBehaviour
{
void Start()
{
// 1. 创建一个原型对象(精英哥布林)
WeaponInfo ironSword = new WeaponInfo("铁剑");
MonsterData prototypeGoblin = new MonsterData("精英哥布林", 500, 50, ironSword);
// 2. 使用原型模式,直接克隆出一只一模一样的哥布林
MonsterData clonedGoblin = prototypeGoblin.Clone();
// 3. 修改克隆体的名字和血量,不会影响原型!
clonedGoblin.monsterName = "克隆哥布林 A";
clonedGoblin.hp = 200;
// 4. 打印验证
prototypeGoblin.PrintInfo(); // 输出: 精英哥布林 | 血量: 500 ...
clonedGoblin.PrintInfo(); // 输出: 克隆哥布林 A | 血量: 200 ...
}
}
🚨 避坑指南:致命的“浅拷贝”与“深拷贝”
这是使用原型模式(尤其是在 C# 中用 MemberwiseClone)时最容易让人抓狂的 Bug 来源,也是面试高频考点。
在上面的例子中,如果我执行了这行代码:
clonedGoblin.weapon.weaponName = "激光神剑";
你猜猜会发生什么?
结果是:连带着原型 prototypeGoblin 的武器也变成了“激光神剑”!
为什么会这样?
- 浅拷贝(Shallow Copy):
MemberwiseClone只会复制值类型(如int,float,string)。对于引用类型(如自定义的类WeaponInfo),它只复制了内存地址的指针。也就是说,原型和克隆体现在正共享同一个武器对象!- 深拷贝(Deep Copy): 想要真正独立,你必须在克隆方法里,把肚子里的引用类型也一起
new一个新的出来。
如何写一个完美的“深拷贝” Clone:
public MonsterData DeepClone()
{
// 1. 先进行浅拷贝,复制基础的值类型
MonsterData newMonster = (MonsterData)this.MemberwiseClone();
// 2. 为引用类型单独创建一个新实例,实现真正的剥离
newMonster.weapon = new WeaponInfo(this.weapon.weaponName);
return newMonster;
}
💡 总结
- 核心思想: 别从头建造了,直接复制我!
- 适用场景:
- 对象的初始化成本极高(比如需要读取大量配置文件、加载大量资源)。
- 需要动态保存/恢复对象的状态(比如肉鸽游戏里的“回溯到前一秒的状态”,直接在每秒开始前 Clone 一个备份即可)。
- 配合 Unity 的
Instantiate进行海量物体的生成(如弹幕、小兵)。