[Unity設計模式與遊戲開發]原型模式

前言

原型模式談的最多的就是克隆,談到克隆我們就會想到第一個克隆羊多利,是我們生物工程史上的一次重大突破。克隆又稱作拷貝,記得在做iOS開發的時候,剛接觸OC開發談的比較多一個知識點就是深拷貝和淺拷貝,淺拷貝只是拷貝了變量的內存地址,深拷貝拷貝了變量的內容。提到克隆我們在Unity開發中最常見的API就是 GameObject.Instantiate(),看他們的註釋,Clones the object original and returns the clone,參數就是我們給定的Object然後克隆返回這個對象。爲什麼要有這個克隆方法呢?假設沒有這個克隆方法,也就是說不用原型模式,我們想要實例化10個甚至100個這樣的對象,我們是不是都要重複這些操作,實例化模型的頂點,網格,材質等等,創建實例化一個模型是非常費資源的操作,沒實例化一個我們就需要從0開始,如果提供克隆方法我們就直接將這段內存copy一份然後返回即可,就方便快捷了許多。在這裏拋出一個問題,思考一下Unity的GameObject.Instantiate()是如何實現的?雖然我們看不到C++實現的源代碼,我們可以先猜測一下,等到學完本章節,你能猜到答案嘛?

原型模式存再必要性

在談基本介紹之前,先解釋一下爲什麼要存在這樣一種設計模式,也就是這個設計模式解決了什麼問題。我們在軟件開發或者遊戲開發中,會存在這樣一種情況,我們需要用到多個相同的實例,最簡單的辦法就是通過多次New來創建多個相同的實例,但是這樣存在問題,首先代碼上會有很多行相同或者類似的代碼,這個是程序員最看不得的,其次是上面提到的創建實例比較耗費資源,創建起來步驟比較繁瑣,如果創建大量相同的實例,都是在重複繁瑣的創建過程。原型模式就因此誕生,原型模式就是通過現有的實例來實現克隆完成新對象的創建。

原型模式模式基本介紹

  • 原型模式是指:用原型實例指定創建對象的種類,並且通過拷貝這些原則,創建新的對象。
  • 原型模式是一種創建型設計模式,允許一個對象再創建另外一個可定製的對象,無需知道如何創建的細節。
  • 工作原理是:通過將一個原型對象傳給那個要創建的對象,這個要創建的對象通過請求原型對象拷貝它們自己來實施創建對象,即對象Clone()。
  • 形象理解:孫大聖拔出猴毛,變出好多其他"大聖"。
    在這裏插入圖片描述

淺拷貝基本介紹

  • 對象語句類型是基本數據類型的成員變量,淺拷貝會直接進行值傳遞,也就是將該屬性值直接複製一份給新對象。
  • 對於數據類型是引用類型的成員變量,那麼淺拷貝會進行引用傳遞,也就是隻會講改成員變量的引用值(內存地址)複製一份給新的對象,實際上兩個對象的該成員變量都指向同一個實例,在這種情況下修改一個對象的變量會影響到另一個對象的該成員變量的值。

深拷貝基本介紹

  • 賦值對蝦乾的所有基本數據類型的成員變量值
  • 爲所有引用數據類型的成員白能量申請存儲空間,並賦值每個引用數據類型成員變量所引用的對象,也就是說對象進行深拷貝要對整個對象進行拷貝。
  • 深拷貝的兩種實現方式,1.重寫clone方法實現深拷貝,2.通過對象序列化和反序列化實現深拷貝(推薦)

最常見的深拷貝寫法

如果讓我們寫一個深拷貝,我們可能信手捏來,下面這種寫法也是我們大多數人最常用的寫法。

public class Character
{
    public int Id { get; set; }
    public string Name { get; set; }

    public bool IsAvatar { get; set; }

    public Character DeepClone()
    {
        Character character = new Character();
        character.Id = this.Id;
        character.Name = this.Name;
        character.IsAvatar = this.IsAvatar;
        return character;
    }
}

這種寫法比較好理解,但是也有一個很明顯的弊端,如果某個類字段特別多,誇張的成百上千,那我們難不成就寫成百上千行,如果"勤快"的初級程序還真很有可能寫那麼多字段賦值,如果稍微高級一點的程序員或許會用反射的方式來字段賦值,針對這種字段比較多的類如何更好的實現深拷貝,請看下面的實現方式,也是推按的實現方式。

.NetFramework中用到的原型模式

在.net framework中,童工了ICloneable接口來對對象進行克隆。當然你也可以不去實現ICloneable接口自己直接定義一個Clone()方法,下面我們就來嘗試使用一下這個接口來實現深淺拷貝的例子。

使用ICloneable接口實現克隆

UML設計圖

在這裏插入圖片描述

代碼
[Serializable]
public class Job
{
    public int Id { get; set; }
    public string JobName { get; set; }
    public override string ToString()
    {
        return this.JobName;
    }
}

[Serializable]
public class Person : ICloneable
{
    public int Age { get; set; }    //值類型字段
    public string Name { get; set; }    //字符串
    public Job Job { get; set; }        //引用類型字段
    //深拷貝
    public Person DeepClone()
    {
        using (Stream objectStream = new MemoryStream())
        {
            IFormatter formatter = new BinaryFormatter();
            formatter.Serialize(objectStream, this);
            objectStream.Seek(0, SeekOrigin.Begin);
            return formatter.Deserialize(objectStream) as Person;
        }
    }

    public object Clone()
    {
        return this.MemberwiseClone();//淺拷貝
    }

    //淺拷貝
    public Person ShallowClone()
    {
        return this.Clone() as Person;
    }
}

測試

Person p = new Person() { Name = "P", Age = 21, Job = new Job() { JobName = "Coder", Id = 1001 } };
Person p1 = p.ShallowClone();
Person p2 = p.DeepClone();
string Str = string.Format("修改前:p.Name={0},p.Age={1},p.Job.Id={2},p.Job.JobName={3}", p.Name, p.Age, p.Job.Id, p.Job.JobName);
Debug.Log(Str);
Str = string.Format("修改前:p1.Name={0},p1.Age={1},p.Job.Id={2},p1.Job.JobName={3}", p1.Name, p1.Age, p1.Job.Id, p1.Job.JobName);
Debug.Log(Str);
Str = string.Format("修改前:p2.Name={0},p2.Age={1},p.Job.Id={2},p2.Job.JobName={3}", p2.Name, p2.Age, p2.Job.Id, p2.Job.JobName);
Debug.Log(Str);

//修改P1的值
p1.Name = "PM";
p1.Age = 30;
p1.Job.JobName = "Manager";
p1.Job.Id = 1002;

Str = string.Format("修改後:p.Name={0},p.Age={1},p.Job.Id={2},p.Job.JobName={3}", p.Name, p.Age, p.Job.Id,p.Job.JobName);
Debug.Log(Str);
Str = string.Format("修改後:p1.Name={0},p1.Age={1},p1.Job.Id={2},p1.Job.JobName={3}", p1.Name, p1.Age, p1.Job.Id, p1.Job.JobName);
Debug.Log(Str);
Str = string.Format("修改後:p2.Name={0},p2.Age={1},p2.Job.Id={2},p2.Job.JobName={3}", p2.Name, p2.Age, p2.Job.Id, p2.Job.JobName);
Debug.Log(Str);

運行效果圖:

在這裏插入圖片描述

代碼優化

如果我們項目中有好多需要實現拷貝的Model,那我們是不是每個自定義Model裏面都要繼承一下ICloneable接口實現一下淺拷貝,再實現一下深拷貝,那多麻煩,程序員最重要的一項技能就是優化代碼的能力,只要看到有重複性的代碼,那我們就要想辦法去抽象和優化,因此就抽象成一個通用的克隆基類,在想要有克隆功能的Model只要繼承這個即可,代碼如下:

//通用深拷貝基類
[Serializable]
public class BaseClone<T> : ICloneable where T : new()
{
    //淺拷貝
    public virtual T ShallowClone()
    {
        return (T)this.Clone();
    }

    //深拷貝
    public virtual T DeepClone()
    {
        try
        {
            using (Stream memoryStream = new MemoryStream())
            {
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(memoryStream, this);
                memoryStream.Position = 0;
                return (T)formatter.Deserialize(memoryStream);
            }
        }
        catch (Exception ex)
        {
            Debug.LogError("克隆異常:" + ex.ToString());
        }
        return default(T);
    }

    public object Clone()
    {
        return this.MemberwiseClone();//淺拷貝
    }
}

使用

[Serializable]
public class Person1 : BaseClone<Person1>
{
    public int Age { get; set; }
    public string Name { get; set; }
    public Job Job { get; set; }
}

代碼測試還是跟上面一樣,具體測試代碼可以見給出的案例工程https://github.com/dingxiaowei/UnityDesignPatterns。

結論

從上面論證的結果來看,淺拷貝之後的對象跟原來的對象並不是一個對象,但淺表副本複製了原對象的值類型和string類型,但是非string類型的引用類型是複製了引用,也可以理解爲複製了數據的地址指針。

這裏有一個特別要注意的一個"坑",也是面試官比較喜歡用來考察面試者的一個點,就是在我們通常理解中,引用類型的淺拷貝在修改拷貝之後的對象的值的時候,是會引起原來初始對象的值的,因爲引用類型的淺拷貝只是拷貝了一份引用類型的值的地址指針,但這裏要注意字符串類型是一個特例。字符串類型是一個不可修改的引用類型,也就是說string雖然是引用類型,但淺表副本卻複製了這個值,把它當值類型一樣處理了。

關於MemberwiseCLone()方法,可以看MSDN上詳細的文檔說明,它上面註釋是這樣寫的:MemberwiseClone 方法創建一個淺表副本,方法是創建一個新的對象,然後將當前對象的非靜態字段複製到新的對象。 如果字段是值類型,則執行字段的逐位副本。 如果字段是引用類型,則會複製引用,但不會複製引用的對象;因此,原始對象及其複本引用相同的對象。

無論是淺拷貝還是深拷貝,C#都將源對象中所有字段複製到新的對象中。不過,對於值類型字段,引用類型字段以及字符串類型字段的處理,兩種拷貝方式存在一定的區別,具體看下面的表:

字段 拷貝類型 拷貝操作詳情 副本或源對象中修改是否相互影響
值類型 淺拷貝 字段值被拷貝至副本中
深拷貝 字段被重新創建並賦值
引用類型 淺拷貝 字段引用被拷貝至副本中
深拷貝 字段被重新創建並賦值
字符串 淺拷貝 字段被重新創建並賦值
(看成值類型即可)
深拷貝 字段被重新創建並賦值

解釋GameObject.Instantiate原理

回到我們一開始拋出的問題,關於Unity中如何快速實例化Object的,我們應該知道原理了吧,就是利用深拷貝,
在這裏插入圖片描述
關鍵在於這個CloneObject的拷貝函數,我們找到這個CloneObject的實現
在這裏插入圖片描述
追本溯源,找到克隆GameObject的地方
在這裏插入圖片描述
想要深入學習源碼,可以自己網上找相關資料,聲明僅限於學習使用!
如果對C++指針不太熟悉的,可以看下圖回顧一下,也可以看鏈接&和*的區別
在這裏插入圖片描述

遊戲開發中哪兒用到原型模式

談了這麼多原理和原型模式的介紹,那麼我們在遊戲開發中哪兒要用到原型模式呢?面試官最喜歡問題的問題不單純是讓你介紹一下原型模式,還要結合具體項目來介紹,那時候就會感覺突然蒙了,在遊戲開發中,最常用於屬性、角色和對象克隆,例如塔防和rpg或者設計類遊戲中的小怪都是一樣的屬性就很適合用原型模式來創建。

原型模式注意事項和小結

  • 創建新的對象比較複雜時,可以利用原型模式簡化對象的創建過程,同時也能夠提高效率。
  • 不用重複初始化對象,而是動態獲得對象運行時的狀態。
  • 缺點:需要每個類適配一個克隆方法,一般獲取淺拷貝就是用MemberwishClone()方法來獲取,這對全新的類來說不是很難,但對已有類進行改造時,需要修改其源碼,違背了OCP原則,這點需要注意。

設計模式系列教程彙總

http://dingxiaowei.cn/tags/設計模式/

教程代碼下載

https://github.com/dingxiaowei/UnityDesignPatterns

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章