基础认知:https://blog.csdn.net/RinKas/article/details/52760032
理解参考:https://blog.csdn.net/dingxiaowei2013/article/details/105628310

简单理解一下概念

通过对象的原型克隆而产生的新的对象,以一个武器生成器为例子,有的游戏具有附魔功能,附魔后的武器在原有属性不变的基础上又增加了新的属性,这也是一种原型模式的应用。

基本流程

首先定义一个抽象类作为武器的父类,抽象类武器拥有所有武器的共同属性,同时抽象类武器必须拥有一个Clone的抽象方法,用以生成新的武器,其他的方法可以自行根据需求拓展。

  1. // 定义一个基本武器类型,基本武器内含有子类共有的属性,拥有Clone的抽象方法
  2. public abstract class Sword
  3. {
  4. protected string Name;
  5. protected int Atk;
  6. protected int Speed;
  7. public Sword()
  8. {
  9. Name =_Name;
  10. Atk =_Atk;
  11. Speed =_Speed;
  12. }
  13. public abstract Sword Clone();
  14. public abstract void ShowInfo();
  15. }

然后我们就是定义子类武器了子类武器继承自基类武器且可以自行拓展各种属性,同时子类武器完善Clone方法,根据需求产生新的武器。

  1. public class SacredSword : Sword
  2. {
  3. private int _heal;
  4. public int Heal
  5. {
  6. get { return _heal; }
  7. set { _heal = value; }
  8. }
  9. public override Sword Clone()
  10. {
  11. SacredSword sacredSword = new SacredSword();
  12. sacredSword.Heal = Random.Range(5, 20);
  13. return sacredSword;
  14. }
  15. public override void ShowInfo()
  16. {
  17. Debug.Log("圣神的" + Name + ":" + " 攻击:" + Atk + " 攻击速度:" + Speed + " 每 秒恢复:" + Heal + "点血");
  18. }
  19. }

武器生成器,通过返回Clone方法生成的新武器

  1. public Sword SwordSpawner(Sword sword)
  2. {
  3. return sword.Clone();
  4. }

必要性:为什么需要原型模式

看到这可能会想创建一个新的升级武器类,继承sword后定义新的武器属性和方法,使用时创建新的武器使用不就可以了。
我们在软件开发或者游戏开发中,会存在这样一种情况,我们需要用到多个相同的实例,最简单的办法就是通过多次New来创建多个相同的实例,但是这样存在问题,首先代码上会有很多行相同或者类似的代码,这个是程序员最看不得的,其次是上面提到的创建实例比较耗费资源,创建起来步骤比较繁琐,如果创建大量相同的实例,都是在重复繁琐的创建过程。原型模式就因此诞生,原型模式就是通过现有的实例来实现克隆完成新对象的创建。

再次定义

原型模式是指:用原型实例指定创建对象的种类,并且通过拷贝这些原则,创建新的对象。
原型模式是一种创建型设计模式,允许一个对象再创建另外一个可定制的对象,无需知道如何创建的细节。
工作原理是:通过将一个原型对象传给那个要创建的对象,这个要创建的对象通过请求原型对象拷贝它们自己来实施创建对象,即对象Clone()。

了解一下浅拷贝与深拷贝

对象语句类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值直接复制一份给新对象。
对于数据类型是引用类型的成员变量,那么浅拷贝会进行引用传递,也就是只会讲改成员变量的引用值(内存地址)复制一份给新的对象,实际上两个对象的该成员变量都指向同一个实例,在这种情况下修改一个对象的变量会影响到另一个对象的该成员变量的值。

浅拷贝:

  • 对象语句类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值直接复制一份给新对象。
  • 对于数据类型是引用类型的成员变量,那么浅拷贝会进行引用传递,也就是只会讲改成员变量的引用值(内存地址)复制一份给新的对象,实际上两个对象的该成员变量都指向同一个实例,在这种情况下修改一个对象的变量会影响到另一个对象的该成员变量的值。

    深拷贝:

  • 赋值对虾干的所有基本数据类型的成员变量值

  • 为所有引用数据类型的成员白能量申请存储空间,并赋值每个引用数据类型成员变量所引用的对象,也就是说对象进行深拷贝要对整个对象进行拷贝。
  • 深拷贝的两种实现方式,1.重写clone方法实现深拷贝 2.通过对象序列化和反序列化实现深拷贝(推荐)

    最常见的深拷贝写法

    1. public class Character
    2. {
    3. public int Id { get; set; }
    4. public string Name { get; set; }
    5. public bool IsAvatar { get; set; }
    6. public Character DeepClone()
    7. {
    8. Character character = new Character();
    9. character.Id = this.Id;
    10. character.Name = this.Name;
    11. character.IsAvatar = this.IsAvatar;
    12. return character;
    13. }
    14. }
    这种写法比较好理解,但是也有一个很明显的弊端,如果某个类字段特别多,夸张的成百上千,那我们难不成就写成百上千行,如果”勤快”的初级程序还真很有可能写那么多字段赋值,如果稍微高级一点的程序员或许会用反射的方式来字段赋值,针对这种字段比较多的类如何更好的实现深拷贝,可以看:利用 ICloneable接口来对对象进行克隆 https://blog.csdn.net/dingxiaowei2013/article/details/105628310

    结论

    从上面论证的结果来看,浅拷贝之后的对象跟原来的对象并不是一个对象,但浅表副本复制了原对象的值类型和string类型,但是非string类型的引用类型是复制了引用,也可以理解为复制了数据的地址指针。
    这里有一个特别要注意的点,也是面试官比较喜欢用来考察面试者的一个点,就是在我们通常理解中,引用类型的浅拷贝在修改拷贝之后的对象的值的时候,是会影响原来初始对象的值的,因为引用类型的浅拷贝只是拷贝了一份引用类型的值的地址指针,但这里要注意字符串类型是一个特例。字符串类型是一个不可修改的引用类型,也就是说string虽然是引用类型,但浅表副本却复制了这个值,把它当值类型一样处理了。

    Unity中的原型模式

    在游戏开发中,最常用于属性、角色和对象克隆,例如塔防和rpg或者设计类游戏中的小怪都是一样的属性就很适合用原型模式来创建。 (动态获得对象运行时的状态)

    需求描述

    在回合制RPG中,创建的角色会有各种各样的属性,最简单的比如血量、物理攻击、魔法攻击、行动速度等等,但不同的模型角色都有不同的模板属性,这些属性需要我们配置,我们可以配置在Excel中,然后导成文本数据供程序实例化使用,也有的项目是通过Unity给我们提供的ScriptableObject文件来配置角色的初始属性值。
    关于ScriptableObject就是unity给我们提供的一种Unity能识别的asset格式的文件,自定义这样的文件只需要继承ScriptableObject类即可,然后就可以鼠标右击创建这样的asset配置文件,用简单的案例来演示,代码如下: ```csharp using System; using UnityEngine;

[System.Serializable] public class Attributes : ICloneable { [Tooltip(“角色血量”)] public int hp; [Tooltip(“物理攻击”)] public int pAtk; [Tooltip(“物理防御”)] public int pDef; [Tooltip(“魔法攻击”)] public int mAtk; [Tooltip(“魔法防御”)] public int mDef; [Tooltip(“行动速度”)] public int spd;

  1. public object Clone()
  2. {
  3. return this.MemberwiseClone();
  4. }
  5. public Person ShallowClone()
  6. {
  7. return this.Clone() as Person;
  8. }
  9. public Attributes DeepClone()
  10. {
  11. Attributes result = new Attributes();
  12. result.hp = hp;
  13. result.pAtk = pAtk;
  14. result.pDef = pDef;
  15. result.mAtk = mAtk;
  16. result.mDef = mDef;
  17. result.spd = spd;
  18. return result;
  19. }

}

[CreateAssetMenu(fileName = “CharacterItem”, menuName = “(ScriptableObject)CharacterItem”)] public class CharacterItem : ScriptableObject { public string Name; public string Desc; [Tooltip(“角色属性”)] public Attributes Attributes; }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/23060831/1651713279474-bbaf26ca-2318-40a1-b84c-0b69ea6bb328.png#clientId=u22933d6b-f9fa-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=236&id=u05aaa1a0&margin=%5Bobject%20Object%5D&name=image.png&originHeight=366&originWidth=479&originalType=url&ratio=1&rotation=0&showTitle=false&size=33515&status=done&style=none&taskId=ub2b075b3-4f0d-4aed-8efa-fc00cb3c3e5&title=&width=309)<br />由于这里字段比较少,所以这里深拷贝就用了最常见的变量赋值的方式,那么我们有了模型属性数据,怎么根据这个配置数据来实例化两个"漩涡鸣人"游戏对象呢,我们不能游戏中一个对象我们就要对应一个asset配置文件吧,如果两个对象一模一样,只需要用一个asset文件即可,但我们需要copy两份这样的属性数据,所以在写了内置的Clone方法来实现,这也是原型模式在游戏开发中的典型应用。
  2. <a name="j9KtG"></a>
  3. ### 根据数据模板创建不同的角色对象
  4. ```csharp
  5. using System.Collections;
  6. using System.Collections.Generic;
  7. using UnityEngine;
  8. public class CharacterEntity
  9. {
  10. public int id;
  11. public Attributes objectAttributes;
  12. }
  13. public class PropertyPatternExample2 : MonoBehaviour
  14. {
  15. public CharacterItem CharDataModel;
  16. void Start()
  17. {
  18. CharacterEntity char1 = new CharacterEntity() { id = 0, objectAttributes = CharDataModel.Attributes.DeepClone() };
  19. CharacterEntity char2 = new CharacterEntity() { id = 1, objectAttributes = CharDataModel.Attributes.DeepClone() };
  20. Debug.Log("修改之前两个角色的属性:");
  21. Debug.Log("char1 property:" + char1.objectAttributes.ToString());
  22. Debug.Log("char2 property:" + char2.objectAttributes.ToString());
  23. Debug.Log("修改之后两个角色的属性");
  24. char1.objectAttributes.hp = 110;
  25. Debug.Log("char1 property:" + char1.objectAttributes.ToString());
  26. Debug.Log("char2 property:" + char2.objectAttributes.ToString());
  27. }
  28. }

image.png
上面结果会发现我们根据一个数据模板实例化了两个不同的角色对象,每个对象的属性都是相互独立的,互不影响。

原型模式优缺点

创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率。 例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
不用重复初始化对象,而是动态获得对象运行时的状态。
缺点: 需要每个类适配一个克隆方法,一般获取浅拷贝就是用MemberwishClone()方法来获取,这对全新的类来说不是很难,但对已有类进行改造时,需要修改其源码,违背了OCP原则,这点需要注意。