7490 字
37 分钟
设计模式笔记(一)

前言#

早在很久之前就学过设计模式,不过非常不幸,在初次接触设计模式之后,便陷入了Reddit上的讨论所提到的”设计模式陷阱”当中:

如果你只有一把铁锤, 那么任何东西看上去都像是钉子。

初次领略到单例等等的代码之美,便开始尝试使用设计模式来重构所有代码,最后便是全局单例满天飞,创建一个实例要经过无数个工厂和装饰器,最后代码冗长到自己都无法写下去。

设计模式,乃至软件工程,确实是学术界和工业界的一道难以达成完美共识的鸿沟,这个问题常常会给初学模式的人们带来困扰: 在学习了某个模式后, 他们会在所有地方使用该模式,即便是在较为简单的代码也能胜任的地方也是如此,也就是我们常说的过度设计

不过即便如此,设计模式的一些OOP的思想仍然值得学习,尤其是单例模式、装饰器模式等较为经典的模式,可以是ECS等架构的工程基础。

这篇文章,先来说说创建型模式

img

单例模式#

1. 定义#

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

img

单例模式存在的目的无非三个:

  • 控制实例数量,节省系统资源
  • 提供全局唯一的实例访问,降低耦合
  • 可以实现懒加载,进一步节省资源

网上的教程经常分为什么”饿汉式”和”懒汉式”,个人认为没有必要,无非前者全局自动初始化,后者提供了懒加载罢了。

2. 实现方式#

单例模式通常通过以下方式实现:

  • 私有构造函数 :防止外部直接实例化。
  • 静态实例 :类内部维护唯一实例。
  • 静态方法 :提供全局访问点。
// 常规
public sealed class Singleton
{
    // 静态实例,类加载时初始化
    private static readonly Singleton Instance = new Singleton();
  
    // 私有构造函数,防止外部实例化
    private Singleton() { }
  
    // 全局访问点
    public static Singleton GetInstance()
    {
        return Instance;
    }
}

// 懒加载
public sealed class Singleton
{
    private static Singleton _instance;
  
    private Singleton() { }
  
    public static Singleton GetInstance()
    {
        if (_instance == null)
        {
            _instance = new Singleton();
        }
        return _instance;
    }
}


// 单锁懒加载
public sealed class Singleton
{
    private static Singleton _instance;
    private static readonly object _lock = new object();
  
    private Singleton() { }
  
    public static Singleton GetInstance()
    {
        lock (_lock)
        {
            if (_instance == null)
            {
                _instance = new Singleton();
            }
            return _instance;
        }
    }
}

// 双锁懒加载
public sealed class Singleton
{
    private static volatile Singleton _instance;
    private static readonly object _lock = new object();
  
    private Singleton() { }
  
    public static Singleton GetInstance()
    {
        if (_instance == null)
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = new Singleton();
                }
            }
        }
        return _instance;
    }
}

// Csharp最佳实践
public sealed class Singleton
{
    private static readonly Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());
  
    private Singleton() { }
  
    public static Singleton GetInstance()
    {
        return _instance.Value;
    }
}

CSharp代码中,推荐使用 Lazy,兼顾线程安全、延迟加载和代码简洁。同时在Unity中,对Monobehaviour创建的Manager等组件实例也有必要采取单例进行控制,这里可以参考本人常用的实现方式:

using UnityEngine;

public class MonoBehaviourSingleton<T> : MonoBehaviour where T : MonoBehaviourSingleton<T>
{
    private static T _instance;
    private static readonly object _lock = new object(); // 线程锁
    private static bool _applicationIsQuitting = false; // 标记是否退出游戏

    public static T Instance
    {
        get
        {
            if (_applicationIsQuitting)
            {
                // 退出游戏时,返回null,同时打印日志,防止出现
                // 【退出游戏瞬间的访问空引用,又没有错误日志打印】 的情况
                Debug.LogWarning($"[MonoSingleton] Instance '{typeof(T)}' already destroyed on application quit. Won't create again - returning null.");
                return null;
            }

            // 锁定线程,防止多线程访问
            lock (_lock)
            {
                // 上锁后,再次判断_instance是否为空,防止多线程访问时,_instance已经被其他线程设置为空
                if (_instance == null)
                {
                    // 如果_instance为空,则尝试查找该脚本的实例
                    _instance = FindObjectOfType<T>();

                    if (FindObjectsOfType<T>().Length > 1)
                    {
                        Debug.LogError($"[MonoSingleton] Something went really wrong - there should never be more than 1 singleton! Reopening the scene might fix it.");
                        return _instance;
                    }

                    // 如果找不到该脚本的实例
                    // 则创建一个新的GameObject,添加该脚本的组件
                    if (_instance == null)
                    {
                        var singleton = new GameObject();
                        _instance = singleton.AddComponent<T>();
                        singleton.name = typeof(T).ToString();
#if UNITY_EDITOR
                        Debug.Log($"[MonoSingleton] An instance of {typeof(T)} is needed in the scene, so '{singleton}' was created with DontDestroyOnLoad.");
#endif
                    }
                    else
                    {
#if UNITY_EDITOR
                        Debug.Log($"[MonoSingleton] Using instance already created: {_instance.gameObject.name}");
#endif
                    }
                }

                return _instance;
            }
        }
    }

    protected virtual void Awake()
    {
        if (_instance == null)
        {
            // 如果_instance引用为空,则指向该实例
            _instance = this as T;
        }
        else if (_instance != this)
        {
            // 如果_instance引用有问题,摧毁当前实例,不进行覆盖
            Debug.LogWarning($"[MonoSingleton] Multiple instances of {typeof(T)} found. Destroying this one.");
            Destroy(this.gameObject);
        }
    }

    protected virtual void OnDestroy()
    {
        // 当游戏退出时,将_instance置为null
        // 防止其他脚本访问已经销毁的对象,而又没有错误日志打印
        if (_instance == this)
        {
            _instance = null;
#if UNITY_EDITOR
            Debug.Log($"[MonoSingleton] Destroying {typeof(T)} Singleton.");
#endif
        }
    }

    private void OnApplicationQuit()
    {
        _applicationIsQuitting = true;
    }
}

对于普通单例,本人也优化了一套更适合Unity开发的实现:

using System;

/// <summary>
/// 懒加载常规单例
/// </summary>
/// <typeparam name="T"> 需要变成单例的类 </typeparam>
public class Singleton<T> where T : class, new()
{
    // 使用Lazy<T>来实现单例
    // Lazy<T>是一个懒加载的泛型类,只有在第一次访问Instance属性时才会创建实例,并且保证线程安全
    private static readonly Lazy<T> _instance = new Lazy<T>(() => new T());

    public static T Instance => _instance.Value;

    protected Singleton()
    {
        if (_instance.IsValueCreated)
        {
            throw new Exception($"{typeof(T)} is a singleton! " +
                                "You should not create a new instance!" +
                                $" Use {typeof(T)}.Instance instead."
                                );
        }
    }
}

3. Unity 特定注意事项#

  • DontDestroyOnLoad :单例 GameObject 需调用 DontDestroyOnLoad 以在场景切换时保留。确保仅在首次创建时调用,避免重复调用导致内存泄漏。
  • 场景切换 :场景加载可能导致 FindObjectOfType 失败,需在 Instance 属性中动态创建。可监听场景加载事件(SceneManager.sceneLoaded)以重新初始化单例引用。
  • 销毁问题 :Unity 的 Destroy 不会立即销毁对象,可能导致短暂的多个实例共存,需在 Awake 或 Start 中处理。
  • 序列化 :如果单例保存数据(如玩家进度),需考虑序列化(如 JSON 或 PlayerPrefs)。
  • 编辑器中调试 :在 Unity 编辑器中,单例可能因脚本重新编译而丢失引用,需在 OnEnable 或 Awake 中重新赋值。
  • 性能 :FindObjectOfType 性能开销较大,尽量缓存实例。避免在 Update 等高频方法中反复访问 Instance。

4. Unity最佳实践#

  • 场景持久化 :使用 DontDestroyOnLoad保持单例跨场景存在
  • 懒加载 :第一次访问时初始化
  • 线程安全 :Unity主线程操作,通常不需要复杂线程安全
  • 接口约束 :通过接口访问单例功能,减少耦合

5. 应用场景#

  • 游戏状态管理
  • 资源管理器
  • 音频管理器
  • 全局事件系统

工厂方法模式#

1. 定义#

工厂方法模式(Factory Method Pattern)是一种创建型设计模式,用于创建对象但不直接实例化具体类,而是通过定义一个创建对象的接口,让子类决定实例化哪个类。

结合 Prefab 和 MonoBehaviour 的工厂方法,可以适配 Unity 的动态创建需求。

img

工厂方法模式的目的很简单:

  • 创建者和产品解耦
  • 提高产品的可扩展性,允许子类选择具体实现
  • 单一职责,产品创建代码独立出来,Unity中可以借此实现动态加载

2. 实现方式#

核心思想#

  • 定义一个抽象工厂类或接口,声明工厂方法(如 Create)。
  • 具体工厂类继承抽象工厂,实现具体对象的创建。
  • 客户端通过工厂接口调用创建方法,无需知道具体实现。

结构#

  • Product :抽象产品接口或基类(如 IEnemy)。
  • ConcreteProduct :具体产品类(如 Goblin、Dragon)。
  • Creator :抽象工厂类,声明工厂方法(如 CreateEnemy)。
  • ConcreteCreator :具体工厂类,实现工厂方法(如 GoblinFactory、DragonFactory)。

这里本人没有写过直接通过泛型提供的接口,我们用一段业务代码来理一理:

using UnityEngine;

// 抽象产品接口
public interface IEnemy
{
    void Attack();
}

// 具体产品:哥布林
public class Goblin : MonoBehaviour, IEnemy
{
    public void Attack()
    {
        Debug.Log("Goblin attacks with a club!");
    }
}

// 具体产品:龙
public class Dragon : MonoBehaviour, IEnemy
{
    public void Attack()
    {
        Debug.Log("Dragon breathes fire!");
    }
}

// 抽象工厂
public abstract class EnemyFactory : MonoBehaviour
{
    public abstract IEnemy CreateEnemy(Vector3 position);
}

// 具体工厂:哥布林工厂
public class GoblinFactory : EnemyFactory
{
    public override IEnemy CreateEnemy(Vector3 position)
    {
        GameObject goblinObject = new GameObject("Goblin");
        goblinObject.transform.position = position;
        IEnemy goblin = goblinObject.AddComponent<Goblin>();
        return goblin;
    }
}

// 具体工厂:龙工厂
public class DragonFactory : EnemyFactory
{
    public override IEnemy CreateEnemy(Vector3 position)
    {
        GameObject dragonObject = new GameObject("Dragon");
        dragonObject.transform.position = position;
        IEnemy dragon = dragonObject.AddComponent<Dragon>();
        return dragon;
    }
}

// 客户端代码:游戏管理器
public class GameManager : MonoBehaviour
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<GameManager>();
                if (_instance == null)
                {
                    GameObject singletonObject = new GameObject(nameof(GameManager));
                    _instance = singletonObject.AddComponent<GameManager>();
                    DontDestroyOnLoad(singletonObject);
                }
            }
            return _instance;
        }
    }

    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }
        _instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public void SpawnEnemy(EnemyFactory factory, Vector3 position)
    {
        IEnemy enemy = factory.CreateEnemy(position);
        enemy.Attack();
    }
}

3. 最佳实践:#

  • ScriptableObject工厂 :创建可配置的工厂资产
  • 对象池集成 :避免频繁实例化销毁
  • 参数化工厂 :支持不同创建参数
  • 异步加载 :使用Addressables异步加载资源

4. 应用场景:#

  • 动态敌人生成
  • UI元素创建
  • 技能特效生成
  • 道具系统

抽象工厂模式#

1. 定义#

抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定具体类。

而在 Unity 中,抽象工厂模式特别适合管理复杂对象的创建(如一组相关的 GameObject、组件或资源),例如创建不同主题的 UI 元素、不同类型的敌人及其武器等。

img

目的

  • 将一组相关对象的创建逻辑封装,保持一致性。
  • 支持动态切换产品族(如切换游戏主题或关卡风格)。
  • 在 Unity 中,适合批量创建相关 GameObject 或组件(如敌人 + 武器、UI 面板 + 按钮)。

2. 实现方式#

核心思想#

  • 定义一个抽象工厂接口或类,声明创建一组相关产品的方法。
  • 具体工厂类实现这些方法,创建具体产品。
  • 客户端通过抽象工厂接口调用创建方法,无需知道具体产品类。

结构#

  • AbstractProduct :抽象产品接口(如 IEnemy、IWeapon)。
  • ConcreteProduct :具体产品类(如 Goblin、Sword)。
  • AbstractFactory :抽象工厂接口,声明创建产品族的方法(如 CreateEnemy、CreateWeapon)。
  • ConcreteFactory :具体工厂类,实现创建特定产品族(如 ForestFactory 创建森林主题的敌人和武器)。
  • Client :使用抽象工厂接口创建产品族。

这里我们用抽象工厂对之前的工厂方法进行优化:

using UnityEngine;

// 抽象产品接口:敌人
public interface IEnemy
{
    void Attack();
}

// 抽象产品接口:武器
public interface IWeapon
{
    void Use();
}

// 具体产品:森林主题的哥布林
public class ForestGoblin : MonoBehaviour, IEnemy
{
    public void Attack()
    {
        Debug.Log("Forest Goblin attacks with a wooden club!");
    }
}

// 具体产品:森林主题的弓
public class ForestBow : MonoBehaviour, IWeapon
{
    public void Use()
    {
        Debug.Log("Forest Bow shoots an arrow!");
    }
}

// 具体产品:沙漠主题的蝎子
public class DesertScorpion : MonoBehaviour, IEnemy
{
    public void Attack()
    {
        Debug.Log("Desert Scorpion stings with its tail!");
    }
}

// 具体产品:沙漠主题的匕首
public class DesertDagger : MonoBehaviour, IWeapon
{
    public void Use()
    {
        Debug.Log("Desert Dagger slashes swiftly!");
    }
}

// 抽象工厂接口
public interface IEntityFactory
{
    IEnemy CreateEnemy(Vector3 position);
    IWeapon CreateWeapon(Vector3 position);
}

// 具体工厂:森林主题工厂
public class ForestFactory : MonoBehaviour, IEntityFactory
{
    [SerializeField] private GameObject goblinPrefab; // 可在 Inspector 中分配 Prefab
    [SerializeField] private GameObject bowPrefab;

    public IEnemy CreateEnemy(Vector3 position)
    {
        GameObject enemyObject = goblinPrefab ? Instantiate(goblinPrefab, position, Quaternion.identity) : new GameObject("ForestGoblin");
        return enemyObject.AddComponent<ForestGoblin>();
    }

    public IWeapon CreateWeapon(Vector3 position)
    {
        GameObject weaponObject = bowPrefab ? Instantiate(bowPrefab, position, Quaternion.identity) : new GameObject("ForestBow");
        return weaponObject.AddComponent<ForestBow>();
    }
}

// 具体工厂:沙漠主题工厂
public class DesertFactory : MonoBehaviour, IEntityFactory
{
    [SerializeField] private GameObject scorpionPrefab; // 可在 Inspector 中分配 Prefab
    [SerializeField] private GameObject daggerPrefab;

    public IEnemy CreateEnemy(Vector3 position)
    {
        GameObject enemyObject = scorpionPrefab ? Instantiate(scorpionPrefab, position, Quaternion.identity) : new GameObject("DesertScorpion");
        return enemyObject.AddComponent<DesertScorpion>();
    }

    public IWeapon CreateWeapon(Vector3 position)
    {
        GameObject weaponObject = daggerPrefab ? Instantiate(daggerPrefab, position, Quaternion.identity) : new GameObject("DesertDagger");
        return weaponObject.AddComponent<DesertDagger>();
    }
}

// 客户端代码:游戏管理器(结合单例模式)
public class GameManager : MonoBehaviour
{
    private static GameManager _instance;

    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<GameManager>();
                if (_instance == null)
                {
                    GameObject singletonObject = new GameObject(nameof(GameManager));
                    _instance = singletonObject.AddComponent<GameManager>();
                    DontDestroyOnLoad(singletonObject);
                }
            }
            return _instance;
        }
    }

    private void Awake()
    {
        if (_instance != null && _instance != this)
        {
            Destroy(gameObject);
            return;
        }
        _instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public void SpawnEntity(IEntityFactory factory, Vector3 enemyPosition, Vector3 weaponPosition)
    {
        IEnemy enemy = factory.CreateEnemy(enemyPosition);
        IWeapon weapon = factory.CreateWeapon(weaponPosition);
        enemy.Attack();
        weapon.Use();
    }
}

3. 与工厂方法模式的区别#

  • 工厂方法模式 :关注单一产品类型的创建(如只创建敌人),每个工厂负责一种产品。
  • 抽象工厂模式 :关注一组相关产品的创建(如敌人 + 武器),每个工厂创建整个产品族。

4. 最佳实践:#

  • ScriptableObject实现 :编辑器可配置
  • 主题切换 :运行时动态更换工厂
  • 资源预加载 :提前加载所需资源
  • 组合模式 :创建复杂对象结构

5. 应用场景:#

  • 关卡主题系统(森林/沙漠/雪地)
  • 角色套装(武器+护甲+技能)
  • UI皮肤系统
  • 多平台适配

建造者模式#

1. 定义#

建造者模式(Builder Pattern)是一种创建型设计模式,通过分步构建复杂对象,将对象的构造过程与其最终表示分离,由建造者负责构造步骤,导演者(Director)控制构建流程。

在 Unity 中,建造者模式特别适合创建复杂的 GameObject 或组件(如角色、场景对象、UI 面板),因为它允许灵活配置对象的属性,同时保持代码清晰和可维护。

img

优点:

  1. 分离构建与表示 :将对象的构建过程与其表示分离
  2. 精细控制构建过程 :可以精确控制复杂对象的创建步骤
  3. 复用构建过程 :相同的构建过程可以创建不同的产品
  4. 更好的封装性 :客户端不需要知道产品内部细节

2. 实现方式#

核心思想#

  • 定义一个建造者接口或抽象类,声明构建步骤(如设置属性、添加组件)。
  • 具体建造者实现这些步骤,创建具体对象。
  • 可选的导演者(Director)控制构建顺序,客户端通过导演者或直接调用建造者创建对象。
  • 最终返回产品(在 Unity 中通常是 GameObject 或组件)。

结构#

  • Product :最终生成的对象(如 GameObject 类型的玩家角色)。
  • Builder :抽象建造者接口,定义构建步骤(如 SetHealth、AddWeapon)。
  • ConcreteBuilder :具体建造者,实现构建步骤(如 PlayerBuilder)。
  • Director (可选):控制构建流程,调用建造者方法。
  • Client :使用建造者或导演者创建对象。

我们以UI系统为例:

// UI窗口产品
public class UIWindow
{
    public GameObject Root { get; set; }
    public Text Title { get; set; }
    public Button CloseButton { get; set; }
    public Image Background { get; set; }
    // 其他UI元素...
}

// UI建造者
public interface IUIWindowBuilder
{
    void CreateRoot();
    void AddTitle(string title);
    void AddCloseButton();
    void AddBackground(Sprite sprite);
    UIWindow GetResult();
}

// 具体实现
public class StandardWindowBuilder : IUIWindowBuilder
{
    private UIWindow window = new UIWindow();
  
    public void CreateRoot()
    {
        window.Root = new GameObject("Window");
        var rectTransform = window.Root.AddComponent<RectTransform>();
        // 设置rectTransform属性...
    }
  
    public void AddTitle(string title)
    {
        GameObject titleObj = new GameObject("Title");
        titleObj.transform.SetParent(window.Root.transform);
        window.Title = titleObj.AddComponent<Text>();
        window.Title.text = title;
        // 设置文本样式...
    }
  
    // 其他方法实现...
  
    public UIWindow GetResult()
    {
        return window;
    }
}

// 使用
UIWindow CreateStandardWindow()
{
    IUIWindowBuilder builder = new StandardWindowBuilder();
    builder.CreateRoot();
    builder.AddTitle("Options");
    builder.AddCloseButton();
    builder.AddBackground(Resources.Load<Sprite>("window_bg"));
    return builder.GetResult();
}

3. 面向Unity的优化#

除了最基础的调用,builder一般在实践过程中会和链式调用结合:

public class CharacterBuilder
{
    private Character character;
  
    public CharacterBuilder WithName(string name)
    {
        character.Name = name;
        return this;
    }
  
    public CharacterBuilder WithHealth(int health)
    {
        character.Health = health;
        return this;
    }
  
    public Character Build()
    {
        return character;
    }
}

// 使用
Character hero = new CharacterBuilder()
    .WithName("Hero")
    .WithHealth(100)
    .Build();

一个很经典的例子就是DOTween插件中的Sequence设置,采用闭包和建造者模式实现了函数式编程。

除了链式调用,于ScriptableObject结合,也是一个常用的扩展点:

// 角色类型配置
[CreateAssetMenu(fileName = "CharacterType", menuName = "Builders/Character Type")]
public class CharacterType : ScriptableObject
{
    public string typeName;
    public GameObject prefab;
    public int healthModifier;
    public float speedModifier;
    public Weapon[] availableWeapons;
    public Armor[] availableArmors;
}

// 角色外观配置
[CreateAssetMenu(fileName = "CharacterAppearance", menuName = "Builders/Character Appearance")]
public class CharacterAppearance : ScriptableObject
{
    public Color primaryColor;
    public Color secondaryColor;
    public Sprite portrait;
    public RuntimeAnimatorController animatorController;
}

// 高级建造者
public class AdvancedCharacterBuilder
{
    private CharacterType typeConfig;
    private CharacterAppearance appearanceConfig;
    private Character character = new Character();
  
    public AdvancedCharacterBuilder WithType(CharacterType type)
    {
        this.typeConfig = type;
        return this;
    }
  
    public AdvancedCharacterBuilder WithAppearance(CharacterAppearance appearance)
    {
        this.appearanceConfig = appearance;
        return this;
    }
  
    public AdvancedCharacterBuilder WithWeaponIndex(int index)
    {
        if (typeConfig != null && typeConfig.availableWeapons.Length > index)
        {
            character.Weapon = typeConfig.availableWeapons[index];
        }
        return this;
    }
  
    public Character Build()
    {
        if (typeConfig == null) throw new System.Exception("Character type config is required");
  
        // 创建游戏对象
        character.GameObject = Object.Instantiate(typeConfig.prefab);
        character.GameObject.name = typeConfig.typeName;
  
        // 设置属性
        character.Health = 100 + typeConfig.healthModifier;
        character.Speed = 3f + typeConfig.speedModifier;
  
        // 应用外观
        if (appearanceConfig != null)
        {
            var renderer = character.GameObject.GetComponent<SpriteRenderer>();
            if (renderer != null) renderer.color = appearanceConfig.primaryColor;
      
            var animator = character.GameObject.GetComponent<Animator>();
            if (animator != null) animator.runtimeAnimatorController = appearanceConfig.animatorController;
        }
  
        // 设置默认装备
        if (character.Weapon == null && typeConfig.availableWeapons.Length > 0)
        {
            character.Weapon = typeConfig.availableWeapons[0];
        }
  
        return character;
    }
}

// 使用示例
public class CharacterManager : MonoBehaviour
{
    public CharacterType warriorType;
    public CharacterAppearance redAppearance;
  
    void CreateWarrior()
    {
        var warrior = new AdvancedCharacterBuilder()
            .WithType(warriorType)
            .WithAppearance(redAppearance)
            .WithWeaponIndex(1)
            .Build();
    }
}

4. 最佳实践:#

  • 链式调用 :流畅接口设计
  • 配置分离 :使用ScriptableObject存储构建参数
  • 步骤验证 :确保构建过程完整性
  • 导演类 :封装常用构建流程

5. 应用场景:#

  • 复杂角色创建
  • 关卡配置
  • 粒子系统组合
  • 对话系统构建

原型模式#

1. 定义#

原型模式(Prototype Pattern)是一种创建型设计模式,通过复制现有对象(原型)来创建新对象,而无需直接使用构造函数或复杂初始化逻辑。

在 Unity 中,原型模式特别适合快速创建相似 GameObject 或组件的副本(如敌人、道具、场景对象),利用 Unity 的 Instantiate 方法实现高效克隆。当然除此之外,CSharp本身也提供了IClonnable接口,用来实现遵循原型模式的深浅拷贝。

img

目的

  • 避免重复的初始化逻辑或构造函数调用。
  • 支持动态创建相似对象,适合运行时复制。

2. 实现方式#

核心思想#

  • 定义一个原型接口或抽象类,声明克隆方法(如 Clone)。
  • 具体原型类实现克隆逻辑,通常通过 Unity 的 Instantiate 复制 GameObject 或组件。
  • 客户端通过调用克隆方法获取新对象,并可自定义配置。

结构#

  • Prototype :抽象原型接口或类,声明克隆方法(如 ICloneable 或自定义 Clone)。
  • ConcretePrototype :具体原型类,实现克隆逻辑(如 EnemyPrototype)。
  • Client :使用原型对象调用克隆方法创建新对象。

Unity的Prefab系统本身就是原型模式的完美实现:

public class PrefabSpawner : MonoBehaviour
{
    public GameObject enemyPrefab;  // 这是我们的原型
  
    void SpawnEnemy()
    {
        // 克隆prefab创建新实例
        GameObject newEnemy = Instantiate(enemyPrefab);
        newEnemy.transform.position = Random.insideUnitSphere * 5f;
    }
}

借此我们可以实现一个敌人生成系统:

public class EnemySpawner : MonoBehaviour
{
    [System.Serializable]
    public class EnemyPrototype
    {
        public GameObject prefab;
        public int minHealth;
        public int maxHealth;
        public float minSpeed;
        public float maxSpeed;
    }
  
    public EnemyPrototype[] enemyPrototypes;
  
    public void SpawnRandomEnemy()
    {
        EnemyPrototype prototype = enemyPrototypes[Random.Range(0, enemyPrototypes.Length)];
        GameObject enemy = Instantiate(prototype.prefab, transform.position, Quaternion.identity);
  
        Enemy enemyScript = enemy.GetComponent<Enemy>();
        enemyScript.health = Random.Range(prototype.minHealth, prototype.maxHealth);
        enemyScript.speed = Random.Range(prototype.minSpeed, prototype.maxSpeed);
    }
}

当然,除了最基本的克隆,我们偶尔还会使用注册表来管理原型:

public class PrototypeRegistry : MonoBehaviour
{
    private static PrototypeRegistry _instance;
    public static PrototypeRegistry Instance => _instance;
  
    [SerializeField] private List<EnemyPrototype> enemyPrototypes = new List<EnemyPrototype>();
    private Dictionary<string, EnemyPrototype> enemyPrototypeDict = new Dictionary<string, EnemyPrototype>();
  
    void Awake()
    {
        _instance = this;
  
        foreach (var prototype in enemyPrototypes)
        {
            enemyPrototypeDict.Add(prototype.name, prototype);
        }
    }
  
    public GameObject SpawnEnemy(string prototypeName, Vector3 position)
    {
        if (enemyPrototypeDict.TryGetValue(prototypeName, out EnemyPrototype prototype))
        {
            GameObject enemy = prototype.CreateInstance();
            enemy.transform.position = position;
            return enemy;
        }
        return null;
    }
  
    // 编辑器方法,用于注册新原型
    #if UNITY_EDITOR
    public void RegisterNewPrototype(EnemyPrototype prototype)
    {
        if (!enemyPrototypes.Contains(prototype))
        {
            enemyPrototypes.Add(prototype);
            enemyPrototypeDict.Add(prototype.name, prototype);
        }
    }
    #endif
}

3. 深浅拷贝#

原型模式本身其实衍生出了一个很重要的概念:深拷贝和浅拷贝。

浅拷贝#

浅拷贝只复制对象的顶层结构,对于引用类型的成员变量,只复制引用而不复制引用的对象。

C#提供了MemberwiseClone的通用方法,直接调用即可:

public class CharacterStats
{
    public string name;
    public int level;
    public float health;
  
    // 浅拷贝方法
    public CharacterStats ShallowCopy()
    {
        return (CharacterStats)this.MemberwiseClone();
    }
}

// 使用示例
CharacterStats original = new CharacterStats();
original.name = "Hero";
original.level = 10;
original.health = 100f;

CharacterStats copy = original.ShallowCopy();

而对于Unity组件,手动实现一套浅拷贝也不是很难:

public class Enemy : MonoBehaviour
{
    public int damage;
    public float attackSpeed;
  
    public Enemy ShallowCopy()
    {
        Enemy copy = new GameObject("Enemy_Copy").AddComponent<Enemy>();
        copy.damage = this.damage;
        copy.attackSpeed = this.attackSpeed;
        return copy;
    }
}

深拷贝#

深拷贝会复制对象及其所有引用的对象,在新的内存中创建一个完全独立的副本。

最经典的深拷贝无非序列化,对于所有语言都是通用的实现逻辑:

using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

public static class ObjectCopier
{
    // 通用的深拷贝方法(要求类标记为[Serializable])
    public static T DeepCopy<T>(T obj)
    {
        if (!typeof(T).IsSerializable)
        {
            throw new ArgumentException("The type must be serializable.", nameof(obj));
        }
  
        if (ReferenceEquals(obj, null))
        {
            return default(T);
        }
  
        BinaryFormatter formatter = new BinaryFormatter();
        using (MemoryStream stream = new MemoryStream())
        {
            formatter.Serialize(stream, obj);
            stream.Seek(0, SeekOrigin.Begin);
            return (T)formatter.Deserialize(stream);
        }
    }
}

// 使用示例(类必须标记为[Serializable])
[Serializable]
public class PlayerData
{
    public string playerName;
    public int score;
    public List<string> inventory;
}

PlayerData original = new PlayerData();
// 初始化original...
PlayerData copy = ObjectCopier.DeepCopy(original);

在CSharp中,也可以通过实现ICloneable接口手动实现深拷贝

public class GameItem : ICloneable
{
    public string itemName;
    public Sprite icon;
    public List<ItemEffect> effects;
  
    // 浅拷贝实现
    public object Clone()
    {
        return this.MemberwiseClone();
    }
  
    // 深拷贝实现
    public GameItem DeepClone()
    {
        GameItem clone = (GameItem)this.MemberwiseClone();
        clone.effects = new List<ItemEffect>();
        foreach (var effect in this.effects)
        {
            clone.effects.Add(effect.DeepClone()); // 假设ItemEffect实现了深拷贝
        }
        return clone;
    }
}

Unity中实现深拷贝时,要注意:

Unity对象生命周期

  • 不能直接深拷贝UnityEngine.Object派生类(如GameObject、Component)
  • 需要使用Instantiate来创建Unity对象的新实例

循环引用问题

  • 深拷贝时要处理可能的循环引用
  • 可以使用序列化方式或维护一个已拷贝对象字典

性能考虑

  • 深拷贝可能很耗性能,特别是对复杂对象
  • 对于频繁拷贝的对象,考虑使用对象池

ScriptableObject的特殊性

  • ScriptableObject.CreateInstance()用于创建新实例
  • 需要手动复制所有需要共享的字段

预制体(Prefab)的处理

  • 使用PrefabUtility.InstantiatePrefab来保持预制体连接
  • 普通Instantiate会创建独立的实例

为了考虑这些复杂的内容,我们一般会封装一份拷贝工具,本人曾用过类似下面的实现,通过泛型和反射实现完整的深拷贝:

using System.Collections.Generic;
using System.Reflection;

public static class DeepCopyUtility
{
    private static Dictionary<object, object> _copiedObjects = new Dictionary<object, object>();
  
    public static T DeepCopy<T>(T original) where T : class
    {
        _copiedObjects.Clear();
        return InternalCopy(original) as T;
    }
  
    private static object InternalCopy(object original)
    {
        if (original == null) return null;
  
        // 处理UnityEngine.Object派生类
        if (original is UnityEngine.Object unityObj)
        {
            // 对于Unity对象,我们使用Instantiate创建新实例
            if (unityObj is GameObject gameObj)
            {
                return GameObject.Instantiate(gameObj);
            }
            else if (unityObj is Component component)
            {
                GameObject copiedGameObject = GameObject.Instantiate(component.gameObject);
                return copiedGameObject.GetComponent(component.GetType());
            }
            // 其他Unity对象可能需要特殊处理
            return unityObj; // 默认返回原对象(不是真正的深拷贝)
        }
  
        // 检查是否已经拷贝过该对象
        if (_copiedObjects.TryGetValue(original, out object copied))
        {
            return copied;
        }
  
        Type type = original.GetType();
  
        // 处理字符串和值类型
        if (type.IsPrimitive || type.IsEnum || type == typeof(string))
        {
            return original;
        }
  
        // 处理数组
        if (type.IsArray)
        {
            Type elementType = type.GetElementType();
            Array originalArray = original as Array;
            Array copiedArray = Array.CreateInstance(elementType, originalArray.Length);
            _copiedObjects.Add(original, copiedArray);
      
            for (int i = 0; i < originalArray.Length; i++)
            {
                copiedArray.SetValue(InternalCopy(originalArray.GetValue(i)), i);
            }
      
            return copiedArray;
        }
  
        // 处理委托
        if (type.IsSubclassOf(typeof(Delegate)))
        {
            // 不拷贝委托,直接返回原对象
            return original;
        }
  
        // 创建新实例
        object copy = Activator.CreateInstance(type);
        _copiedObjects.Add(original, copy);
  
        // 复制字段
        FieldInfo[] fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        foreach (FieldInfo field in fields)
        {
            // 跳过只读字段
            if (field.IsInitOnly) continue;
      
            object fieldValue = field.GetValue(original);
            field.SetValue(copy, InternalCopy(fieldValue));
        }
  
        return copy;
    }
}

一个典型的应用场景就是卡牌类游戏的卡牌复制:

[System.Serializable]
public class CardData
{
    public string cardName;
    public int attack;
    public int health;
    public Sprite artwork;
    public List<CardEffect> effects;
  
    // 浅拷贝
    public CardData ShallowCopy()
    {
        return (CardData)this.MemberwiseClone();
    }
  
    // 深拷贝
    public CardData DeepCopy()
    {
        CardData copy = ShallowCopy();
        copy.effects = new List<CardEffect>();
        foreach (var effect in effects)
        {
            copy.effects.Add(effect.DeepCopy()); // 假设CardEffect实现了深拷贝
        }
        return copy;
    }
}

// 在Unity中使用
public class Card : MonoBehaviour
{
    public CardData data;
  
    public Card CreateCopy()
    {
        GameObject cardCopyObj = Instantiate(this.gameObject);
        Card cardCopy = cardCopyObj.GetComponent<Card>();
        cardCopy.data = data.DeepCopy(); // 使用深拷贝
        return cardCopy;
    }
}

在Unity中实现深拷贝和浅拷贝时,最重要的是要理解哪些对象需要真正独立复制,哪些可以共享引用。

对于Unity引擎对象(GameObject、Component等),通常需要使用Instantiate来创建新实例(如果确认其在场景中实例化的情况下),而对于自定义数据类,则可以使用序列化或其他深拷贝技术。

4. 最佳实践:#

  • Prefab利用 :Unity原生支持原型模式
  • ScriptableObject原型 :数据驱动设计
  • 变体支持 :克隆时添加随机变化
  • 原型注册表 :集中管理所有原型

5. 应用场景:#

  • 敌人生成系统
  • 道具复制
  • 地形生成
  • 技能效果模板

对象池模式#

严格来讲,市面上常见23种设计模式介绍中,并不会提到对象池的模式,然而这种模式在游戏开发方面应用广泛,也是不得不提的一种关键创建型设计模式。

1. 定义#

对象池模式(Object Pool Pattern)维护一个对象池,存储一组预先创建的可重用对象,客户端从池中获取对象使用,用完后归还给池,而不是销毁。

在 Unity 中,对象池模式特别适合优化性能,因为 Unity 中 GameObject 的实例化(Instantiate)和销毁(Destroy)操作开销较大,尤其在需要频繁生成对象(如子弹、敌人、粒子效果)的场景。

目的

  • 减少 Instantiate 和 Destroy 的性能开销。
  • 提高运行时效率,适合频繁创建/销毁对象的场景。
  • 在 Unity 中,优化内存管理和帧率,特别适合子弹、敌人、特效等。

2. 实现方式#

核心思想#

  • 创建一个对象池,预先实例化一组 GameObject 或组件。
  • 提供方法从池中获取可用对象(激活)或归还对象(禁用)。
  • 若池中无可用对象,可动态扩展池大小。
  • 客户端通过池接口获取和归还对象,无需直接操作 Instantiate 或 Destroy。

结构#

  • PooledObject :池化的对象,通常是 GameObject 或组件(如 Bullet)。
  • ObjectPool :管理对象池,负责初始化、获取、归还对象。
  • Client :使用对象池获取和归还对象。

给出一个常规的代码实现:

public class ProjectilePool : MonoBehaviour
{
    public GameObject projectilePrefab;
    public int poolSize = 20;
  
    private Queue<GameObject> pool = new Queue<GameObject>();
  
    void Start()
    {
        for (int i = 0; i < poolSize; i++)
        {
            GameObject projectile = Instantiate(projectilePrefab);
            projectile.SetActive(false);
            pool.Enqueue(projectile);
        }
    }
  
    public GameObject GetProjectile(Vector3 position)
    {
        if (pool.Count == 0)
        {
            // 池空了,创建新实例
            GameObject newProjectile = Instantiate(projectilePrefab);
            pool.Enqueue(newProjectile);
        }
  
        GameObject projectile = pool.Dequeue();
        projectile.transform.position = position;
        projectile.SetActive(true);
        return projectile;
    }
  
    public void ReturnProjectile(GameObject projectile)
    {
        projectile.SetActive(false);
        pool.Enqueue(projectile);
    }
}

代码很简洁,但是实际工程中的对象池会用到很多优化方案。

3. 最佳实践:#

  • 通用池实现 :创建泛型对象池
  • 预热机制 :场景加载时初始化对象
  • 容量管理 :动态调整池大小
  • 回收策略 :自动回收超时对象

4. 应用场景:#

  • 子弹管理系统
  • 特效复用
  • NPC生成
  • UI元素回收

总结#

我们写一个所有创建型模式的大杂烩,作为总结:

// 1. 单例管理全局角色系统
public class CharacterSystem : Singleton<CharacterSystem>
{
    // 2. 工厂创建角色
    public Character CreateCharacter(CharacterFactory factory, CharacterConfig config)
    {
        return factory.Create(config);
    }
}

// 3. 抽象工厂接口
public interface CharacterFactory
{
    Character Create(CharacterConfig config);
}

// 4. 具体工厂 - 英雄角色
public class HeroFactory : CharacterFactory
{
    public Character Create(CharacterConfig config)
    {
        // 5. 使用建造者构建复杂角色
        return new CharacterBuilder(config)
            .WithClass(CharacterClass.Hero)
            .WithDefaultSkills()
            .Build();
    }
}

// 6. 角色配置(原型)
[CreateAssetMenu(menuName = "Characters/Config")]
public class CharacterConfig : ScriptableObject
{
    public GameObject basePrefab;
    public int baseHealth;
    public Skill[] defaultSkills;
  
    // 7. 原型克隆方法
    public Character Clone()
    {
        Character clone = Instantiate(basePrefab).GetComponent<Character>();
        clone.Initialize(this);
        return clone;
    }
}

// 8. 对象池管理角色实例
public class CharacterPool : GenericObjectPool<Character>
{
    protected override Character CreatePooledItem()
    {
        return CharacterSystem.Instance.CreateCharacter(
            new HeroFactory(), 
            Resources.Load<CharacterConfig>("HeroConfig"));
    }
}

在Unity中,创建型设计模式可以遵循如下的实践方案(但是谨记,不要把方案当作教条)

ScriptableObject驱动设计

  • 将配置数据与逻辑分离
  • 创建可重用的数据资产
  • 支持热重载和实时调整

预制件(Prefab)作为基础原型

  • 充分利用Unity原生原型系统
  • 支持嵌套预制件和变体
  • 结合Addressables实现动态加载

组合使用模式

  • 工厂+对象池:高效管理实例
  • 建造者+原型:灵活创建复杂对象
  • 抽象工厂+策略:支持运行时切换实现

性能优化

  • 避免频繁Instantiate/Destroy
  • 使用对象池管理高频创建对象
  • 异步加载大型资源

架构设计

  • 依赖注入控制对象创建
  • 事件系统解耦对象间依赖
  • ECS架构与创建型模式结合

编辑器扩展

  • 为工厂创建自定义编辑器
  • 可视化建造者配置界面
  • 原型管理器工具

平台适配

  • 使用抽象工厂处理平台差异
  • 针对不同平台配置不同原型
  • 移动端优化:减少运行时创建

当然,没有”最好”的模式,只有”最适合”当前场景的模式。实践中应根据具体需求灵活选择和组合不同的设计模式。写代码还是先实现功能再进行优化最为高效。

场景推荐模式原因
全局访问点单例提供统一访问入口
简单对象创建工厂方法封装创建逻辑
相关对象族抽象工厂创建整套相关对象
复杂对象构建建造者分步骤构建复杂对象
高效对象复用原型+对象池减少实例化开销
配置驱动创建ScriptableObject数据与逻辑分离
设计模式笔记(一)
https://monsterstation.netlify.app/posts/se/pattern/创建型设计模式/
作者
Furry Monster
发布于
2025-07-01
许可协议
CC BY-NC-SA 4.0