如果说前面的工厂和建造者模式是“根据图纸从零组装一台新机器”,那么原型模式就简单粗暴多了:它是一台“克隆机”。直接拿一个现有的对象(原型),复制出一模一样的新对象。

在 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 进行海量物体的生成(如弹幕、小兵)。