Unity 设计模式 之 创建型模式 -【单例模式】【原型模式】 【建造者模式】
目录
Unity 设计模式 之 创建型模式 -【单例模式】【原型模式】 【建造者模式】
3、 具体建造者类:木屋建造者 WoodenHouseBuilder 和砖屋建造者 BrickHouseBuilder
一、简单介绍
设计模式 是指在软件开发中为解决常见问题而总结出的一套 可复用的解决方案。这些模式是经过长期实践证明有效的 编程经验总结,并可以在不同的项目中复用。设计模式并不是代码片段,而是对常见问题的 抽象解决方案,它提供了代码结构和模块间交互的一种设计思路,帮助开发者解决特定的设计问题。
二、单例模式 (Singleton Pattern)
单例模式 (Singleton Pattern) 是一种创建型设计模式,保证一个类只有一个实例,并提供一个全局访问点来获取该实例。它通过控制类的实例化过程,确保系统中只有一个该类的对象存在。
在单例模式中,类的构造函数通常是私有的,防止外部通过 new
来创建对象,类内部维护一个静态实例,通过公共的静态方法提供访问。
1、什么时候使用单例模式
2、单例模式的好处
3、使用单例模式的注意事项
-
全局状态的复杂性:虽然单例模式提供全局访问,但这也意味着它引入了全局状态,可能导致系统的状态管理变得复杂,特别是在大型系统中。
-
并发和线程安全:在多线程环境下,需要确保单例的实现是线程安全的,否则可能会出现多个线程同时创建实例的情况,导致数据不一致。
-
难以扩展:由于单例模式限制了对象的创建数量,在某些情况下,可能不利于系统的扩展或测试。比如,在单元测试中,单例模式可能难以进行模拟。
-
滥用风险:单例模式提供全局访问点,容易被滥用为全局变量,违背面向对象设计中高内聚、低耦合的原则。因此,使用时应考虑是否真的有必要将类设计为单例。
总之,单例模式 主要用于控制对象的实例化,确保系统中只有一个类的实例,并通过全局访问点来控制对象的使用。它适用于需要全局共享资源、统一管理的场景,如日志系统、数据库连接池等。尽管单例模式在某些场景下有助于提升系统的稳定性和效率,但也应谨慎使用,以避免全局状态管理复杂化或滥用全局访问带来的耦合问题。
三、在 Unity 中使用 单例模式
在 Unity 中,实现一个线程安全的普通类和MonoBehaviour 类的泛型单例时,必须考虑以下几点:
1、普通型(new)泛型单例模式
方法一:
public abstract class Singleton<T> where T : class, new()
{
private static T instance = null;
// 多线程安全机制
private static readonly object locker = new object();
public static T Instance
{
get
{
lock (locker)
{
if (instance == null)
instance = new T();
return instance;
}
}
}
}
方法二:
using System;
using System.Reflection;
/// <summary>
/// 单例
/// 继承需要一个非公有的无参构造函数
/// </summary>
/// <typeparam name="T">类名的泛型</typeparam>
public abstract class Singleton<T> where T : class
{
private static T instance = null;
// 多线程安全机制
private static readonly object locker = new object();
public static T Instance
{
get
{
// 线程锁
lock (locker)
{
if (null == instance)
{
// 反射获取实例
var octors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic) ;
// 获取无参数的非公共构造函数
var octor = Array.Find(octors, c => c.GetParameters().Length == 0);
// 没有则提示没有私有的构造函数
if (null == octor)
{
throw new Exception("No NonPublic constructor without 0 parameter");
}
// 实例化
instance = octor.Invoke(null) as T;
}
return instance;
}
}
}
/// <summary>
/// 构造函数
/// 避免外界new
/// </summary>
protected Singleton() { }
}
2、继承 MonoBehaviour 的泛型单例模式
using UnityEngine;
public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance = null;
private static readonly object locker = new object();
private static bool bAppQuitting;
public static T Instance
{
get
{
if (bAppQuitting)
{
instance = null;
return instance;
}
lock (locker)
{
if (instance == null)
{
// 保证场景中只有一个 单例
T[] managers = Object.FindObjectsOfType(typeof(T)) as T[];
if (managers.Length != 0)
{
if (managers.Length == 1)
{
instance = managers[0];
instance.gameObject.name = typeof(T).Name;
return instance;
}
else
{
Debug.LogError("Class " + typeof(T).Name + " exists multiple times in violation of singleton pattern. Destroying all copies");
foreach (T manager in managers)
{
Destroy(manager.gameObject);
}
}
}
var singleton = new GameObject();
instance = singleton.AddComponent<T>();
singleton.name = "(singleton)" + typeof(T);
singleton.hideFlags = HideFlags.None;
DontDestroyOnLoad(singleton);
}
instance.hideFlags = HideFlags.None;
return instance;
}
}
}
protected virtual void Awake()
{
bAppQuitting = false;
}
protected virtual void OnDestroy()
{
bAppQuitting = true;
}
}
3、单例使用方式
继承泛型单例模式即可
public class Test1 : MonoSingleton<Test1>{}
// 方法一
public class Test2 : Singleton<Test2>{}
// 方法二
public class Test2 : Singleton<Test2>{{private Test2(){}}
4、实现分析
-
普通类单例:
- 通过 lock 实现线程安全,避免了多线程环境下重复创建实例的问题。
- 将构造函数设为
protected
,确保外部不能通过new
关键字实例化该类。 - 在 Unity 中,适合管理游戏状态、配置数据等不需要与 Unity 引擎生命周期强绑定的对象。
-
MonoBehaviour 类单例:
- 使用了
lock
机制确保多线程环境中的安全性。 - 使用
FindObjectOfType<T>
检查场景中是否已经有该类型的实例,如果没有则创建新的对象并添加该组件。 - 通过
DontDestroyOnLoad
确保实例在场景切换时不被销毁,适合音频管理器、游戏控制器等需要跨场景保持的对象。
- 使用了
这两种单例的模式在 Unity 中广泛适用,可以有效地管理全局对象和跨场景对象。
四、原型模式 (Prototype Pattern)
原型模式 (Prototype Pattern) 是一种创建型设计模式,它允许通过复制现有的对象来创建新对象,而不是通过直接实例化类。这意味着你可以通过克隆原型对象来生成新的实例,而不必依赖类的构造函数。该模式的核心思想是,通过创建一个对象的副本(即原型)来避免昂贵的初始化操作。
在 C# 中,通常通过实现 ICloneable
接口或者自定义的克隆方法来实现原型模式。
1、原型模式的原理
原型模式的工作原理是:
- 定义一个原型接口或基类,用来提供克隆的方法(如
Clone
)。 - 具体的类实现该接口,提供自己的克隆方法,该方法返回当前对象的深拷贝或浅拷贝。
- 客户端可以通过克隆原型对象来创建新对象,而无需知道如何构造对象。
拷贝的类型可以分为:
- 浅拷贝 (Shallow Copy):只复制对象的基本数据类型,对于引用类型的字段,只复制引用地址。
- 深拷贝 (Deep Copy):不仅复制对象的所有值,还递归地复制所有引用对象,创建独立的副本。
2、什么时候使用原型模式
-
需要频繁创建相似对象时:如果系统需要创建大量相似或结构复杂的对象时,通过克隆原型对象来生成新对象可以提高性能。
-
对象的创建代价高昂时:如果对象的初始化非常复杂,包含很多属性设置或涉及昂贵的资源操作(如网络连接、文件系统操作),使用原型模式避免了重复创建过程。
-
避免对象过多的子类化:当不希望为了创建新对象而引入更多的子类或扩展类时,原型模式可以避免通过继承创建不同类型对象的繁琐过程。
-
需要保存对象的历史状态或备份时:如果需要将某个对象的状态备份或在某个时刻进行复制(如撤销操作、快照功能),可以通过原型模式来克隆对象。
3、使用原型模式的好处
-
提高对象创建效率:对于某些对象的创建过程非常复杂或耗时时,通过克隆现有对象(即原型)可以避免重复的复杂初始化,节省时间和资源。
-
避免子类膨胀:通过原型模式,程序不需要通过继承或工厂模式来生成对象,这可以减少子类的数量,避免继承层次的膨胀。
-
动态生成对象:原型模式允许在运行时动态生成对象,不需要事先知道具体类的名称。
-
减少对象依赖:克隆对象是独立于具体类的,原型模式不需要直接依赖构造函数,这降低了对象之间的耦合度。
-
灵活性高:通过克隆,可以对现有的对象进行修改后创建新的对象,特别适合那些需要频繁修改对象属性的场景。
4、使用原型模式的注意事项
-
克隆的深浅拷贝:对于引用类型字段,使用浅拷贝时需要特别小心,浅拷贝只复制引用,而不复制对象本身,可能导致两个对象共用同一份数据。若需要完全独立的对象,必须使用深拷贝。
-
复杂对象的克隆:当对象包含复杂的嵌套结构或其他依赖时,确保实现合适的深拷贝逻辑,否则可能会出现数据共享或数据覆盖问题。
-
性能考虑:尽管原型模式避免了对象的重复构造,但深拷贝可能引入较大的性能开销,特别是在对象嵌套复杂时。因此,在性能敏感的场景下要慎重考虑深拷贝的实现。
-
原型的可变性:当通过原型模式创建对象时,如果不慎修改了原型本身的状态,所有基于该原型创建的对象也可能受到影响。因此在设计时,需要确保原型对象的状态是安全的。
总之,原型模式 通过复制现有对象来生成新对象,避免了类构造的开销,特别适用于对象创建代价高昂或需要动态创建对象的场景。它提供了灵活的对象创建方式,减少了类的复杂性和耦合度。使用时需要根据具体需求选择浅拷贝或深拷贝,并确保对象的可复制性和独立性。
五、在 Unity 中使用 原型模式
在 Unity 中,原型模式可以应用于场景中需要频繁生成的对象,比如 3D 模型(如立方体、球体等)。通过原型模式,我们可以避免每次都从头实例化对象,而是通过复制现有的对象来创建新的实例。
我们将创建一个简单的原型模式应用,用于克隆 Unity 中的 3D 对象(立方体、球体等),并根据用户输入生成多个克隆对象。
1、 定义基类 ShapePrototype
using UnityEngine;
// 定义一个抽象的原型基类
public abstract class ShapePrototype : MonoBehaviour
{
// 定义一个抽象的克隆方法
public abstract ShapePrototype Clone();
// 通用的显示形状信息的方法
public abstract void DisplayInfo();
}
2、创建具体的形状类
2.1 立方体类
using UnityEngine;
public class Cube : ShapePrototype
{
// 重写克隆方法
public override ShapePrototype Clone()
{
// 实现浅拷贝,复制当前对象的属性
GameObject clone = Instantiate(this.gameObject);
return clone.GetComponent<ShapePrototype>();
}
public override void DisplayInfo()
{
Debug.Log("This is a Cube.");
}
}
2.2 球体类
using UnityEngine;
public class Sphere : ShapePrototype
{
// 重写克隆方法
public override ShapePrototype Clone()
{
// 实现浅拷贝,复制当前对象的属性
GameObject clone = Instantiate(this.gameObject);
return clone.GetComponent<ShapePrototype>();
}
public override void DisplayInfo()
{
Debug.Log("This is a Sphere.");
}
}
3、创建 ShapeSpawner
负责克隆对象
using UnityEngine;
public class ShapeSpawner : MonoBehaviour
{
public ShapePrototype cubePrototype; // 立方体原型
public ShapePrototype spherePrototype; // 球体原型
private void Update()
{
// 按下 C 键克隆立方体
if (Input.GetKeyDown(KeyCode.C))
{
ShapePrototype cubeClone = cubePrototype.Clone();
cubeClone.transform.position = new Vector3(Random.Range(-5, 5), 1, Random.Range(-5, 5));
cubeClone.DisplayInfo();
}
// 按下 S 键克隆球体
if (Input.GetKeyDown(KeyCode.S))
{
ShapePrototype sphereClone = spherePrototype.Clone();
sphereClone.transform.position = new Vector3(Random.Range(-5, 5), 1, Random.Range(-5, 5));
sphereClone.DisplayInfo();
}
}
}
4、设置场景中的原型对象
- 创建一个 Unity 场景,并添加一个空物体
ShapeSpawner
,将上面的ShapeSpawner
脚本挂载到该物体上。 - 在场景中创建一个立方体和一个球体,并将它们的
Cube
和Sphere
脚本分别挂载到立方体和球体上。 - 在
ShapeSpawner
脚本的cubePrototype
和spherePrototype
变量中,分别拖拽场景中的立方体和球体作为原型对象。
5、运行效果
- 按下
C
键:克隆一个新的立方体,随机放置在场景中,并在控制台输出 "This is a Cube." - 按下
S
键:克隆一个新的球体,随机放置在场景中,并在控制台输出 "This is a Sphere."
每次克隆都是基于场景中的原型对象,这样可以避免重新生成对象的开销,并且保证克隆对象与原型对象的所有属性相同。
节省资源,通过克隆现有对象,而不是每次都从头实例化对象,可以提高性能,尤其是在场景中需要大量生成对象时;灵活性,可以动态调整原型对象的属性,并通过克隆生成新的实例,而不需要修改对象的创建逻辑;方便扩展,如果需要新的形状,可以轻松扩展原型类,无需修改现有代码。
这种模式在 Unity 的游戏开发中非常适合用于生成大量相似对象(如敌人、物品、特效等)。
六、建造者模式 (Builder Pattern)
建造者模式 (Builder Pattern) 是一种创建型设计模式,它将复杂对象的构建过程与对象的表示分离,使得同样的构建过程可以创建不同的对象。建造者模式特别适用于那些构建步骤复杂且具有多种配置的对象。
在建造者模式中,通常有以下几个关键部分:
1、什么时候使用建造者模式
2、使用建造者模式的好处
3、建造者模式的注意事项
总之,建造者模式通过分离对象的构建过程和表示方式,使得构建复杂对象的流程更加清晰和灵活。它非常适合用于那些具有多个可选参数、构建过程复杂或者有不同表示方式的对象。建造者模式的使用可以提高代码的可读性和可维护性,同时减少对象构建中的复杂性。
七、在 Unity 中使用 建造者模式
在 Unity 中,建造者模式可以应用于构建复杂的 3D 场景或对象。假设我们要在场景中建造一个由多个部分组成的复杂建筑(如房子),包括地基、墙壁、屋顶等,这正是使用建造者模式的典型场景。
我们将设计一个简单的场景,展示如何通过建造者模式生成不同配置的房屋。房屋由三部分组成:地基、墙壁和屋顶。通过建造者模式,我们可以灵活地创建不同样式的房屋。
步骤:
1、定义房屋类 House
using UnityEngine;
// 房屋类,包含房屋的各个部分
public class House
{
public GameObject Foundation { get; set; }
public GameObject Walls { get; set; }
public GameObject Roof { get; set; }
public void ShowHouseInfo()
{
Debug.Log("House has been built with Foundation, Walls, and Roof.");
}
}
2、定义建造者接口 IHouseBuilder
public interface IHouseBuilder
{
void BuildFoundation();
void BuildWalls();
void BuildRoof();
House GetResult();
}
3、 具体建造者类:木屋建造者 WoodenHouseBuilder
和砖屋建造者 BrickHouseBuilder
3.1 木屋建造者
using UnityEngine;
public class WoodenHouseBuilder : IHouseBuilder
{
private House house = new House();
public void BuildFoundation()
{
house.Foundation = GameObject.CreatePrimitive(PrimitiveType.Cube);
house.Foundation.transform.localScale = new Vector3(5, 0.5f, 5);
house.Foundation.GetComponent<Renderer>().material.color = Color.gray;
}
public void BuildWalls()
{
house.Walls = GameObject.CreatePrimitive(PrimitiveType.Cube);
house.Walls.transform.localScale = new Vector3(5, 2.5f, 5);
house.Walls.transform.position = new Vector3(0, 1.5f, 0);
house.Walls.GetComponent<Renderer>().material.color = Color.yellow; // 木墙
}
public void BuildRoof()
{
house.Roof = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
house.Roof.transform.localScale = new Vector3(5, 1, 5);
house.Roof.transform.position = new Vector3(0, 3f, 0);
house.Roof.GetComponent<Renderer>().material.color = Color.red;
}
public House GetResult()
{
return house;
}
}
3.2 砖屋建造者
using UnityEngine;
public class BrickHouseBuilder : IHouseBuilder
{
private House house = new House();
public void BuildFoundation()
{
house.Foundation = GameObject.CreatePrimitive(PrimitiveType.Cube);
house.Foundation.transform.localScale = new Vector3(5, 0.5f, 5);
house.Foundation.GetComponent<Renderer>().material.color = Color.gray;
}
public void BuildWalls()
{
house.Walls = GameObject.CreatePrimitive(PrimitiveType.Cube);
house.Walls.transform.localScale = new Vector3(5, 2.5f, 5);
house.Walls.transform.position = new Vector3(0, 1.5f, 0);
house.Walls.GetComponent<Renderer>().material.color = Color.red; // 砖墙
}
public void BuildRoof()
{
house.Roof = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
house.Roof.transform.localScale = new Vector3(5, 1, 5);
house.Roof.transform.position = new Vector3(0, 3f, 0);
house.Roof.GetComponent<Renderer>().material.color = Color.green;
}
public House GetResult()
{
return house;
}
}
4、定义指挥者 Director
public class Director
{
private IHouseBuilder houseBuilder;
public Director(IHouseBuilder builder)
{
houseBuilder = builder;
}
// 控制房屋的建造流程
public void ConstructHouse()
{
houseBuilder.BuildFoundation();
houseBuilder.BuildWalls();
houseBuilder.BuildRoof();
}
// 获取构建好的房屋
public House GetHouse()
{
return houseBuilder.GetResult();
}
}
5、 在 Unity 场景中使用建造者模式
using UnityEngine;
public class HouseBuilderExample : MonoBehaviour
{
void Start()
{
// 创建一个木屋建造者
IHouseBuilder woodenHouseBuilder = new WoodenHouseBuilder();
Director director = new Director(woodenHouseBuilder);
director.ConstructHouse();
House woodenHouse = director.GetHouse();
woodenHouse.ShowHouseInfo();
// 创建一个砖屋建造者
IHouseBuilder brickHouseBuilder = new BrickHouseBuilder();
Director brickDirector = new Director(brickHouseBuilder);
brickDirector.ConstructHouse();
House brickHouse = brickDirector.GetHouse();
brickHouse.ShowHouseInfo();
}
}
6、运行效果
通过这两种建造者类的不同实现,我们展示了如何通过建造者模式灵活创建不同类型的复杂对象。
建造者模式允许分步骤创建对象,并且能够复用构建过程生成不同类型的产品。它也提高了代码的可读性和维护性,特别是在对象构建逻辑复杂时。
当对象的构建步骤固定,但最终产品可能有不同的表现方式时,建造者模式尤其适用。在 Unity 中,这种模式可以用于场景中复杂对象(如建筑物、场景)的灵活构建。