https://github.com/sttsai/PBaseDefense_Unity3Dgit
第1篇 設計模式與遊戲設計github
第2章 遊戲範例說明數據庫
第2篇 基礎系統設計模式
第3章 遊戲場景的轉換----狀態模式(State) (已看)安全
第4章 遊戲主要類----外觀模式(Facade) (已看)服務器
第5章 獲取遊戲服務的惟一對象----單例模式(Singleton) (已看)網絡
第6章 遊戲內各系統的整合----中介者模式(Mediator) (已看)數據結構
第7章 遊戲的主循環----Game Loop (已看)架構
第3篇 角色的設計
第8章 角色系統的設計分析 (已看)
第9章 角色與武器的實現----橋接模式(Bridge) (已看)
第10章 角色的屬性----策略模式(Strategy) (已看)
第11章 攻擊特效與擊中反應----模板方法模式(Template Method) (已看)
第13章 角色系統 (已看)
第4篇 角色的產生
第14章 遊戲角色的產生----工廠方法模式(Factory Method) (已看)
第15章 角色的組裝----建造者模式(Builder) (已看)
第16章 遊戲屬性管理功能----享元模式(Flyweight) (已看)
第5篇 戰爭開始
第17章 Unity3D的界面設計----組合模式(Composite)
第19章 兵營訓練單位----命令模式(Command) (已看)
第20章 關卡設計----責任鏈模式(Chain of Responsibility) (已看)
第6篇 輔助系統
第21章 成就係統----觀察者模式(Observer) (已看)
第7篇 調整與優化
第8篇 未明確使用的模式
第27章 迭代器模式(Iterator), 原型模式(Prototype) 和解釋器模式(Interpreter)
第1章 遊戲實現中的設計模式
1.1 設計模式的起源
1.2 軟件的設計模式是什麼?
1.3 面向對象設計中常見的設計原則
1.4 爲何要學習設計模式
1.5 遊戲程序設計與設計模式
1.6 模式的應用與學習方式
1.7 結論
第2章 遊戲範例說明
2.1 遊戲範例
2.2 GoF的設計模式範例
第3章 遊戲場景的轉換----狀態模式(State)
3.1 遊戲場景
Unity3D是使用場景(Scene)做爲遊戲運行時的環境.開始製做遊戲時,開發者會將遊戲須要的素材(3D模型,遊戲對象)放到一個場景中,而後編寫對應的程序代碼,以後只要單擊Play按鈕,就能夠開始運行遊戲
筆者過去開發遊戲時使用的遊戲引擎(Game Engine)或開發框架(SDK, Framework),多數也都存在"場景"的概念,例如
早期Java Phone的J2ME開發SDK中使用的Canvas類
Android的Java開發SDK中使用的Activity類
iOS上2D遊戲開發工具Cocos2D中使用的CCScene類
雖然各類工具不見得都使用場景(Scene)這個名詞,但在實際上,同樣可以使用相同的方式來呈現.而上面所列的各個類,均可以被拿來做爲遊戲實現中"場景"轉換的目標
3.1.1 場景的轉換
切分場景的好處
將遊戲中不一樣的功能分類在不一樣的場景中來執行,除了能夠將遊戲功能執行時須要的環境明確分類以外,"重複使用"也是使用場景轉換的好處之一
本書範例場景的規劃
3.1.2 遊戲場景可能的實現方式
Easy Way
public class SceneManager { private string m_state = "開始"; // 改換場景 public void ChangeScene(string StateName) { m_state = StateName; switch (m_state) { case "菜單": Application.LoadLevel("MainMenuScene"); break; case "主場景": Application.LoadLevel("GameScene"); break; } } // 更新 public void Update() { switch (m_state) { case "開始": // ... break; case "菜單": // ... break; case "主場景": // ... break; } } }
缺點
只要增長一個狀態,則全部switch(m_state)的程序代碼都須要增長對應的程序代碼
與每個狀態有關的對象, 都必須在SceneManager類中被保留,當這些對象被多個狀態共享時,可能會產生混淆,不太容易識別是由哪一個狀態設置的,形成遊戲程序調試上的困難
每個狀態可能使用不一樣的類對象,容易形成StageManager類過分依賴其餘類,讓SceneManager類不容易移植到其餘項目中
爲了不出現上述缺點,修正的目標會但願使用一個"場景類"來負責維護一個場景,讓與此場景相關的程序代碼和對象能整合在一塊兒.這個負責維護的"場景類",其主要工做以下:
場景初始化
場景結束後,負責清除資源
定時更新遊戲邏輯單元
轉換到其餘場景
其餘與該場景有關的遊戲實現
因爲在範例程序中咱們規劃了3個場景,因此會產生對應的3個"場景類",但如何讓這3個"場景類"相互合做,彼此轉換呢?咱們能夠使用GoF的狀態模式(State)來解決這些問題
3.2 狀態模式(State)
3.2.1 狀態模式(State)的定義
狀態模式(State), 在GoF中的解釋是:
"讓一個對象的行爲隨着內部狀態的改變而變化,而該對象也像是換了類同樣"
3.2.2 狀態模式(State)的說明
參與者的說明以下:
Context(狀態擁有者)
是一個具備"狀態"屬性的類, 能夠制定相關的接口, 讓外界可以得知狀態的改變或經過操做讓狀態改變
有狀態屬性的類, 例如: 遊戲角色有潛行, 攻擊, 施法等狀態; 好友上線, 脫機, 忙碌等狀態; GoF使用TCP聯網爲例, 有已鏈接, 等待鏈接, 斷線等狀態. 這些類中會有一個ConcreteState[X]子類的對象爲其成員, 用來表明當前的狀態
State(狀態接口類)
制定狀態的接口, 負責規範Context(狀態擁有者)在特定狀態下要表現的行爲
ConcreteState(具體狀態的類)
繼承自State(狀態接口類)
實現Context(狀態擁有者)在特定狀態下該有行爲.例如, 實現角色在潛行狀態時該有的行動變緩, 3D模型變半透明, 不能被敵方角色察覺等行爲
3.2.3 狀態模式(State)的實現範例
public class Context { State m_state = null; public void Request(int Value) { m_State.Handle(Value); } public void SetState(State theState) { Debug.Log("Context.SetState:" + theState); m_state = theState; } } public abstract class State { protected Context m_Context = null; public State(Context theContext) { m_Context = theContext; } public abstract void Handle(int Value); } public class ConcreteStateA: State { public ConcreteStateA(Context theContext): base(theContext) { } public override void Handle(int Value) { Debug.Log("ConcreteStateA.Handle"); if (Value > 10) { m_Context.SetState(new ConcreteStateB(m_Context)); } } } public class ConcreteStateB: State { public ConcreteStateB(Context theContext): base(theContext) { } public override void Handle(int Value) { Debug.Log("ConcreteStateB.Handle"); if (Value > 20) { m_Context.SetState(new ConcreteStateC(m_Context)); } } } public class ConcreteStateC: State { public ConcreteStateC(Context theContext): base(theContext) { } public override void Handle(int Value) { Debug.Log("ConcreteStateC.Handle"); if (Value > 30) { m_Context.SetState(new ConcreteStateA(m_Context)); } } } void UnitTest() { Context theContext = new Context(); theContext.SetState(new ConcreteStateA(theContext)); theContext.Request(5); theContext.Request(15); theContext.Request(25); theContext.Request(35); } Context.SetState:DesignPattern_State.ConcreteStateA ConcreteStateA.Handle ConcreteStateA.Handle Context.SetState:DesignPattern_State.ConcreteStateB Context.SetState:DesignPattern_State.ConcreteStateC Context.SetState:DesignPattern_State.ConcreteStateA
Context類中提供了一個SetState方法,讓外界可以設置Context對象當前的狀態,而所謂的"外界", 也能夠是由另外一個State狀態來調用.因此實現上,狀態的轉換能夠有下列兩種方式:
交由Context類自己,按條件在各狀態之間轉換
產生Context類對象時, 立刻指定初始狀態給Context對象,而在後續執行過程當中的狀態轉換則交由State對象負責,Context對象再也不介入
筆者在實現時,大部分狀況下會選擇第2種方式,緣由在於:
狀態對象自己比較清楚"在什麼條件下, 可讓Context對象轉移到下一個State狀態".因此在每一個ConcreteState類的程序代碼中,能夠看到"狀態轉換條件"的判斷, 以及設置哪個ConcreteState對象成爲新的狀態
每一個ConcreteState狀態均可以保持本身的屬性值,做爲狀態轉換或展示狀態行爲的依據,不會與其餘的ConcreteState狀態混用,在維護時比較容易理解
由於判斷條件及狀態屬性都被轉換到ConcreteState類中,故而可縮減Context類的大小
3.3 使用狀態模式(State)實現遊戲場景的轉換
3.3.1 SceneState的實現
參與者以下:
ISceneState: 場景類的接口, 定義《P級陣地》種場景轉換和執行時須要調用的方法
StartState, MainMenuState, BattleState: 分別對應範例中的開始場景(StartScene), 主畫面場景(MainMenuScene)及戰鬥場景(BattleScene),做爲這些場景執行時的操做類
SceneStateController: 場景狀態的擁有者(Context), 保持當前遊戲場景狀態, 並做爲與GameLoop類互動的接口. 除此以外, 也是執行"Unity3D場景轉換"的地方
GameLoop:: 遊戲主循環類做爲Unity3D與《P級陣地》的互動接口,包含了初始化遊戲和按期調用更新操做
3.3.2 實現說明
public class ISceneState { private string m_StateName = "ISceneState"; public string StateName { get { return m_StateName; } set { m_StateName = value; } } protected SceneStateController m_Controller = null; public ISceneState(SceneStateController Controller) { m_Controller = Controller; } public virtual void StateBegin() { } public virtual void StateEnd() { } public virtual void StateUpdate() { } public override string ToString() { return string.Format("[I_SceneState: StateName = {0}]", StateName); } } public class StartScene: ISceneState { public StartScene(SceneStateController Controller): base(Controller) { this.StateName = "StartState"; } public override void StateBegin() { } public override void StateUpdate() { m_Controller.SetState(new MainMenuState(m_Controller), "MainMenuScene"); } } public class MainMenuState: ISceneState { public MainMenuState(SceneStateController Controller): base(Controller) { this.StateName = "MainMenuState"; } public override void StateBegin() { Button tmpBtn = UITool.GetUIComponent<Button>("StartGameBtn"); if (tempBtn != null) { tmpBtn.onClick.AddListener(()=>OnStartGameBtnClick(tmpBtn)); } public void OnStartGameClick(Button theButton) { m_Controller.SetState(new BattleState(m_Controller), "BattleScene"); } } public class BattleScene: ISceneState { public BattleScene(SceneStateController Controller): base(Controller) { this.StateName = "BattleState"; } public overrride void StateBegin() { PBaseDefenseGame.Instance.Initial(); } public ovrride void StateEnd() { PBaseDefenseGame.Instance.Update(); } public ovrride void StateUpdate() { InputProcess(); PBaseDefenseGame.Instance.Update(); if (PBaseDefenseGame.Instance.ThisGameIsOver()) { m_Controller.SetState(new MainMenuState(m_Controller), "MainMenuScene"); } } private void InputProcess() { //... } } public class SceneStateController { private ISceneState m_State; private bool m_bRunBegin = false; public SceneStateController() { } public void SetState(ISceneState State, string LoadSceneName) { m_bRunBegin = false; LoadScene(LoadSceneName); if (m_State != null) { m_State.StateEnd(); } m_State = State; } private void LoadScene(string LoadSceneName) { if (LoadSceneName == null || LoadSceneName.Length == 0) { return; } Application.LoadLevel(LoadSceneName); } public void StateUpdate() { if (Application.isLoadingLevel) { return; } if (m_state != null && m_bRunBegin == false) { m_State.StateBegin(); m_bRunBegin = true; } if (m_State != null) { m_State.StateUpdate(); } } } public class GameLoop: MonoBehavior { SceneStateController m_SceneStateController = new SceneStateController(); void Awake() { GameObject.DontDestroyOnLoad(this.gameObject); UnityEngine.Random.seed = (int)DateTime.Now.Ticks; } void Start() { m_SceneStateController.SetState(new StartState(m_SceneStateController), ""); } void Update() { m_SceneStateController.StateUpdate(); } }
3.3.3 使用狀態模式(State)的優勢
使用狀態模式(State)來實現遊戲場景轉換,有下列優勢:
減小錯誤的發生並下降維護難度
再也不使用switch(m_state)來判斷當前的狀態,這樣能夠減小新增遊戲狀態時,因未能檢查到全部switch(m_state)程序代碼而形成的錯誤
狀態執行環境單一化
與每個狀態有關的對象及操做都被實如今一個場景狀態類下,對程序設計師來講,這樣能夠清楚地瞭解每個狀態執行時所須要的對象及配合的類
項目之間能夠共享場景
3.3.4 遊戲執行流程及場景轉換說明
3.4 狀態模式(State)面對變化時
隨着項目開發進度進入中後期,遊戲企劃可能會提出新的系統功能來增長遊戲內容.這些提案多是增長小遊戲關卡,提供查看角色信息圖鑑,玩家排行等功能.當程序人員在分析這些新增的系統需求後,若是以爲沒法在現有的場景(Scene)下實現,就必須使用新的場景來完成.而在現有的架構下,程序人員只須要完成下列幾項工做:
在Unity3D編輯模式下新增場景
加入一個新的場景狀態類對應到新的場景,並在其中實現相關功能
決定要從哪一個現有場景轉換到新的場景
決定新的場景結束後要轉換到哪個場景
上述流程,就程序代碼的修改而言,只會新增一個程序文件(.cs)用來實現新的場景狀態類,並修改一個現有的遊戲狀態,讓遊戲能按照需求轉換到新的場景狀態.除此以外,不須要修改其餘任何的程序代碼
3.5 結論
在本章中,咱們利用狀態模式(State)實現了遊戲場景的切換,這種作法並不是全然都是優勢,但與傳統的switch(state_code)相比,已經算是更好的設計.
狀態模式(State)的優缺點
使用狀態模式(State)能夠清楚地瞭解某個場景狀態執行時所須要配合使用的類對象,而且減小因新增狀態而須要大量修改現有程序代碼的維護成本
《P級陣地》只規劃了3個場景來完成整個遊戲,算是"產出較少狀態類"的應用.但若是狀態模式(State)是應用在大量狀態的系統時,就會遇到"產生過多狀態類"的狀況,此時會伴隨着類爆炸的問題,這算是一個缺點.不過與傳統使用switch(state_code)的實現方式相比,使用狀態模式(State)對於項目後續的長期維護效益上,仍然具備優點
與其餘模式(Pattern)的合做
在《P級陣地》的BattleScene類實現中,分別調用了PBaseDefenseGame類的不一樣方法,此時的PBaseDefenseGame使用的是"單例模式(Singleton)",這是一種讓BattleScene類方法中的程序代碼,能夠取得惟一對象的方式.而PBaseDefenseGame也使用了"外觀模式(Facade)"來整合PBaseDefenseGame內部的複雜系統,所以BattleScene類沒必要了解太多關於PBaseDefenseGame內部的實現方式.
狀態模式(State)的其餘應用方式:
角色AI: 使用狀態模式(State)來控制角色在不一樣狀態下的AI行爲
遊戲服務器連線狀態: 網絡遊戲的客戶端,須要處理與遊戲服務器的連線狀態,通常包含開始連線,連線中,斷線等狀態,而在不一樣的狀態下,會有不一樣的封包信息處理方式,須要分別實現
關卡進行狀態: 若是是通關型遊戲,進入關卡時一般會分紅不一樣的階段,包含加載數據,顯示關卡信息,倒數通知開始,關卡進行,關卡結束和分數計算,這些不一樣的階段能夠使用不一樣的狀態類來負責實現
第4章 遊戲主要類----外觀模式(Facade)
4.1 遊戲子功能的整合
一款遊戲要能順利運行,必須同時由內部數個不一樣的子系統一塊兒合做完成.在這些子系統中,有些是在早期遊戲分析時規劃出來的,有些則是實現過程當中,將相同功能重構整合以後才完成的.以《P級陣地》爲例,它是由下列遊戲系統所組成:
遊戲事件系統(GameEventSystem)
兵營系統(CampSystem)
關卡系統(StageSystem)
角色管理系統(CharacterSystem)
行動力系統(APSystem)
成就係統(AchivementSystem)
這些系統在遊戲運行時會彼此使用對方的功能,而且通知相關信息或傳送玩家的指令.另外,有些子系統必須在遊戲開始運行前,按照必定的步驟將它們初始化並設置參數,或者遊戲在完成一個關卡時,也要按照必定的流程替它們釋放資源
能夠理解的是, 上面這些子系統的溝通及初始化過程都發生在"內部"會比較恰當,由於對於外界或客戶端來講,大可沒必要去了解它們之間的相關運行過程.若是客戶端了解太多系統內部的溝通方式及流程,那麼對於客戶端來講,就必須與每個遊戲系統綁定,而且調用每個遊戲系統的功能.這樣的作法對於客戶端來講並非一件好事,由於客戶端可能只是單純地想使用某一項遊戲功能而已,但它卻必須通過一連串的子系統調用以後才能使用,對於客戶端來講,壓力太大,而且讓客戶端與每一個子系統都產生了依賴性,增長了遊戲系統與客戶端的耦合度
若是要在咱們的遊戲範例中舉一個例子,那麼上一章所提到的"戰鬥狀態類(BattleScene)"就是一個必須使用到的遊戲系統功能的客戶端
根據上一章的說明,戰鬥狀態類(BattleState)主要負責遊戲戰鬥的運行,而《P級陣地》在進行一場戰鬥時,須要大部分的子系統一塊兒合做完成.在實現時,能夠先把這些子系統及相關的執行流程全都放在BattleState類之中一塊兒完成
public class BattleState: ISceneState { private GameEventSystem m_GameEventSystem = null; private CampSystem m_CampSystem = null; private StageSystem m_StageSystem = null; private CharacterSystem m_CharacterSystem = null; private APSystem m_ApSystem = null; private AchivementSystem m_AchievementSystem =null; public GameState(SceneStateController Controller): base(Controller) { this.StateName = "GameState"; InitGameSystem(); } private void InitGameSystem() { m_GameEventySystem = new GameEventSystem(); ... } private void UpdateGameSystem() { m_GameEventSystem.Update(); ... } }
雖然這樣的實現方式很簡單,但就如本章一開始所說明的,讓戰鬥狀態類(BattleState)整個客戶端去負責調用全部與遊戲玩法相關的系統功能,是很差的實現方式,緣由是:
從讓事情單一化(單一職責原則)這一點來看,BattleScene類負責的是遊戲在"戰鬥狀態"下的功能執行及狀態切換,因此不該該負責遊戲子系統的初始化,執行操做及相關的整合工做
以"可重用性"來看,這種設計方式會使得BattleState類不容易轉換給其餘項目使用,由於BattleState類與太多特定的子系統類產生關聯,必須將它們刪除才能轉換給其餘項目,所以喪失可重用性
綜合上述兩個緣由,將這些子系統從BattleState類中移出,整合在單一類之下,會是比較好的作法.因此,在《P級陣地》中應用了外觀模式(Facade)來整合這些子系統,使它們成爲單一界面並提供外界使用
4.2 外觀模式(Facade)
其實,外觀模式(Facade)是在生活中最容易碰到的模式.當咱們可以利用簡單的行爲來操做一個複雜的系統時,當下所使用的接口,就是之外觀模式(Facade)來定義的高級接口
4.2.1 外觀模式(Facade)的定義
外觀模式(Facade)在GoF的解釋是:
"爲子系統定義一組統一的接口,這個高級的接口會讓子系統更容易被使用"
以駕駛汽車爲例,當駕駛者可以開着一輛汽車在路上行走,汽車內部還必須由許多的子系統一塊兒配合才能完成汽車行走這項功能,這些子系統包含引擎系統,傳動系統,懸吊系統,車身骨架系統,電裝系統等.但對於客戶端(駕駛者)而言,並不須要瞭解這些子系統是如何協調工做的,駕駛者只須要經過高級接口(方向盤,踏盤,儀表盤)就能夠輕易操控汽車
以微波爐爲例,微波爐內部包含了電源供應系統,微博加熱系統,冷卻系統,外裝防禦等.當咱們想要使用微波爐加熱食物時,只須要使用微波爐上面的面板調整火力和時間,按下啓動鍵後,微波爐的子系統就會當即交互合做將食物加熱
因此,外觀模式(Facade)的重點在於,它能將系統內部的互動細節隱藏起來,並提供一個簡單方便的接口.以後客戶端只須要經過這個接口,就能夠操做一個複雜系統並讓它們順利運行
4.2.2 外觀模式(Facade)的說明
參與者的說明以下:
client(客戶端,用戶)
從本來須要操做多個子系統的狀況,改成只須要面對一個整合後的界面
subSystem(子系統)
本來會由不一樣的客戶端(非同一系統相關)來操做,改成只會由內部系統之間交互使用
Facade(統一對外的界面)
整合全部子系統的接口及功能,並提供高級界面(或接口)供客戶端使用
接收客戶端的信息後,將信息傳送給負責的子系統
4.2.3 外觀模式(Facade)的實現說明
從以前提到的一些實例來看,駕駛座位前的方向盤,儀表板,以及微波爐上的面板,都是製造商提供給用戶使用的Facade界面
4.3 使用外觀模式(Facade)實現遊戲主程序
4.3.1 遊戲主程序架構設計
PBaseGameDefenseGame就是"整合全部子系統,並提供高級界面的外觀模式類"
參與者說明以下:
GameEventSystem, CampSystem......: 分別爲遊戲的子系統,每一個系統負責各自應該實現的功能並提供接口
PBaseDefenseGame: 包含了和遊戲相關的子系統對象, 並提供了界面讓客戶端使用
BattleScene: 戰鬥狀態類, 便是《P級陣地》中與PBaseDefenseGame互動的客戶端之一
4.3.2 實現說明
PBaseDefenseGame.cs public class PBaeDefenseGame { ... private GameEventSystem m_GameEventSystem =null; ... } public void Initinal() { ... m_GameEventSystem = new GameEventSystem(this); ... } public void Update() { ... m_GameEventSystem.Update(); ... } BattleState.cs public class BattleState: ISceneState { public override void StateBegin() { PBaseDefenseGame.Instance.Initinal(); } public override void StateEnd() { PBaseDefenseGame.Instance.Release(); } public override void StateUpdate() { ... PBaseDefenseGame.Instance.Update(); ... if (PBaseDefenseGame.Instance.ThisGameIsOver()) { m_Controller.SetState(new MainMenuState(m_Controller), "MainMenuState"); } } }
4.3.3 使用外觀模式(Facade)的優勢
將遊戲相關的系統整合在一個類下,並提供單一操做界面供客戶端使用,與當初全部功能都直接實如今BattleScene類中的方式相比,具備如下幾項優勢
使用外觀模式(Facade)可將戰鬥狀態類BattleState單一化,讓該類只負責遊戲在"戰鬥狀態"下的功能執行及狀態切換,不用負責串接各個遊戲系統的初始化和功能調用
使用外觀模式(Facade)使得戰鬥狀態類BattleScene減小了沒必要要的類引用及功能整合,所以增長了BattleState類被重複使用的機會
除了上述優勢以外,外觀模式(Facade)若是應用得當,還具備下列優勢:
節省時間
Unity3D自己提供了很多系統的Facade接口,例如物理引擎,渲染系統,動做系統,粒子系統等.
易於分工開發
對於一個既龐大又複雜的子系統而言,若應用外觀模式(Facade), 便可成爲另外一個Facade接口.因此,在工做的分工配合上,開發者只須要了解對方負責系統的Facade接口類,沒必要深刻了解其中的運行方式
增長系統的安全性
隔離客戶端對子系統的接觸,除了能減小耦合度以外,安全性也是重點之一
4.3.4 實現外觀模式(Facade)時的注意事項
因爲將全部子系統集中在Facade接口類中,最終會致使Facade接口類過於龐大且難以維護,當發生這種狀況時,能夠重構Facade接口類,將功能相近的子系統進行整合,以減小內部系統的依賴性,或是整合其餘設計模式來減小Facade接口類過分膨脹
4.4 外觀模式(Facade)面對變化時
隨着開發需求的變動,任何遊戲子系統的修改及更換,都被限制在PBaseDefenseGame這個Facade接口類內.因此, 當有新的系統須要增長時,也只會影響PBaseDefenseGame類的定義及增長對外開放的方法,這樣就能使項目的變更範圍減到最小
4.5 結論
將複雜的子系統溝通交給單一的一個類負責,並提供單一界面給客戶端使用,使客戶減小對系統的耦合度是外觀模式(Facade)的優勢.
與其餘模式(Pattern)的合做
在《P級陣地》中,PBaseDefenseGame類使用單例模式(Singleton)來產生惟一的類對象,內部子系統之間則使用中介者模式(Mediator)做爲互相溝通的方式,而遊戲事件系統(GameEventSystem)是觀察者(Observer)的實現,主要的目的就是減小PBaseDefenseGame類接口過於龐大而加入的設計
其餘應用方式
網絡引擎: 網絡通訊是一項複雜的工做,一般包含連線管理系統,信息事件系統,網絡數據封包管理系統等, 因此通常會用外觀模式(Facade)將上述子系統整合爲一個系統
數據庫引擎: 在遊戲服務器的實現中,能夠將與"關係數據庫"(MySQL, MSSQL等)相關的操做, 以一種較爲高級的接口隔離, 這個接口能夠將數據庫系統中所需的連線,數據表修改,新增,刪除,更新,查詢等的操做加以封裝,讓不是很瞭解關係數據庫原理的設計人員也能使用
第5章 獲取遊戲服務的惟一對象----單例模式(Singleton)
5.1 遊戲實現中的惟一對象
生活中的許多物品都是惟一的,地球是惟一的,太陽是惟一的等.軟件設計上也會有惟一的對象的的需求,例如:服務器端的程序只能鏈接到一個數據庫,只能有一個日誌產生器,有些世界也是同樣的,同時間只能有一個關卡正在進行,只能連線到一臺遊戲服務器,只能同時操做一個橘色等
5.2 單例模式(Singleton)
5.2.1 單例模式(Singleton)的定義
單例模式(Singleton)在GoF中的定義是:
"確認類只有一個對象,並提供一個全局的方法來獲取這個對象"
單例模式(Singleton)在實現時,須要程序設計語言的支持.只要具備靜態類屬性,靜態類方法和從新定義類建造者存取層級.3項語句功能的程序設計語言,就能夠實現出單例模式(Singleton)
5.2.2 單例模式(Singleton)的說明
參與者以下:
能產生惟一對象的類,而且提供"全局方法"讓外界能夠方便獲取惟一的對象
一般會把惟一的類對象設置爲"靜態類屬性"
習慣上會使用Instance做爲全局靜態方法的名稱,經過這個靜態函數可能獲取"靜態類屬性"
5.2.3 單例模式(Singleton)的實現範例
public class Singleton { public string Name { get; set; } private static Singleton _instance; public static Singleton Instance { get { if (_instance == null) { Debug.Log("產生Singleton"); _instance = new Singleton(); } return _instance; } } private Single() { } } void UnitTest() { Singleton.Instance.Name = "Hello"; Singleton.Instance.Name = "World"; Debug.Log(Singleton.Instance.Name); }
5.3 使用單例模式(Singleton)獲取惟一的遊戲服務對象
5.3.1 遊戲服務類的單例模式實現
在《P級陣地》中,由於PBaseDefenseGame類包含了遊戲大部分的功能和操做,所以但願只產生一個對象,並提供方便的方法來取用PBaseDefenseGame功能,因此將該類運用單例模式
參與者說明:
PBaseDefenseGame
遊戲主程序,內部包含了類型爲PBaseDefenseGame的靜態成員屬性_instance,做爲該類惟一的對象
提供使用C# getter實現的靜態成員方法Instance,用它來獲取惟一的靜態成員屬性_instance
BattleScene
PBaseDefenseGame類的客戶端,使用PBaseDefenseGame.Instance來獲取惟一的對象
5.3.2 實現說明
PBaseDefenseGame.cs public class PBaseDefenseGame { private static PBaseDefenseGame _instance; public static PBaseDefenseGame Instance { get { if (_instance == null) { _instance = new PBaseDefenseGame(); } return _instance; } } ... private PBaseDefenseGame() { } } BattleState.cs public class BattleScene: ISceneState { ... pubic override void StateBegin() { PBaseDefenseGame.Instance.Initinal(); } ... } SoldierClickScript.cs public class SoldierOnClick: MonoBehavior { ... public void OnClick() { PBaseDefenseGame.Instance.ShowSoldierInfo(Solder); } }
5.3.3 使用單例模式(Singleton)後的比較
5.3.4 反對使用單例模式(Singleton)的緣由
5.4 少用單例模式(Singleton)時如何方便地引用到單一對象
讓類具備技術功能來限制對象數量
ClassWithCounter.cs public class ClassWithCounter { protected static int m_ObjCounter = 0; protected bool m_bEnable = false; public ClassWithCounter() { m_ObjCounter++; m_bEnable = (m_ObjCounter == 1) ? true : false; if (m_bEnalbe == false) { Debug.LogError("當前對象數[" + m_ObjCounter + "]超過1個!!"); } public void Operator() { if (m_bEnable == false) { return; } Debug.Log("能夠執行"); } } SingletonTest.cs void UnitTest_ClassWithCounter() { ClassWithCounter pObj = new ClassWithCounter(); pObj1.Operator(); ClassWithCounter pObj2 = new ClassWithCounter(); pObj2.Operator(); pObj1.Operator(); }
設置成爲類的引用,讓對象能夠被取用
某個類的功能被大量使用時,能夠將這個類對象設置爲其餘類中的成員,方便直接引用這些類.而這種實現方法是"依賴性注入"的方式之一,可讓被引用的對象沒必要經過參數傳遞的方式,就能被類的其餘方法引用.按照設置的方式又能夠分爲"分別設置"和"指定類靜態成員"兩種
分別設置
在《P級陣地》中,PBaseDefenseGame是最常被引用的.雖然已經運用了單例模式(Singleton),但筆者仍是以此來示範如何經過設置它成爲其餘類引用的方式,來減小對單例模式的使用
public class PBaseDefenseGame { public void Initinal() { m_GameEventSystem = new GameEventSystem(this); } } public abstract class IGameSystem { protected PBaseDefenseGame m_PBDGame = null; public IGameSystem(PBaseDefenseGame PBDGame) { m_PBDGame = PBDGame; } } public class CampSystem: IGameSystem { public CampSystem(PBaseDefenseGame PBDGame): base(PBDGame) { Initialize(); } public void ShowCaptiveCamp() { m_PBDGame.ShowGameMsg("得到俘兵營"); } }
指定類的靜態成員
A類的功能若須要使用到B類的方法,而且A類在產生其對象時具備下列幾種狀況:
1. 產生對象的位置不肯定
2. 有多個地方能夠產生對象
3. 生成的位置沒法引用到
4. 有衆多子類
當知足上述狀況之一時,能夠直接將B類對象設置爲A類中的"靜態成員屬性", 讓該類的對象均可以直接使用
// PBaseDefenseGame.cs public class PBaseDefenseGame { public void Initinal() { m_StageSystem = new StageSystem(this); // 注入其餘系統 EnemyAI.SetStageSystem(m_StageSystem); } }
舉例來講,敵方單位AI類(EnemyAI), 在運行時須要使用關卡系統(StageSystem)的信息,但EnemyAI對象產生的位置是在敵方單位建造者(EnemyBuilder)之下:
EnemyBuilder.cs public class EnemyBuilder: ICharacterBuilder { public override void AddAI() { EnemyAI theAI = new EnemyAI(m_BuildParam.NewCharacter, m_BuildParam.AttackPosition); m_BuildParam.NewCharacter.SetAI(theAI); } }
按照"最少知識原則(LKP)",會但願敵方單位的建造者(EnemyBuilder)減小對其餘無關類的引用.所以,在產生敵方單位AI(EnemyAI)對象時,敵方單位建造者(EnemyBuilder)沒法將關卡系統(StageSystem)對象設置給敵方單位AI,這是屬於上述"生成的位置沒法引用到"的狀況.因此,能夠在敵方單位AI(EnemyAI)類中,提供一個靜態成員屬性和靜態方法,讓關卡系統(StageSystem)對象產生的當下,就設置給敵方單位AI(EnemyAI)類:
public class EnemyAI: ICharacterAI { private static StageSystem m_StageSystem = null; public static void SetStageSystem(StageSystem StageSystem) { m_StageSystem = StageSystem; } public ovrride bool CanAttackHeart() { m_StageSystem.LoseHeart(); return true; } }
使用類的靜態方法
每當增長一個類名稱就等同於又少了一個能夠使用的全局名稱,但若是是在類下增長"靜態方法"就不會減小可以使用的全局名稱數量,並且還能立刻增長這個靜態類方法的"可視性"----就是全局均可以引用這個靜態類方法.若是在項目開發時,不存在限制全局引用的規則,或者已經沒有更好的設計方法時,使用"類靜態方法"來獲取某一系統功能的接口,應該就是最佳的方式了.它有着單例模式(Singleton)的第二個特性:方便獲取對象
舉例來講,在《P級陣地》中,有一個靜態類PBDFactory就是按照這個概念去設計的.因爲它在《P級陣地》中負責的是全部資源的產生,因此將其定義爲"全局引用的類"並不違反這個遊戲項目的設計原則.它的每個靜態方法都負責返回一個"資源生成工廠接口",注意,是"接口",因此在之後的系統維護更新中,是能夠按照需求的改變來替換子類而不影響其餘客戶端:
public static class PBDFactory { private static IAssetFactory m_AssetFactory = null; public static IAssetFactory GetAssetFactory() { if (m_AssetFactory == null) { if (m_bLoadFromResource) { m_AssetFactory = new ResourceAssetFactory(); } else { m_AssetFactory = new RemoteAssetFactory(); } } return m_AssetFactory; } }
5.5 結論
單例模式(Singleton)的優勢是: 能夠限制對象的產生數量;提供方便獲取惟一對象的方法.單例模式(Singleton)的缺點是容易形成設計思考不周和過分使用的問題,但並非要求設計者徹底不使用這個模式,而是應該在仔細設計和特定的前提之下,適當地採用單例模式(Singleton)
在《P級陣地》中,只有少數地方引用到單例類PBaseDefenseGame,而引用點能夠視爲單例模式(Singleton)優勢的呈現
其餘應用方式
網絡在線遊戲的客戶端,能夠使用單例模式(Singleton)來限制鏈接數,以預防誤用而產生過多鏈接,避免服務器端所以失敗
日誌工具是比較不受項目類型影響的功能之一,因此能夠設計爲跨項目共享使用,此外,日誌工具大多使用在調試或重要信息的輸出上,而單例模式(Singleton)能讓程序設計師方便快速地獲取日誌工具,因此是個不錯的設計方式
第6章 遊戲內各系統的整合----中介者模式(Mediator)
6.1 遊戲系統之間的溝通
回顧單一職責原則(SRP)強調的是,將系統功能細分,封裝,讓每個類都能各司其職,負責系統中的某一功能.所以,一個分析設計良好的軟件或遊戲,都是由一羣子功能或子系統一塊兒組合起來運行的
public class CampInfoUI { CampSystem m_CampSystem; public void TrainSoldier(int SoldierID) { m_CampSystem.TrainSoldier(SoldierID); } } public class CampSystem { APSystem m_ApSystem; CharacterSystem m_CharacterSystem; } public class APSystem { GameStateInfoUI m_StateInfoUI; } public class GameStateInfoUI { APSystem m_ApSystem; } public class CharacterSystem { }
從上面的程序代碼能夠看出,全部系統在實現上都必須引用其餘系統的對象,而這些被引用的對象都必須在功能執行前設置好,或者在調用方法時經過參數傳入.但這些方法都會增長系統之間的依賴程度,也與最少知識原則(LKP)有所抵觸
系統切分越細,則意味着系統之間的溝通越複雜,若是系統內部持續存在這樣的鏈接,就會產生如下缺點:
1. 單一系統引入太多其餘系統的功能,不利於單一系統的轉換和維護
2. 單一系統被過多的系統所依賴,不利於接口的更改,容易牽一髮而動全身
3. 由於須要提供給其餘系統操做,系統的接口可能會過於龐大,不容易維護
要解決上述問題,能夠使用中介者模式(Mediator)的設計方式
中介者模式(Mediator)簡單解釋的話,比較相似於中央管理的概念.創建一個信息集中的中心,任何子系統要與它的子系統溝通時,都必須先將請求交給中央單位,再由中央單位分派給對應的子系統.這種交給中央單位統一分配的方式,在物流業中已證實是最有效率的方式
6.2 中介者模式(Mediator)
剛開始學習中介者模式(Mediator)時,會以爲爲何要如此麻煩,讓兩個功能直接調用就行了.但隨着經驗的累積,接觸過許多項目,而且想要跨項目轉換某個功能時就會知道,減小類之間的耦合度是一項很重要的設計原則.中介者模式(Mediator)在內部系統的整合上,扮演着重要的角色
6.2.1 中介者模式(Mediator)的定義
中介者模式(Mediator)在GoF中的說明是:
"定義一個接口用來封裝一羣對象的互動行爲.中介者經過移除對象之間的引用,來減小它們之間的耦合度,而且能改變它們之間的互動獨立性"
6.2.2 中介者模式(Mediator)的說明
參與者的說明以下:
Colleague(同事接口)
擁有一個Mediator屬性成員,能夠經過它來調用中介者的功能
ConcreateColleagueX(同事接口實現類)
實現Colleague界面的類,對於單一實現類而言,只會依賴一個Mediator接口
Mediator(中介者接口),ConcreteMediator(中介者接口實現類)
由Mediator定義讓Colleague類操做的接口
ConcreteMediator實現類中包含全部ConcreteColleague的對象引用
ConcreteMediator類之間的互動會在ConcreteMediator中發生
6.2.3 中介者模式(Mediator)的實現範例
Colleague.cs public abstract class Colleague { protected Mediator m_Mediator = null; public Colleague(Mediator theMediator) { m_Mediator = theMediator; } public abstract void Request(string Message); } ConcreteColleague1.cs public class ConcreteColleague1: Colleague { public ConcreteColleague1(Mediator theMediator): base(theMediator) { } public void Action() { m_Mediator.SendMessage(this, "Colleague1發除通知"); } public override void Request(string Message) { Debug.Log("ConcreteColleague1.Request: " + Message); } } public class ConcreteColleague2: Colleague { public ConcreteColleague2(Mediator theMediator): base(theMediator) { } public void Action() { m_Mediator.SendMessage(this, "Colleague2發除通知"); } public override void Request(string Message) { Debug.Log("ConcreteColleague2.Request: " + Message); } } public abstract class Mediator { public abstract void SendMesasge(Colleague theColleague, string Message); } public class ConcreteMediator: Mediator { ConcreteColleague1 m_Colleague1 = null; ConcreteColleague2 m_Colleague2 = null; public void SetColleague1(ConcreateColleague1 theColleague) { m_Colleague1 = theColleague; } public void SetColleague2(ConcreteColleague2 theColleague) { m_Colleague2 = theColleague; } public override void SendMessage(Colleague theColleague, string Message) { if (m_Colleague1 == theColleague) { m_Colleage2.Request(Message); } if (m_Colleague2 == theColleague) { m_Colleage1.Request(Message); } } } MediatorTest.cs void UnitTest() { ConcreteMediator pMediator = new ConcreateMediator(); ConcreateColleague1 pColleague1 = new ConcreateColleague1(pMediator); ConcreateColleague2 pColleague2 = new ConcreteColleague2(pMediator); pMediator.SetColleague1(pColleague1); pMediator.SetColleague2(pColleague2); pColleague1.Action(); pColleague2.Action(); } ConcreteColleague2.Request:Colleague1發出通知 ConcreteColleague1.Request:Colleague2發出通知
6.3 中介者模式(Mediator)做爲系統之間的溝通接口
6.3.1 使用中介者模式(Mediator)的系統架構
參與者的說明以下:
PBaseDefenseGame:
擔任中介者角色,定義相關的操做界面給全部遊戲系統與玩家界面來使用,幷包含這些遊戲系統和玩家界面的對象,同時負責相關的初始化流程
IGameSystem:
遊戲系統的共同父類,包含一個指向PBaseDefenseGame對象的類成員,在其下的子類都能經過這個成員向PBaseDefenseGame發出需求
GameEventSystem, CampSystem,...:
負責遊戲內的系統實現,這些系統之間不會互相引用及操做,必須經過PBaseDefenseGame來完成
IUserInterface:
玩家界面的共同父類,包含一個指向PBaseDefenseGame對象的類成員,在其下的子類都能經過這個成員向PBaseDefenseGame發出需求
SoldierInfoUI, CampInfoUI,...:
負責各玩家界面的實現,這些玩家界面與遊戲系統之間不會互相引用及操做,必須經過PBaseDefenseGame來完成
6.3.2 實現說明
PBaseDefenseGame.cs public class PBaseDefenseGame { private GameEventSystem m_GameEventSystem = null; ... private CampInfoUI m_CampInfoUI = null; ... public void Initinal() { m_GameEventSyste = new GameEventSystem(this); ... m_CampInfoUI = new CampInfoUI(this); ... EnemyAI.SetStageSystem(m_StageSystem); } public void UpgrateSoldier() { if (m_CharacterSystem != null) { m_CharacterSystem.UpgrateSoldier(); } } ... public void ShowCampInfo(ICamp camp) { m_CampInfoUI.ShowInfo(Camp); m_SoldierInfoUI.Hide(); } ... }
爲了可以更靈活地處理遊戲系統之間的溝通,《P級陣地》也實現了觀察者模式(Observer),遊戲事件系統(GameEventSystem)即觀察者模式(Observer)的類.經過它能減小PBaseDefenseGame中增長接口方法,而且讓信息的通知更有效率.
PBaseDefenseGame.cs public void RegisterGameEvent(ENUM_GameEvent emGameEvent, IGameEventObserver Observer) { m_GameEventSystem.RegisterObserver(emGameEvent, Observer); } public void NotifyGameEvent(ENUM_GameEvent emGameEvent, System.Object Param) { m_GameEventySystem.NotifySubject(emGameEvent, Param); } IGameSystem.cs public abstract class IGameSystem { protected PBaseDefenseGame m_PBDGame = null; public IGameSystem(PBaseDefenseGame PBDGame) { m_PBDGame = PBDGame; } ... } IUserInterface.cs public abstract class IUserInterface { protected PBaseDefenseGame m_PBDGame = null; public IUserInterface(PBaseDefenseGame PBDGame) { m_PBDGame = PBDGame; } ... } StageSystem.cs public class StageSystem: IGameSystem { public StageSystem(PBaseDefenseGame PBDGame): base(PBDGame) { Initialize(); } ... }
6.3.3 使用中介者模式(Mediator)的優勢
不會引入太多其餘的系統
從上面《P級陣地》的實現來看,每個遊戲系統和玩家界面除了會引用與自己功能相關的類外,不管是對外的信息獲取仍是信息的傳遞,都只經過PBaseDefenseGame類對象來完成.這使得每個遊戲系統,玩家界面對外的依賴度縮小到只有一個類(PBaseDefenseGame)
系統被依賴的程度也下降
每個遊戲系統或玩家界面,也只在PBaseDefenseGame類的方法中調用.因此,當遊戲系統或玩家界面有所更動時,受影響的也僅僅侷限於PBaseDefenseGame類,所以能夠減小系統維護的難度
6.3.4 實現中介者模式(Mediator)時的注意事項
因爲PBaseDefenseGame類擔任中介者(Mediator)的角色,再加上各個遊戲系統和玩家界面都必須經過它來進行信息交換即溝通,因此要注意的是,PBaseDefenseGame類會由於擔任過多中介者的角色而容易出現"操做接口爆炸"的狀況.所以,在實現上,咱們能夠搭配其餘設計模式來避免發生這種狀況.在前面的說明中,咱們說起的遊戲事件系統(GameEventSystem),其做用就是用來提供更好的信息傳遞方式,以減輕PBaseDefenseGame類的負擔
6.4 中介者模式(Mediator)面對變化時
如何應對變化
當遊戲系統或玩家界面須要新增功能,且該功能須要由外界提供信息才能完成時,能夠先在PBaseDefenseGame類中增長獲取信息的方法,以後再經過PBaseDefenseGame類來獲取信息完成新的功能.這樣一來,項目的修改能夠保持在兩個類或最多3個類的更改,而不會影響任何類的"依賴性"
如何面對新增
當須要新增長遊戲系統或玩家界面時,只要是繼承自IGameSystem或IUserInterface的遊戲系統和玩家界面,均可以直接加入PBaseDefenseGame的類成員中,並經過現有的接口進行實現或增長功能.這時候項目更改的幅度,可能只是新增一個程序文件和修改一個PBaseDefenseGame類而已,不太容易影響到其餘系統或接口
6.5 結論
與其餘模式(Pattern)的合做
PBaseDefenseGame類在《P級陣地》中,除了是中介者模式(Mediator)中的中介者(Mediator)以外,也是外觀模式(Facade)中對外系統整合接口的主要類,而且還運用單例模式(Singleton)來產生惟一的類對象
此外,爲了下降PBaseDefenseGame類有接口過大的問題,其子系統"遊戲事件系統"(GameEventSystem)專門運用觀察者模式(Observer)來解決遊戲系統之間,對於信息的產生和通知的需求,減小這些信息和通知的方法充滿在PBaseDefenseGame類之中
在進行分析設計時,集合多種設計模式是良好設計常見的方式,如何將所學設計模式融合並適當地運用,纔是設計模式之道
其餘應用方式
網絡引擎
連線管理系統與網絡數據封包管理系統之間,如何能夠經過中介者模式(Mediator)進行溝通,那麼就能輕鬆地針對連線管理系統抽換所使用的通訊方式(TCP或UDP)
數據庫引擎
內部能夠分紅數個子系統, 有專門負責數據庫鏈接的功能和產生數據庫操做數據的功能,兩個子功能之間的溝通能夠經過中介者模式(Mediator)來進行,讓二者之間不相互依賴,方便抽換另外一個子系統
第7章 遊戲的主循環----Game Loop
7.1 GameLoop由此開始
遊戲開發時特有的設計模式----遊戲循環
7.2 怎麼實現遊戲循環(Game Loop)
void main() { GameInit(); while (IsGameOver() == false) { UserInput(); UpdateGameLogic(); Render(); } GameRelease(); } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLine, int iCmdShow) { ... while (TRUE) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) { if (msg.message == WM_QUIT) { break; } TranslateMessage(&msg); DispatchMessage(&msg); } else { UserInput(); UpdateGameLogic(); Render(); } } }
7.3 在Unity3D中實現遊戲循環
public class GameLoop: MonoBehaviour { void Awake() { GameObject.DontDestroyOnLoad(this.gameObject); } void Start() { GameInit(); } void Update() { UserInput(); UpdateGameLogic(); } } public class GameFunction { public void Update() { } } public class GameLoop: MonoBehaviour { GameFunction m_GameFunction = new GameFunction(); void Awake() { GameObject.DontDestroyOnLoad(this.gameObject); } void Start() { GameInit(); } void Update() { UserInput(); m_GameFunction.Update(); ... } ... }
7.4 P級陣地的遊戲循環
GameLoop.cs public class GameLoop: MonoBehaviour { void Update() { m_SceneStateController.StateUpdate(); } } BattleState public class BattleState: ISceneState { public override void StateUpdate() { PBaseDefenseGame.Instance.Update(); } } PBaseDefenseGame.cs public class PBaseDefenseGame { public void Update() { ... } }
7.5 結論
每一款遊戲在實現時,都會有專用於這款遊戲的"玩家操做"和"遊戲邏輯更新"這兩項特殊需求.所以,在PBaseDefenseGame類內實現"遊戲循環"是比較好的設計方式,這樣能夠提升PBaseDefenseGame類整個移植道其餘項目的可能性
雖然《P級陣地》中大部分的遊戲功能和用戶界面類,都採用"不"繼承MonoBehaviour的方式來運行,但對於會出如今場景中的每一個遊戲3D角色上,仍是會搭配使用組件(繼承MonoBehaviour),因此每個腳本組件仍是會按照Unity3D引擎的流程去操做每個遊戲對象(GameObject)
第8章 角色系統的設計分析
8.1 遊戲角色的架構
8.2 角色類的規劃
public abstract class ICharacter [ } public abstract class ISoldier: ICharacter { public ISoldier() { } } public abstract class IEnemy: ICharacter { public IEnemy() { } }
第9章 角色與武器的實現----橋接模式(Bridge)
9.1 角色與武器的關係
Weapn.cs public enum ENUM_Weapn { Null = 0, Gun = 1, Rifle = 2, Rocket = 3, Max, } public class Weapon { } ICharacter.cs public abstract class ICharacter { protected Weapon m_Weapon = null; } IEnemy.cs public class IEnemy: ICharacter { public override void Attack(IChatacter theTarget) { case Gun: break; ... } } ISoldier.cs public class ISoldier: ICharacter { public override void Attack(IChatacter theTarget) { case Gun: break; ... } }
將兩種角色與3種武器交叉組合,而後以上述方式實現,會存在如下兩個缺點:
1. 每一個繼承自ICharacter角色接口的類,在從新定義Attack方式時,都必須針對每一種武器來實現(顯示特效和播放音效),或者進行額外的公式計算.因此當要新增角色類時,也要在新的子類種重複編寫相同的程序代碼
2. 當要新增武器類型時,全部角色子類種的Attack方法,都必須修改,針對新的武器類型編寫新的對應程序代碼.這樣會增長維護的難度,使得武器類型不容易增長
通常來講,上述的狀況能夠視爲兩個類羣組交互使用所引起的問題
GoF的設計模式中,橋接模式(Bridge)能夠用來解決上述實現方式的缺點
9.2 橋接模式(Bridge)
筆者認爲,在GoF的23種設計模式中,橋接模式是最好應用但也是最難理解的,尤爲是它的定義不長,其中關鍵的"抽象與實現分離(Decouple an abstraction from its implementation)",常讓設計師花費許多時間,才能慢慢了解它背後所表明的原則
9.2.1 橋接模式(Bridge)的定義
橋接模式(Bridge),在GoF中的解釋是:
"將抽象與實現分離,使兩者能夠獨立地變化"
多數人會覺得這是"只依賴接口而不依賴實現"原則的另一個解釋:
"定義一個接口類,而後將實現的部分在子類中完成"
public class DirectX { public void DXRender(string ObjName) { Debug.Log("DXRender:" + ObjName); } } public class OpenGL { public void GLRender(string ObjName) { Debug.Log("OpenGL:" + ObjName); } } public abstract class ISphere { public abstract void Draw(); }
public class ShpereDX: ISphere { DirectX m_DirectX; public override void Draw() { m_DirectX.DXRender("Sphere"); } } public class SphereGL: ISphere { OpenGL m_OpenGL; public override void Draw() { m_OpenGL.GLRender("Sphere"); } }
9.2.2 橋接模式(Bridge)的說明
若是要避免被限制在只能以"繼承實現"來完成功能實現,可考慮使用橋接模式(Bridge).從先前的例子中能夠看出,基本上這是兩個類組羣之間,關係呈現"交叉組合彙編"的狀況:
羣組一的"抽象類"指的是將對象或功能經"抽象"以後所定義出來的類接口,並經過子類繼承的方式產生多個不一樣的對象或功能.例如上述的"形狀"類,其用途是用來描述一個有"形狀"的對象應該具有的功能和操做方式.因此,這個羣組只負責增長"抽象類",不負責實現"接口定義的功能
羣組二的"實現類"指的是這些類能夠用來實現"抽象類"中所定義的功能.例如上述例子中的OpenGL引擎類和DirectX引擎類,它們能夠用來實現"形狀"類中所定義的"繪出"功能,能將形狀繪製道屏幕上.因此,這個羣組只負責增長"實現類"
繼承"抽象類"的子類須要實現功能時,只要經過"實現類"的對象引用m_RenderEngine來調用實現功能便可.這樣一來,就真正讓"抽象與實現分離",也就是"抽象不與實現綁定",讓"球體"或者"立方體"這種抽象概念的類,再也不經過產生不一樣子類的方式去完成特定的"實現方式"(OpenGL或DirectX),將"抽象類羣組"與"實現類羣組"完全分開
參與者的說明以下:
Abstraction(抽象體接口)
擁有指向Implementor的對象引用
定義抽象功能的接口,也可做爲子類調用實現功能的接口
RefinedAbstraction(抽象體實現,擴充)
繼承抽象體並調用Implementor完成實現功能
擴充抽象體的接口,增長額外的功能
Implementor(實現體接口)
定義實現功能的接口,提供給Abstraction(抽象體)使用
接口功能能夠只有單一的功能,真正的選擇則再由Abstraction(抽象體)的需求加以組合應用
ConcreteImplementA/B(實現體)
實際完成實現體接口上所定義的方法
9.2.3 橋接模式(Bridge)的實現範例
public abstract class RenderEngine { public abstract void Render(string ObjName); } public class DirectX: RenderEngine { public ovrride void Render(string ObjName) { DXRender(ObjName); } public void DXRender(string ObjName) { Debug.Log("DXRender:" + ObjName); } } public class OpenGL: RenderEngine{ public override void Render(string ObjName) { GLRender(ObjName); } public void GLRender(string ObjName) { Debug.Log("OpenGL:" + ObjName); } } public abstract class IShape { procted RenderEngine m_RenderEngine = null; public void SetRenderEngine(RenderEngine theRenderEngine) { m_RenderEngine = theRenderEngine; } public abstract void Draw(); } public class Sphere: IShape { public override void Draw() { m_RenderEngine.Render("Sphere"); } } public class Cube: IShape { public override void Draw() { m_RenderEngine.Render("Cube"); } } public class Cylinder: IShape { public override void Draw() { m_RenderEngine.Render("Cylinder"); } }
9.3 使用橋接模式(Bridge)實現角色與武器接口
定義哪一個羣組類是"抽象類",哪一個又是"實現類"並不容易.不過,若是從兩個類羣組的交叉合做開始分析,那麼對於橋接模式(Bridge)的運用就不會那麼困難了
9.3.1 角色與武器接口設計
橋接模式(Bridge)除了可以應用在"抽象與實現"的分離以外,還能夠應用在: "當兩個羣組由於功能上的需求,想要鏈接合做,但又但願兩組類能夠各自發展不受彼此影響時"
參與者的說明以下:
ICharacter: 角色的抽象接口擁有一個IWeapon對象引用,而且在接口中聲明瞭一個武器攻擊目標WeaponAttackTarget()方法讓子類能夠調用,同時要求繼承的子類必須在Attack()中從新實現攻擊目標的功能
ISoldier,IEnemy: 雙方陣營單位,實現攻擊目標Attack()時,只須要調用父類的WeaponAttackTarget()方法,就能夠使用當前裝備的武器攻擊對手
IWeapon: 武器接口,定義遊戲中對於武器的操做和使用方法
WeaponGun, WeaponRifle, WeaponRocket: 遊戲中能夠使用的3種武器類型的實現
9.3.2 實現說明
public abstract class IWeapon { } public class WeaponGun: IWeapon { } public class WeaponRifle: IWeapon { } public class WeaponRocket: IWeapon { } public abstract class ICharacter { private IWeapon m_Weapon = null; } public class ISoldier: ICharacter { } public class IEnemy: ICharacter { }
9.3.3 使用橋接模式(Bridge)的優勢
運用橋接模式(Bridge)後的ICharacter(角色接口)就是羣組一"抽象類",它定義了"攻擊目標「功能,但真正實現"攻擊目標"功能的類,則是羣組二IWeapon(武器接口)"實現類".對於ICharacter及其繼承類都沒必要理會IWeapon羣組的變化,尤爲是遊戲開發後期可能增長的武器類型.二對於ICharacter來講,它面對的至於IWeapon這個接口類,相對地,IWeapon類羣組也沒必要理會角色類羣組內的新增或修改,讓兩個羣組之間的耦合度降到最低
9.3.4 實現橋接模式(Bridge)的注意事項
9.4 橋接模式(Bridge)面對變化時
應用了橋接模式(Bridge)的角色與武器系統,在後續的遊戲系統設計上,增長了很多的彈性和靈活度.當須要新增武器類型時,繼承IWeapon類並從新實現抽象方法後,就可以讓角色系統裝備使用
public class WeaponCannon: IWeapon { public WeaponCannon() { } public override void Fire(ICharacter theTarget) { ... } }
9.5 結論
橋接模式(Bridge)能夠將兩個羣組有效地分離,讓兩個羣組彼此互相不受影響.這兩個羣組能夠是"抽象定義"與"功能實現",也能夠是兩個須要交叉合做後才能完成某項任務的類
與其餘模式(Pattern)的合做
《P級陣地》將使用建造者模式(Builder)負責產生遊戲中的遊戲角色對象,當角色產生時會設置須要裝備的武器,而設置武器的操做則是由角色接口中的方法來完成
其餘應用方式
兩組類羣組須要搭配使用的實現方式,常見於遊戲設計中,例如:
遊戲角色能夠駕駛不一樣的行動載具,如汽車,飛機,水上摩托......
奇幻類型遊戲的角色能夠施展法術,除了多樣的角色以外,"法術」自己也是另外一個複雜的系統,火系法術,冰系法術,......,遠程法術,近戰法術,補血法術......,想額外加上使用限制的話,就必須使用橋接模式(Bridge)讓角色與法術類羣組妥善結合
第10章 角色的屬性----策略模式(Strategy)
10.1 角色屬性的計算需求
public enum ENUM_Character { Soldier = 0, Enemy } public class Character { protected Weapon m_Weapon = null; ENUM_Character m_CharacterType; int m_MaxHP = 0; int m_NowHP = 0; float m_MoveSpeed = 1.0f; int m_SoldierLv = 0; int m_CritRate = 0; ... public void InitCharacter() { switch (m_CharacterType) { case ENUM_Character.Soldier: if (m_SoldierLv > 0) { m_MaxHP += (m_SoldierLv - 1) * 2; } break; case ENUM_Character.Enemy: break; } m_NowHP = m_MaxHP; } public void Attack(ICharacter theTarget) { int AtkPlusValue = 0; switch (m_CharacterType) { case ENUM_Character.Soldier: break; case ENUM_Character.Enemy: int RandValue = UnityEngine.Random.Range(0, 100); if (m_CritRate >= RandValue) { AtkPlusValue = m_MaxHP * 5; } break; } m_Weapon.SetAtkPlusValue(AtkPlusValue); m_Weapon.Fire(theTarget); } public void UnderAttack(ICharacter Attacker) { int AtkValue = Attacker.GetWeapon().GetAtkValue(); switch (m_CharacterType) { case ENUM_Character.Soldier: AtkValue -= (m_SoldierLv - 1) * 2; break; case ENUM_Character.Enemy: break; } m_NowHP -= AtkValue; if (m_NowHP <= 0) { Debug.Log("角色陣亡"); } } ... }
在這3個操做方法中,都針對不一樣的角色類型進行了相對應的屬性計算,但這樣的實現方式有如下缺點:
每一個方法都針對角色類型進行屬性計算,因此這3個方法依賴角色類型,當新增角色類型時,必須修改這3個方法,所以會增長維護的難度
同一類型的計算規則分散在角色類Character中,不易閱讀和了解
對於這些因角色不一樣而有差別的計算公式,該如何從新設計才能解決上述問題呢?GoF的策略模式爲咱們提供瞭解答
10.2 策略模式(Strategy)
因條件的不一樣而須要有所選擇時,剛入門的程序設計師會使用If else或多組的if elseif else 來完成需求,或者使用switch case 語句來完成.固然,這是由於入門的程序書籍大可能是這樣建議的,並且也是最快完成實現的方式.對於小型項目或快速開發驗證用的項目而言,或許能夠使用比較快速的條件判斷方式來實現.但若遇到具備規模或產品化(須要長期維護)項目時,最好仍是選擇策略模式來完,由於這將有利於項目的維護
10.2.1 策略模式(Strategy)的定義
GoF對策略模式的解釋是
"定義一組算法,並封裝每一個算法,讓它們能夠彼此交換使用.策略模式讓這些算法在客戶端使用它們時能更加獨立"
就"策略"一詞來看,有當發生「某狀況」時要作出什麼「反應」的含義.從生活中能夠舉出許多在相同的環境下針對不一樣條件,要進行不一樣計算方式的例子:
當購買商品滿399時,要加送100元折價券
當購買商品滿699時,要加送200元折價券
...
在策略模式中,這些不一樣的計算方式就是所謂的"算法",而這些算法中的每個都應該獨立出來,將計算細節加以封裝隱藏,並讓它們成爲一個算法類羣組.客戶端只須要根據狀況來選擇對應的算法類便可,至於計算方式及規則,客戶端不須要去理會
10.2.2 策略模式(Strategy)的說明
參與者的說明以下:
Strategy(策略接口類) 提供"策略客戶端「能夠使用的方法
ConcreteStrategyA~ConcreteStrategyC(策略實現類) 不一樣算法的實現
Context(策略客戶端) 擁有一個Strategy類的對象引用,並經過對象引用獲取想要的計算結果
10.2.3 策略模式(Strategy)的實現範例
public abstract class Strategy { public abstract void AlgorithmInterface(); } public class ConcreteStrategyA: Strategy { public override void AlgorithmInterface() { Debug.Log("ConcreteStrategyA.AlgorithmInterface"); } } public class ConcreteStrategyB: Strategy { public override void AlgorithmInterface() { Debug.Log("ConcreteStrategyB.AlgorithmInterface"); } } public class ConcreteStrategyC: Strategy { public override void AlgorithmInterface() { Debug.Log("ConcreteStrategyC.AlgorithmInterface"); } } public class Context { Strategy m_Strategy = null; public void SetStrategy(Strategy theStrategy) { m_Strategy = theStrategy; } public void ContextInterface() { m_Strategy.AlgorithmInterface(); } } void UnitTest() { Context theContext = new Context(); theContext.SetStrategy(new ConcreteStrategyA()); theContext.ContextInterface(); theContext.SetStrategy(new ConcreteStrategyB()); theContext.ContextInterface(); theContext.SetStrategy(new ConcreteStrategyC()); theContext.ContextInterface(); }
10.3 使用策略模式(Strategy)實現攻擊計算
許多人在想到要應用策略模式時,經常會遇到不知從何切入的狀況.究其緣由,一般是不知道如何在不使用if else或switch case語句的狀況下,將這些計算策略配對調用.其實,有時候處理方式是必須利用重構方法或搭配其餘的設計模式來完成的,也就是先利用重構方法或搭配其餘的設計模式將這些條件判斷語句從程序代碼中刪除,再將策略模式加入到項目的設計方案中.不然,最多見的策略模式應用方式,仍是會在if else或 switch case語句中調用對應的策略類對象
10.3.1 攻擊流程的實現
參與者的說明以下
ICharacterAttr 聲明遊戲內使用的角色屬性,訪問方法和聲明攻擊流程中所須要的方法,並擁有一個IAttrStrategy對象,經過該對象來調用真正的計算公式
IAttrStrategy 聲明角色屬性計算的接口方法,用來把ICharacterAttr與計算方法分離,讓ICharacterAttr可輕易地更換計算策略
EnemyAttrStrategy 實現敵方陣營單位在攻擊流程中所須要的各項公式的計算
SoldierAttrStrategy 實現玩家陣營單位在攻擊流程中所須要的各項公式的計算
10.3.2 實現說明
public abstract class IAttrStrategy { // 初始的屬性 public abstract void InitAttr(ICharacterAttr CharacterAttr); // 攻擊加成 public abstract int GetAtkPlusValue(ICharacterAttr CharacterAttr); // 獲取減小傷害值 public abstract int GetDmgDescValue(ICharacterAttr CharacterAttr); } public class SoldierAttrStrategy: IAttrStrategy { public override void IniAttr(ICharacterAttr CharacterAttr) { SoliderAttr theSoliderAttr = CharacterAttr as SoliderAttr; if (theSoldierAttr == null) { return; } int AddMaxHP = 0; int Lv = theSoldierAttr.GetSoldierLv(); if (Lv > 0) { AddMaxHP = (Lv - 1) *2; } theSoldierAttr.AddMaxHP(AddMaxHP); } public override int GetAtkPlusValue(ICharacterAttr CharacterAttr) { return 0; } public override int GetDmgDescValue(ICharacterAttr CharacterAttr) { SoliderAttr theSoliderAttr = CharacterAttr as SoldierAttr; if (theSoliderAttr == null) { return 0; } return (theSoliderAttr.GetSoldierLv() - 1) * 2; } } public class EnemyAttrStrategy: IAttrStrategy { public override void InitAttr(ICharacterAttr CharacterAttr) { EnemyAttr theEnemyAttr = CharacterAttr as EnemyAttr; if (theEnemyAttr == null) { return 0; } int RandValue = UnityEngine.Random.Range(0, 100); if (theEnemyAttr.GetCritRate() >= RandValue) { theEnemyAttr.CutdownCritRate(); return theEnemyAttr.GetMaxHP() * 5; } return 0; } public override int GetDmgDescValue(ICharacterAttr CharacterAttr) { return 0; } } public abstract class ICharacterAttr { protected int m_MaxHP = 0; protected int m_NowHP = 0; protected float m_MoveSpeed = 1.0f; protected string m_AttrName = ""; protected IAttrStrategy m_AttrStrategy = null; public void SetAttStrategy(IAttrStrategy theAttrStrategy) { m_AttrStrategy = theAttrStrategy; } public IAttrStrategy GetAttrStrategy() { return m_AttrStrategy; } public virtual void InitAttr() { m_AttrStrategy.InitAttr(this); FullNowHP(); } public int GetAtkPlusValue() { return m_AttrStrategy.GetAtkPlusValue(this); } public void CalDmgValue(ICharacter Attacker) { int AtkValue = Attacker.GetAtkValue(); AtkValue -= m_AttrStrategy.GetDmgDescValue(this); m_NowHP -= AtkValue; } } public abstract class ICharacter { ... private IWeapon m_Weapon = null; protected ICharacterAttr m_Attribute = null; public virtual void SetCharacterAttr(ICharacterAttr CharacterAttr) { m_Attribute = CharacterAttr; m_Attribute.InitAttr(); m_NavAgent.speed = m_Attribute.GetMoveSpeed(); m_Name = m_Attribute.GetAttrName(); } public void Attack(ICharacter Target) { SetWeaponAtkPlusValue(m_Attribute.GetAtkPlusValue()); WeaponAttackTarget(Target); } public void Attack(ICharacter theTarget) { m_Weapon.SetAtkPlusValue(m_Attribute.GetAtkPlusValue()); m_Weapon.Fire(theTarget); } public void UnderAttack(ICharacter Attacker) { m_Attribute.CalDmgValue(Attacker); if (m_Attribute.GetNowHP() <= 0) { Debug.Log("角色陣亡"); } } }
10.3.3 使用策略模式(Strategy)的優勢
讓角色屬性變得好維護
對於改進後的角色類ICharacter來講,將角色屬性有關的屬性以專屬類ICharacterAttr來取代,能夠使之後角色屬性變更時,不會影響到角色類ICharacter.此外,隨着遊戲需求的複雜化,加入更多的角色屬性是可預期的,因此讓角色屬性集中在同一個類下管理,將有助於後續遊戲項目的維護,也能夠減小角色類ICharacter的更改及下降複雜度
沒必要再針對角色類型編寫程序代碼
經過ICharacterAttr與其子類的分工,將雙方陣營的屬性放置於不一樣的類中.對於角色類ICharacter而言,使用ICharacterAttr的對象引用,徹底不用考慮將使用哪個子類對象,避免了使用switch case語句的編寫方式及後續可能產生的維護問題.當有新的陣營類產生時,角色類ICharacter並不須要有任何改動
計算公式的替換更爲方便
在遊戲開發的過程當中,屬性計算公式是最常變換的.運用策略模式後的ICharacterAttr,更容易替換公式,除了能夠保留原來的計算公式外,還可讓全部公式同時並存,而且能自由切換.
10.3.4 實現策略模式(Strategy)的注意事項
與狀態模式的差異
若是讀者仔細分析狀態模式與策略模式的類結構圖,可能會發現二者看起來很是類似
二者都被GoF歸類在行爲模式分類下,都是由一個Context類來維護對象引用,並藉此調用提供功能的方法.就筆者過去的實踐經驗,對於這兩種模式,可歸類出下面幾點差別,供讀者做爲之後選擇時的引用依據:
State是在一羣狀態中進行切換,狀態之間有對應的和鏈接的關係; Strategy則是由一羣沒有任何關係的類所組成,不知彼此的存在
State受限於狀態機的切換規則,在設計初期就會定義全部可能狀態,就算後期追加也須要和現有的狀態有所關聯,而不是想加入就加入;Strategy是由封裝計算算法而造成的一種設計模式,算法之間不存在任何依賴關係,有新增的算法就能夠立刻加入或替換
10.4 策略模式(Strategy)面對變化時
10.5 結論
將複雜的計算公式從客戶端獨立出來成爲一個羣組,以後客戶端能夠按狀況來決定使用的計算公式策略,既提升了系統應用的靈活程度,也強化了系統中對全部計算策略的維護方式.讓後續開發人員很容易找出相關計算公式的差別,同時修改點也會縮小到計算公式自己,也不會影響到使用的客戶端
與其餘模式的合做
在第15章中,《P級陣地》將使用建造者模式負責產生遊戲中的角色對象.當角色產生時,會須要設置該角色要使用的"角色屬性",這部分將有各陣營的建造者來完成.若策略模式搭配其餘設計模式一塊兒應用的話,就能夠沒必要使用if else或switch case 來選擇要使用的策略類
其餘應用方式
有些角色扮演型遊戲的屬性系統,會使用轉換計算的方式來獲取角色最終要使用的屬性.例如:玩家看到角色界面上只會顯示體力,力量,敏捷......,但實際在運用攻擊計算時,這些屬性會被再轉換爲生命力,攻擊力,閃避率.......而之因此會這樣設計的緣由在於,該遊戲有"職業"的設置,對於不一樣的"職業",在計算轉換時會有不一樣的轉換方式,利用策略模式將這些轉換公式獨立出來是比較好的
遊戲角色操做載具時,會引用角色當前對該類型載具的累積時間,並將之轉換爲"操控性",操控性越好,就越能控制該載具,而獲取操控性的計算公式,也能夠利用策略模式將其獨立出來
網絡在線型遊戲每每須要玩家註冊帳號,註冊帳號有多種方式,例如OpenID,自建帳號,隨機產生等.經過策略模式能夠將不一樣帳號的註冊方式獨立爲不一樣的登陸策略.這樣作,除了能夠強化項目的維護,也能夠方便轉換到不一樣的遊戲項目上,增長重複利用的價值
第11章 攻擊特效與擊中反應----模板方法模式(Template Method)
11.1 武器的攻擊流程
public class WeaponGun: IWeapon { public Weapon() { } public override void Fire(ICharacter theTarget) { ShowShootEffect(); ShowBulletEffect(theTarget.GetPosition(), 0.03f, 0.2f); ShowSoundEffect("GunShot"); theTarget.UnderAttack(m_WeaponOwner); } } public class WeaponRifle: IWeapon { public WeaponRifle() { } public override void Fire(ICharacter theTarget) { ShowShootEffect(); ShowBulletEffect(theTarget.GetPosition(), 0.5f, 0.2f); ShowSoundEffect("RifleShot"); theTarget.UnderAttack(m_WeaponOwner); } } public class WeaponRocket: IWeapon { public WeponRocket() { } public override void Fire(ICharacter theTarget) { ShowShootEffect(); ShowBulletEffect(theTarget.GetPosition(), 0.8f, 0.5f); ShowSoundEffect("RocketShot"); theTarget.UnderAttack(m_WeaponOwner); } }
11.2 模板方法模式(Template Method)
程序代碼中的"流程",有時候不太容易觀察出來,尤爲是當原有的程序代碼尚未通過適當重構.有個很好的判斷技巧,若是程序設計師發現更新一段程序代碼以後,還有另外一段程序代碼也使用相同的"演算流程",且實現的內容不太同樣,那麼這兩段程序代碼就能夠用模板方法模式加以重寫
11.2.1 模板方法模式(Template Method)的定義
GoF對於模板方法模式的定義是:
"在一個操做方法中定義算法的流程,其中某些步驟由子類完成.模板方法模式讓子類在不變動原有算法流程的狀況下,還可以從新定義其中的步驟"
從上述的定義來看,模板方法模式包含如下兩個概念:
1. 定義一個算法的流程,便是很明確地定義算法的每個步驟,並寫在父類的方法中,而每個步驟均可以是一個方法的調用
2. 某些步驟由子類完成,爲何父類不本身完成,卻要由子類去實現呢?
定義算法的流程中,某些步驟須要由執行時"當下的環境"來決定
定義算法時,針對每個步驟都提供了預設的解決方案,但有時候會出現"更好的解決方法",此時就須要讓這個更好的解決方案,可以在原有的架構中被使用
如下提供幾個例子跟你們說明:
以麪包的配方和製做方法爲例,大概是這樣寫的:
食材: A1.xxx, A2.xxx, A3.xxxx ... B1.yyy, B2.yyy
步驟:
1. 將材料A1~A5混合在一塊兒攪拌至光滑
2. 至於密閉空間醒面30~50分鐘
3. 分紅5等份, 整形滾圓再靜置越10~20分鐘
4. 包入B1~B3內餡, 整造成長條形狀
5. 置於密閉空間作二次發酵,約30~50分鐘
6. 烤培:越熱180°,進爐降溫165°,烘烤15~20分鐘至表面上色便可
若是將麪包配方和製做方法當作是"算法的流程",那麼其中的1~6就是每個步驟,並且每個步驟遵循着必定的前後順序.作過麪包的讀者應該能夠了解,麪包要好吃,發酵的時間長度是關鍵,而溫度,溼度等都會影響發酵所需的時間.因此,上述製做麪包的步驟中,第2,3,5項是須要實現麪包的人按照當天的環境狀況來決定發酵的時間,這也是爲何食譜上常出現xx~xx分鐘,而不是明確告訴你必定要多少分鐘,也就是GoF定義中所提示的"定義算法的流程中,某些步驟須要由執行時'當下的環境'來決定"
11.2.2 模板方法模式(Template Method)的說明
參與者的說明以下:
AbstractClass(算法定義類)
定義算法架構的類
能夠在某個操做方法(Template Method)中定義完整的流程
定義流程中會調用到方法(PrimitiveOperation), 這些方法將由子類從新實現
ConcreteClass(算法步驟的實現類)
從新實現父類中定義的方法,並可按照子類的執行狀況反應步驟實際的內容
11.2.3 模板方法模式(Template Method)的實現範例
public abstract class AbstractClass { public void TemplateMethod() { PrimitiveOperation1(); PrimitiveOperation2(); } protected abstract void PrimitiveOperation1(); protected abstract void PrimitiveOperation2(); } public class ConcreteClassA: AbstractClass { protected override void PrimitiveOperation1() { Debug.Log("ConcreteClassA.PrimitiveOperation1"); } protected override void PrimitiveOperation2() { Debug.Log("ConcreateClassA.PrimitiveOperation2"); } } public class ConcreteClassB: AbstractClass { protected override void PrimitiveOperation2() { Debug.Log("ConcreteClassB.PrimitiveOperation1"); } protected override void PrimitiveOperation2() { Debug.Log("ConcreteClassB.PrimitiveOperation2"); } } void UnitTest() { AbstractClass theClass = new ConcreteClassA(); theClass.TemplateMethod(); theClass = new ConcreteClassB(); theClass.TemplateMethod(); }
11.3 使用模板方法模式實現攻擊與擊中流程
很難找出程序代碼中相同的演算流程,是程序設計放棄使用模板方法模式的緣由之一;另外一種常見的狀況是,有時這些演算流程中會有一些小變化,也是由於這些小變化致使程序設計放棄使用模板方法模式.而那個小變化多是,A流程中有一個if判斷語句用以決定是否執行某項功能,但在B流程中卻沒有這個if判斷語句.當筆者在遇到這種狀況時,會連同這個if判斷語句一塊兒設置爲步驟的一部分,只是重構後的B類(B流程)不去從新定義這一步驟所調用的方法
11.3.1 攻擊與擊中流程的實現
參與者的說明以下:
IWeapon 在攻擊目標Fire方法中定義流程, 也就是要執行的各個步驟,並將這些步驟聲明爲抽象方法
WeaponGun, WeponRifle, WeaponRocket 實現IWeapon類中須要從新實現的抽象方法
11.3.2 實現說明
public abstract class IWeapon { protected int m_AtkPlusValue = 0; protected int m_Atk = 0; protected float m_Range = 0.0f; public void Fire(ICharacter theTarget) { ShowShootEffect(); DoShowBulletEffect(theTarget); DoShowSoundEffect(); theTarget.UnderAttack(m_WeaponOwner); } protected abstract void DoShowBulletEffect(ICharacter theTarget); protected abstract void DoShowSoundEffect(); } public class WeaponGun: IWeapon { public WeaponGun() { } protected override void DoShowBulletEffect(ICharacter theTarget) { ShowBulletEffect(theTarget.GetPosition(), 0.03f, 0.2f); } protected override void DoShowSoundEffect() { ShowSoundEffect("GunShot"); } } public class WeaponRifle: IWeapon { protected override void DoShowBulletEffect(ICharacter theTarget) { ShowBulletEffect(theTarget.GetPosition(), 0.5f, 0.2f); } protected override void DoShowSoundEffect() { ShowSoundEffect("RifleShot"); } } public class WeaponRocket: IWeapon { public WeaponRocket() { } protected override void DoShowBulletEffect(ICharacter theTarget) { ShowBulletEffect(theTarget.GetPosition(), 0.8f, 0.5f); } protected override void DoShowSoundEffect() { ShowSoundEffect("RocketShot"); } }
11.3.3 運用模板方法模式(Template Method)的優勢
在IWeapon類中,將"攻擊目標Fire方法"從新修改後,攻擊目標的"算法"只被編寫一次,須要變化的部分,則由實現的子類負責,這樣一來,本來須要在子類中"重複實現算法"的缺點就不會再出現了
11.3.4 修改擊中流程的實現
public abstract class ICharacter { ... public abstract void UnderAttack(ICharacter Attacker); ... } public abstract class ISoldier: ICharacter { public override void UnderAttack(ICharacter Attacker) { m_Attribute.CalDmgValue(Attacker); if (m_Attribute.GetNowHP() <= 0) { DoPlayKilledSound(); DoShowKilledEffect(); Killed(); } } ... } public abstract class IEnemy: ICharacter { public override void UnderAttack(ICharacter Attacker) { m_Attribute.CalDmgValue(Attacker); DoPlayHitSound(); DoShowHitEffect(); if (m_Attribute.GetNowHP() <= 0) { Killed(); } } ... }
11.4 模板方法模式(Template Method)面對變化時
11.5 結論
運用模板方法模式的優勢是,將可能重複出現的"算法流程",從子類提高到父類中,減小重複的發生,而且也開放子類參與算法中各個步驟的執行或優化.但若是"算法流程"開放太多的步驟,並要求子類必須所有從新實現的話,反而會形成實現的困難,也不容易維護
其餘應用方式
奇幻類角色扮演遊戲,對於遊戲角色要施展一個法術時,會有許多特定的檢查條件,如魔力是否足夠,是否還在冷卻時間內,對象是否在法術施展範圍內等.若是這些檢查條件會按照施展法術的類型而有所不一樣,那麼就能夠使用模板方法模式將檢查流程固定下來,真正檢查的功能則交給各法術子類去實現.另外,一個法術的施展流程和擊中計算也能夠如同本章範例同樣,將流程固定下來,細節交給各法術子類去實現
在線遊戲的角色登陸,也能夠使用模板方法模式將登陸流程固定下來,例如: 顯示登陸畫面,選擇登陸方法,輸入帳號密碼,向Server請求登陸等,而後讓登陸功能的子類去從新實現其中的步驟,另外,也能夠實現不一樣的登陸流程樣板來對應不一樣的登陸方式
第12章 角色AI----狀態模式(State)
12.1 角色的AI
12.2 狀態模式(State)
12.3 使用狀態模式(State)實現角色AI
12.3.1 角色AI的實現
12.3.2 實現說明
12.3.3 使用狀態模式(State)的優勢
12.3.4 角色AI執行流程
12.4 狀態模式(State)面對變化時
12.5 結論
第13章 角色系統
13.1 角色類
13.2 遊戲角色管理系統
public class CharacterSystem: IGameSystem { private List<ICharacter> m_Soliders = new List<ICharacter>(); private List<ICharacter> m_Enemys = new List<ICharacter>(); ... public override void Update() { UpdateCharacter(); UpdateAI(); } private void UpdateCharacter() { foreach (ICharacter Character in m_Soldiers) { Character.Update(); } foreach (ICharacter Character in m_Enemys) { Character.Update(); } } private void UpdateAI() { UpdateAI(m_Soldiers, m_Enemys); UpdateAI(m_Enemys, m_Soldiers); RemoveCharacter(); } private void UpdateAI(List<ICharacter> Characters, List<ICharacter> Targets) { foreach (ICharacter Character in Characters) { Character.UpdateAI(Targets); } } }
第14章 遊戲角色的產生----工廠方法模式(Factory Method)
14.1 產生角色
public class SoldierCamp { public ISoldier TrainRookie(ENUM_Weapon emWeapon, int Lv) { } public ISoldier TrainSergeant(ENUM_Weapon emWeapon, int Lv) { } public ISoldier TrainCaption(ENUM_Weapon emWeapon, int Lv) { } } public class StageSystem { public IEnemy AddElf(ENUM_Weapon emWeapon) { } public IEnemy AddOrge(ENUM_Weapon emWeapon) { } public IEnemy AddTroll(Enum_Weapon emWeapon) { } }
在兩個類中,共聲明瞭6個方法來產生不一樣的角色對象.在實踐中,聲明功能類似太高的方法會有不易管理的問題,並且這一次實現的6個方法中,每一個角色對象的組裝流程重複性過高.此外,將產生相同類羣組對象的實現,分散在不一樣的遊戲功能下不易管理和維護.因此,是否能夠將這些方法都集合在一個類下實現,而且以更靈活的方式來決定產生對象的類呢?GoF的工廠方法模式爲上述問題提供了答案
14.2 工廠方法模式(Factory Method)
提到工廠,大多數人的概念多是能夠大量生產東西的地方,而且是以有組織,有規則的方式來生產東西.它會有多條生產線,每一條生產線都有特殊的配置,專門用來生產特定的東西.沒錯,工廠方法模式就是用來搭建專門生產軟件對象的地方,並且這樣的軟件工廠,也能針對特定的類配置特定的組裝流程,來知足客戶端的要求
14.2.1 工廠方法模式(Factory Method)的定義
GoF對工廠方法模式的解釋是:
"定義一個能夠產生對象的接口,可是讓子類決定要產生哪個類的對象.工廠方法模式讓類的實例化程序延遲到子類中實施"
工廠方法模式就是將類產生對象的流程集合管理的模式.集合管理帶來的好處是:
1. 能針對對象產生的流程指定規則
2. 減小客戶端參與對象生成的過程,尤爲是對於那種類對象生產過程過於複雜的,若是讓客戶端操做對象的組裝過程,將使得客戶端與該類的耦合度(即依賴度)太高,不利於後續的項目維護
工廠方法模式是先定義一個產生對象的接口,以後讓它的子類去決定產生哪種對象,這有助於將龐大的類羣組進行分類.
14.2.2 工廠方法模式(Factory Method)的說明
參與者的說明以下:
Product(產品類) 定義產品類的操做接口,而這個產品將由工廠產生
ConcreteProduct(產品實現) 實現產品功能的類,能夠不僅定義一個產品實現類,這些產品實現類的對象都會由ConcreteCreator(工廠實現類)產生
Creator(工廠類) 定義能產生Product(產品類)的方法: FactoryMethod
ConcreteCreator(工廠實現類) 實現FactoryMethod, 併產生指定的ConcreteProduct(產品實現)
14.2.3 工廠方法模式(Factory Method)的實現範例
在實現工廠方法模式的選擇上並不是是固定的,而是按照程序語言設計的特性來決定有多少種實現方式.由於C#支持泛型程序設計,因此有4種實現方式
第一種方式: 有子類產生
public abstract class Creator { public abstract Product FactoryMethod(); } public abstract class Product { } public class ConcreteProductA: Product { public ConcreteProductA() { Debug.Log("生成對象類A"); } } public class ConcreteProductB: Product { public ConcreteProductB() { Debug.Log("生成對象類B"); } } public class ConcreteCreatorProductA: Creator { public ConcreteCreatorProductA() { Debug.Log("產生工廠:ConcreteCreatorProductA"); } public override Product FactoryMethod() { return new ConcreteProductA(); } } public class ConcreteCreatorProductB: Creator { public ConcreteCreatorProductB() { Debug.Log("產生工廠:ConcreteCreatorProductB"); } public override Product FactoryMethod() { return new ConcreteProductB(); } } void UnitTest() { Product theProduct = null; Creator theCreator = null; theCreator = new ConcreteCreatorProductA(); theProduct = theCreator.FactoryMethod(); theCreator = new ConcreateCreatorProductB(); theProduct = theCreator.FactoryMethod(); }
第二種方式: 在FactoryMethod增長參數
public abstract class Creator_MethodType { public abstract Product FactoryMethod(int Type); } public class ConcreteCreator_MethodType: Creator_MethodType { public ConcreteCreator_MethodType() { Debug.Log("產生工廠:ConcreteCreator_MethodType"); } public override Product FactoryMethod(int Type) { switch (Type) { case 1: return new ConcreteProductA(); break; case 2: return new ConcreateProducB(); break; default: Debug.Log("Type[" + Type +"]沒法產生對象"); break; } return null; } } void UntiTest() { Creator_MethodType theCreatorMethodType = new ConcreteCreator_MethodType(); theProduct = theCreatorMethodType.FactoryMethod(1); theProduct = theCreatorMethodType.FactoryMethod(2); }
第三種方式: Creator泛型類
public class Creator_GenericClass<T> where T: Product, new() { public Creator_GenericClass() { Debug.Log("產生工廠:Creator_GenericClass<" + typeof(T).ToString() + ">"); } public Product FactoryMethod() { return new T(); } } void UnitTest() { Creator_GenericClass<ConcreteProductA> Creator_ProductA = new Creator_GenericClass<ConcreteProductA>(); theProduct = Creator_ProductA.FactoryMethod(); Creator_GenericClass<ConcreteProductB> Creator_ProductB = new Creator_GenericClass<ConcreteProductB>(); theProduct = Creator_ProductB.FactoryMethod(); }
第四種方式: FactoryMethod泛型方法
interface Creator_GenericMethod { Product FactoryMethod<T>() where T: Product, new(); } public class ConcreteCreator_GenericMethod: Creator_GenericMethod { public ConcreteCreator_GenericMethod() { Debug.Log("產生工廠:ConcreteCreator_GenericMethod"); } public Product FactoryMethod<T>() where T: Product, new() { return new T(); } } void UnitTest() { Creator_GenericMethod theCreatorGM = new ConcreteCreator_GenericMethod(); theProduct = theCreatorGM.FactoryMethod<ConcreteProductA>(); theProduct = theCreatorGM.FactoryMethod<ConcreteProductB>(); }
14.3 使用工廠方法模式(Factory Method)產生角色對象
當類的對象產生時,若出現下列狀況:
須要複雜的流程
須要加載外部資源,如從網絡,存儲設備,數據庫
有對象上限
可重複使用
建議使用工廠方法模式來實現一個工廠類,而這個工廠類內還能夠搭配其餘的設計模式,讓對象的產生與管理更有效率
14.3.1 角色工廠類
參與者說明以下:
ICharacterFactory: 負責產生角色類ICharacter的工廠接口,並提供兩個工廠方法來產生不一樣陣營的角色對象: CharacterSoldier負責產生玩家陣營的角色對象;CharacterEnemy負責產生敵方陣營的角色對象
CharacterFactory: 繼承並實現ICharacter工廠接口的類,其中實現的工廠方法是實際產生對象的地方
ISoldier, SoldierCaption......: 由工廠類產生的"產品", 在《P級陣地》中爲玩家角色
IEnemy, EnemyElf......: 由工廠類產生的另外一項"產品",在《P級陣地》中爲敵方角色
14.3.2 實現說明
public abstract class ICharacterFactory { public abstract ISolder CreateSoldier(ENUM_Soldier emSoldier, ENUM_Weapon, emWeapon, int Lv, Vector3 SpwanPosition); public abstract IEnemy CreateEnemy(ENUM_Enemy emEnemy, ENUM_Weapon emWeapon, Vector3 SpawnPosition, Vector3 AttackPosition); } public class CharacterFactory: ICharacterFactory { public override ISoldier CreateSoldier(ENUM_Soldier emSoldier, ENUM_Weapon emWeapon, int Lv, Vector3 SpawnPosition) { ISoldier theSoldier = null; switch (emSoldier) { case ENUM_Soldier.Rookie: theSoldier = new SoldierRookie(); break; ... } theSoldier.SetGameObejct(go); theSoldier.SetWeapon(weapn); theSoldier.Set...(); return theSoldier; } ... }
14.3.3 使用工廠方法模式(Factory Method)的優勢
角色工廠類CharacterFactory將"角色類羣組"產生對象的實現,都整合到兩個工廠方法下,並將有關的程序從客戶端刪除,同時下降了客戶端與"角色產生過程"的耦合度(或成爲依賴度).此外,角色生成後的後續設置功能(給武器,設置屬性,設置AI等),也都在同一個地方實現,讓開發人員能快速瞭解類之間的關聯性及設置的前後順序
14.3.4 工廠方法模式(Factory Method)的實現說明
public static class PBDFactory { private static bool m_bLoadFromResource = true; private static ICharacterFactory m_CharacterFactory = null ... public static IAssetFactory GetAssetFactory() { } public static ICharacterFactory GetCharacterFactory() { } public static IWeaponFactory GetWeaponFactory() { } public static IAttrFactory GetAttrFactory() { } } public class TrainSoldierExecute { ... public void Action(TrainSoldierCommand Command) { ICharacterFactory Factory = PBDFactory.GetCharacterFactory(); ... } }
14.4 工廠方法模式(Factory Method)面對變化時
14.5 結論
工廠方法模式的優勢是,將類羣組對象的產生流程整合於同一個類下實現,並提供惟一的工廠方法,讓項目內的"對象產生流程"更加獨立.不過,當類羣組過多時,不管使用哪一種方式,都會出現工廠子類爆量或switch case 語句過長的問題,這是美中不足的地方
與其餘模式的合做
角色工廠(CharacterFactory)中,產生不一樣陣營的角色時,會搭配建造者模式(Builder)的需求,將須要的參數設置給各角色的建造者
本地資源加載工廠(ResourceAssetFactory)若同時要求系統性能的優化,可以使用代理者模式來優化本地加載性能
屬性產生工廠(AttrFactory)可以使用享元模式(Flyweight)來減小重複對象的產生
其餘應用方式
就如同本章的重點,若是系統實現人員想要將對象的產生及相關的初始化工做集中在一個地方完成,那麼均可以使用工廠方法模式來完成,換句話說,就是工廠方法模式的應用層面很是普遍
第15章 角色的組裝----建造者模式(Builder)
15.1 角色功能的組裝
public class CharacterFactory: ICharacterFactory { public override ISoldier CreateSoldier(ENUM_Soldier emSoldier, ENUM_Weapon emWeapon, int Lv, Vector3 SpawnPosition) { ISoldier theSoldier = null; switch (emSoldier) { case ENUM_Soldier.Rookie: theSoldier = new SoldierRookie(); break; ... } theSoldier.SetGameObject(...); theSoldier.SetWeapon(Weapon); ... } }
15.2 建造者模式(Builder)
工廠類是將生產對象的地點所有集中到一個地點來管理,可是如何在生產對象的過程當中,可以更有效率而且更具彈性,則須要搭配其餘的設計模式.建造者模式就是經常使用來搭配使用的模式之一
15.2.1 建造者模式(Builder)的定義
在GoF中對建造者模式的定義是:
"將一個複雜對象的構建流程與它的對象表現分離出來,讓相同的構建流程能夠產生不一樣的對象行爲表現"
15.2.2 建造者模式(Builder)的說明
參與者的說明以下:
Director(建造指示者)
負責對象構建時的"流程分析安排"
在Constructor方法中,會明肯定義對象組裝的流程,即調用Builder接口方法的順序
Builder(功能實現者接口)
定義不一樣的操做方法將"功能分開來實現"
其中的每個方法都是用來提供給某複雜對象的一部分功能,或是提供設置規則
ConcreteBuilder(功能實現者)
Builder的具體實現,實現產出功能的類
不一樣的ConcreteBuilder(功能實現者)能夠產出不一樣的功能,用來實現不一樣對象的行爲表現和功能
Product(產品)
表明最終完成的複雜對象,必須提供方法讓Builder類能夠將各部位功能設置給它
15.2.3 建造者模式(Builder)的實現範例
public class Product { private List<string> m_Part = new List<string.(); public Product() { } public void AddPart(string Part) { m_Part.Add(part); } public void ShowProduct() { Debug.Log(Part); } } public class Director { private Product m_Product; public Director() { } public void Construct(Builder theBuilder) { m_Product = new Product(); theBuilder.BuildPart1(m_Product); theBuilder.BuildPart2(m_Product); } public Product GetResult() { return m_Product; } } public abstract class Builder { public abstract void BuildPart1(Product theProduct); public abstract void BuildPart2(Product theProduct); } public class ConcreteBuilderA: Builder { public override void BuildPart1(Product theProduct) { theProduct.AddPart("ConcreteBuilderA_Part1"); } public override void BuildPart2(Product theProduct) { theProduct.AddPart("ConcreteBuilderA_Part2"); } } public class ConcreteBuilderB: Builder { public override void BuildPart1(Product theProduct) { theProduct.AddPart("ConcreteBuilderB_Part1"); } public override void BuildPart2(Product theProduct) { theProduct.AddPart("ConcreteBuilderB_Part2"); } } void UnitTest() { Director theDirector = new Director(); Product theProduct = null; theDirector.Construct(new ConcreteBuilderA()); theProduct = theDirector.GetResult(); theProduct.ShowProduct(); theDirector.Construct(new ConcreteBuilderB()); theProduct = theDirector.GetResult(); theProduct.ShowProduct(); }
15.3 使用建造者模式(Builder)組裝角色的各項功能
角色的組裝算是遊戲實現上最複雜的功能之一.每款遊戲遇到這個部分時,都要針對程序代碼不斷地重構,調整,修正,防呆......, 緣由是"角色"是遊戲的賣點之一.
15.3.1 角色功能的組裝
參與者的說明以下:
CharacterBuilderSystem: 角色建造者系統負責《P級陣地》中雙方角色構建時的裝配流程.它是一個"IGameSystem遊戲系統",由於角色構建完成後,還須要通知其餘遊戲系統,因此將其加入已經具備中介者模式(Mediator)的PBaseDefenseGame類中,方便與其餘遊戲功能溝通
ICharacterBuilder: 定義遊戲角色功能的組裝方法,包含3D模型,武器,屬性,AI等功能
SoldierBuilder: 負責玩家陣營角色功能的產生並設置給玩家角色
EnemyBuilder: 負責地方陣營角色功能的產生並設置給敵方角色
15.3.2 實現說明
public abstract class ICharacterBuildParam { public ENUM_Weapon emWeapon = ENUM_Weapon.NULL; public ICharacter NewCharacter = null; public Vector3 SpawnPosition; public int AttrID; public string AssetName; public string IconSpriteName; } public abstract class ICharacterBuilder { public abstract void SetBuildParam(ICharacterBuildParam theParam); public abstract void LoadAsset(int GameObjectID); public abstract void AddOnClickScript(); public abstract void AddWeapon(); public abstract void AddAI(); public abstract void SetCharacterAttr(); public abstract void AddCharacterSystem(PBaseDefenseGame PBDGame); } public class SoldierBuilderParam: ICharacterBuildParam { public int Lv = 0; public SoldierBuildParam() { } } public class SoldierBuilder: ICharacterBuilder { private SoldierBuildParam m_BuilderParam = null; public override void SetBuildParam(ICharacterBuildParam theParam) { m_BuildParam = theParam as SoldierBuildParam; } public override void LoadAsset() { } public override void AddOnClickScript() { } public override void AddWeapon() { } public override void SetCharacterAttr() { } public override void AddAI() { } public override void AddCharacterSystem() { } } public class EnemyBuildParam: ICharacterBuildParam { public Vector3 AttackPosition = Vector3.zero; public EnemyBuildParam() { } } public class EnemyBuilder: ICharacterBuilder { private EnemyBuildParam m_BuildParam = null; public override void SetBuildParam(ICharacterBuildParam theParam) { m_BuildParam = theParam as EnemyBuildParam; } public override void LoadAsset() { } public override void AddOnClickScript() { } public override void AddWeapon() { } public override void SetCharacterAttr() { } public override void AddAI() { } public override void AddCharacterSystem() { } } public class CharacterFactory: ICharacterFactory { private CharacterBuilderSystem m_BuilderDirector = new CharacterBuilderSystem(PBaseDefenseGame.Instance); public override ISoldier CreateSoldier() { SoldierBuildParam SoldierParam = new SoldierBuildParam(); SoldierParam.NewCharacter = new SoldierRookie(); SoldierParam.emWeapon = emWeapon; SoldierParam.SpanPosition = SpawnPosition; SoldierParam.Lv = Lv; SoldierBuider theSoldierBuilder = new SoldierBuilder(); theSoldierBuider.SetBuilderParam(SoldierParam); m_BuilderDirector.Construct(theSoldierBuilder); return SoldierParam.NewCharacter as ISoldier; } public override IEnemy CreateEnemy() { ... } }
15.3.3 使用建造者模式(Builder)的優勢
在重構後的角色工廠中,之簡單負責角色的"產生",而複雜的功能組裝工做則交由新增長的角色建造者系統來完成.運用建造者模式的角色建造者系統,將角色功能的"組裝流程"給獨立出來,並以明確的方法調用來實現,這有助於程序代碼的閱讀和維護.而各個角色的功能裝備任務,也交由不一樣的類來實現,並使用接口方法操做,將系統之間的耦合度(即依賴度)下降.因此當實現系統有任何變化時,也能夠使用替換實現類的方式來應對
15.3.4 角色建造者的執行流程
15.4 建造者模式(Builder)面對變化時
15.5 結論
建造者模式的優勢是,能將複雜對象的"產生流程"與"功能實現"拆分後, 讓系統調整和維護變得更容易.此外,在不須要更新實現者的狀況下,調整產生流程的順序就能完成裝備線的更改,這也是建造者模式的另外一優勢
與其餘模式的合做
建造者模式在實現過程當中, 大多利用《P級陣地》的工廠類獲取所需的功能組件,而這兩種生成模式的相互配合,也是章範例的重點之一
其餘應用方式
在奇幻類型的角色扮演遊戲中,設計者爲了增長法術系統的聲光效果,在施展法術時,大多會分紅不一樣的段落來呈現法術特效.例如發射箭的法術吟唱特效,發射時的特效,法術在進行時的特效,擊中對手時的特效,對手被打中時的特效,最後消失時的特效.有時爲了執行性能的考慮,會在施展法術時,就將全部特效所有準備完成.這個時候就能夠利用建造者模式將全部特效組裝完成
遊戲的用戶界面就如同通常的網頁或App,有時也會有複雜的版面配置和信息顯示.利用建造者模式能夠將界面的呈現,分紅不一樣的區域或內容來實現,讓界面也能夠有"功能裝組"的應用方式
第16章 遊戲屬性管理功能----享元模式(Flyweight)
16.1 遊戲屬性的管理
public abstract class ICharacterAttr { protected int m_MaxHP = 0; ... } public class SoldierAttr: ICharacterAttr { protected int m_SoldierLv = 0; ... } public class EnemyAttr: ICharacterAttr { protected int m_CritRate = 0; ... } public class WeaponAttr { protected int m_Atk = 0; protected float m_Range = 0.0f; public WeaponAttr(int AtkValue, float Range) { m_Atk = AtkValue; m_Range = Range; } public virtual int GetAtkValue() { return m_Atk; } public virtual float GetAtkRange() { return m_Range; } } public abstract class IWeapon { protected int m_AtkPlusValue = 0; protected WeaponAttr m_WeponAttr = nul; ... public void SetWeaponAttr(WeaponAttr theWeaponAttr) { m_WeaponAttr = theWeaponAttr; } public vodi SetAtkPlusValue(int Value) { m_AtkPlusValue = Value; } public int GetAtkValue() { return m_WeaponAttr.GetAtkValue() + m_AtkPlusValue; } public float GetAtkRange() { return m_WeaponAttr.GetAtkRange(); } ... }
16.2 享元模式(Flyweight)
享元模式是用來解決"大量且重複的對象"的管理問題,尤爲是程序設計師最常忽略的"雖然小但卻大量重複的對象". 隨着計算機設備的升級,程序設計師漸漸遺忘了在內存受限制環境下,對每個字節都很計較的程序編寫方式.但近幾年來,因爲移動設備App的興起,有大小限制的內存環境又成爲設計師必須考慮的設計條件之一,善用享元模式(Flyweight)能夠解決大部分對象共享的問題
16.2.1 享元模式(Flyweight)的定義
GoF中享元模式(Flyweight)的定義是:
"使用共享的方式,讓一大羣小規模對象能更有效地運行"
定義中的兩個重點: "共享"與"一大羣小規模的對象"
首先,"一大羣小規模對象"指的是: 雖然有時候類的組成很簡單,可能只有幾個類型爲int的類成員,但若是這些類成員的屬性是相同並且能夠共享的,那麼當系統產生了一大羣類的對象時,這些重複的部分就都是浪費的,由於它們只須要存在一份便可
而"共享"指的是使用"管理結構"來設計信息的存取方式,讓能夠被共享的信息,只須要產生一份對象,而這個對象可以被引用到其餘對象中
但必須注意的是,既然能夠被多個對象"共享",那麼對於共享對象的"修改"就必須加以限制,由於被多個對象共享以後,任何更改共享對象中的屬性,均可能致使其餘引用對象的錯誤
所以在設計上,對象中那些"只能讀取而不能寫入"的共享部分被稱爲"內在(intrinsic)狀態",就如前一節中提到的最大生命力(MaxHP),移動速度(MoveSpeed),攻擊力(Atk),攻擊距離(Range)這些值.而對象中"不能被共享"的部分,如當前的生命力(NowHP),等級(LV),暴擊率(CritRate)等,這些屬性會隨着遊戲運行的過程而變化,則稱爲"外在(extrinsic)狀態".
享元模式提供的解決方案是: 產生對象時,將可以共享的"內在(intrinsic)狀態"加以管理,而且將屬於各對象能自由更改的"外部(extrinsic)狀態」也一塊兒設置給新產生的對象中
16.2.2 享元模式(Flyweight)的說明
GoF參與者的說明以下:
FlyweightFactory(工廠類)
負責產生和管理Flyweight的組件
內部一般使用容器類來存儲共享的Flyweight組件
提供工廠方法產生對應的組件,當產生的是共享組件時,就加入到Flyweight管理容器內
Flyweight (組件接口)
定義組件的操做接口
ConcreteFlyweight (能夠共享的組件)
實現Flyweight接口
產生的組件是能夠共享的,並加入到Flyweight管理器中
UnsharedConcreteFlyweight (不能夠共享的組件)
實現Flyweight接口,也能夠選擇不繼承自Flyweight接口
能夠定義爲單獨的組件,不包含任何共享資源
也能夠將一些共享組件定義爲類的成員,稱爲內部狀態;並另外定義其餘不被共享的成員,做爲外部狀態使用
16.2.3 享元模式(Flyweight)的實現範例
public abstract class Flyweight { protected string m_Content; public Flyweight() { } public Flyweight(string Content) { m_Content = Content; } public string GetContent() { return m_Content; } public abstract void Operator(); } public class ConcreteFlyweight: Flyweight { public ConcreteFLyweight(string Content): base(Content) { } public override void Operator() { Debug.Log("ConcreteFlyweight.Content[" + m_Content + "]"); } } public class UnsharedConcreteFlyweight //: Flyweight { Flyweight m_Flyweight = null; string m_UnsharedContent; public UnsharedConcreteFlyweight(string Content) { m_UnsharedContent = Content; } public void SetFlyweight(Flyweight theFlyweight) { m_Flyweight = theFlyweight; } public void Operator() { string Msg = string.Format("UnsharedConcreteFlyweight.Content [{0}]", m_UnsharedContent); if (m_Flyweight != null) { Msg += "包含了: " + m_Flyweight.GetContent(); } Debug.Log(Msg); } } public class FlyweightFactory { Dictionary<string, Flyweight> m_Flyweight = new Dictionary<string, Flyweight>(); public Flyweight GetFlyweight(string Key, string Content) { if (m_Flyweights.ContainsKey(Key)) { return m_Flyweights[Key]; } ConcreteFlyweight theFlyweight = new ConcreteFlyweight(Content); m_Flyweights[Key] = theFlyweight; Debug.Log("New ConcreteFlyweight Key[" + Key + "] Content [" +Content + "]"); return theFlyweight; } public UnsharedConcreteFlyweight GetUnsharedFlyweight(string Content) { return new UnsharedConcreteFlyweight(Content); } public UnsharedConcreteFlyweight GetUnsharedFlyweight(string Key, string SharedContent, string UnsharedContent) { Flyweight SharedFlyweight = GetFlyweight(Key, SharedContent); UnsharedConcreteFlyweight theFlyweight = new UnsharedConcreteFlyweight(UnsharedContent); theFlyweight.SetFlyweight(SharedFlyweight); return theFlyweight; } } void UnitTest() { FlyweightFactory theFactory = new FlyweightFactory(); theFactory.GetFlyweight("1", "共享組件1"); theFactory.GetFlyweight("2", "共享組件2"); theFactory.GetFlyweight("3", "共享組件3"); Flyweight theFlyweight = theFactory.GetFlyweight("1", ""); theFlyweight.Operator(); UnsharedConcreteFlyweight theUnshared1 = theFactory.GetUnsahredFlyweight("不共享的信息1"); theUnshared1.Operator(); theUnshared1.SetFlyweight(theFlyweight); UnsharedConcreteFlyweight theUnshared2 = theFactory.GetUnsharedFlyweight("1", "", "不共享的信息2"); theUnshared1.Operator(); theUnshared2.Operator(); }
16.3 使用享元模式(Flyweight)實現遊戲
16.3.1 SceneState的實現
16.3.2 實現說明
public abstract class ICharacterAttr { protected BaseAttr m_BaseAttr = null; ... } public class SoldierAttr: ICharacterAttr { public void SetSolderAttr(BaseAttr BaseAttr) { } ... } public class EnemyAttr: ICharacterAttr { ... } public class AttrFactory: IAttrFactory { private Dictionary<int, BaseAttr> m_SoldierAttrDB = null; private Dictionary<int, EnemyBaseAttr> m_EnemyAttrDB = null; private Dictionary<int, WeaponAttr> m_WeaponAttrDB = null; ... }
16.3.3 使用享元模式(Flyweight)的優勢
新版運用享元模式的屬性工廠AttrFactory, 將屬性設置集以更簡短的格式呈現,免去了使用switch case的一長串語句,方便企劃人員閱讀和設置.此外,由於共享屬性的部分(BaseAttr),每個編號對應的屬性對象,在整個遊戲執行中只會產生一份,不像舊方法那樣會產生重複的對象而增長內存的負擔,對於遊戲性能有所提高
16.3.4 享元模式(Flyweight)的實現說明
就筆者的經驗來講,享元模式在遊戲開發領域中,最常被應用到的地方就是屬性系統.每一款遊戲不管規模大小,都須要屬性系統協助調整遊戲平衡,如角色等級屬性,裝備屬性,武器屬性,寵物屬性,道具屬性等,而每一種屬性設置數據又可能多達上百或上千之多,當這些屬性設置都成爲對象並存在遊戲之中時,即符合了享元模式定義中所說的"一大羣小規模對象",每一項屬性可能只包含3,4個字段,也可能包含多達數十個字段,若不採用"共享」的方式管理,很容易形成系統的問題和實現上的困難,應用上也會產生相關的問題.
16.4 享元模式(Flyweight)面對變化時
16.5 結論
將有可能散步在程序代碼中,零碎的遊戲屬性對象進行統一管理,是享元模式應用在遊戲開發領域帶來的好處之一
與其餘模式合做
每個陣營的角色建造函數,在設置武器屬性和角色屬性時,都會經過屬性工廠來獲取屬性,而這些屬性則是使用享元模式產生的,在遊戲的執行過程當中只會存在一份
其餘應用方式
在射擊遊戲中,畫面上出現的子彈或導彈,大多會使用"對象"的方式來表明.而爲了讓遊戲系統可以有效地產生和管理這些對象,導彈對象,可以使用享元模式來創建子彈對象池,讓其餘遊戲對象也使用對象池內的子彈,減小由於重複處理產生子彈對象,刪除子彈對象所致使的性能損失
第17章 Unity3D的界面設計----組合模式(Composite)
17.1 玩家界面設計
17.2 組合模式(Composite)
17.2.1 組合模式(Composite)的定義
17.2.2 組合模式(Composite)的說明
17.2.3 組合模式(Composite)的實現範例
17.2.4 分了兩個子類可是要使用同一個操做界面
17.3 Unity3D遊戲對象的分層式管理功能
17.3.1 遊戲對象的分層管理
17.3.2 正確有效地獲取UI的遊戲對象
17.3.3 遊戲用戶界面的實現
17.3.4 兵營界面的實現
17.4 結論
第18章 兵營系統及兵營信息顯示
18.1 兵營系統
18.2 兵營系統的組成
18.3 初始兵營系統
18.4 兵營信息的顯示流程
第19章 兵營訓練單位----命令模式(Command)
19.1 兵營界面上的命令
public class CampInfoUI: IUserInterface { public override void Initialize() { m_levelUpBtn.onClick.AddListener(()=>OnLevelUpBtnClick()); ... } private void OnLevelUpBtnClick() { } ... }
19.2 命令模式(Command)
在本節使用軟件開發做爲範例說明設計模式以前,咱們先舉個較爲生活化的例子來講明命令模式.例如,在餐廳用餐就是命令模式的一種表現,當餐廳的前臺服務人員接收到客人的點餐以後,就會將餐點內容記載在點餐單(命令)上,這張點餐單(命令)就會隨着其餘客人的點餐單(命令)一塊兒排入廚房的"待作"列表(命令管理器)內.廚房內的廚師(功能提供者)根據先到先作的原則,將點餐單(命令)上的內容一個個製做(執行)出來.固然,若是餐廳不計較的話,那麼等待好久的客人,也能夠選擇不繼續等待(取消命令),改去其餘餐廳用餐
19.2.1 命令模式(Command)的定義
GoF對於命令模式(Command)的定義以下:
"將請求封裝成爲對象,讓你能夠將客戶端的不一樣請求參數化,並配合隊列,記錄,復原等方法來執行請求的操做."
上述定義能夠簡單分紅兩部分來看待:
請求的封裝
請求的操做
請求的封裝
所謂的請求,簡單來講就是某個客戶端組件,想要調用執行某種功能,而這個某種功能是被實如今某個類中.
請求的操做
當請求能夠被封裝成一個對象時,那麼這個請求對象就能夠被操做,例如:
存儲: 能夠將請求對象放入一個數據結構中進行排序,排對,搬移,刪除,暫緩執行等操做
記錄: 當某一個請求對象被執行後,能夠先不刪除,將其移入"已執行"數據容器內,經過查看"已執行"數據容器的內容,就能夠知道系統過去執行命令的流程和軌跡
復原: 延續上一項記錄功能,若系統針對每項請i去命令實現了"反向"操做時,能夠將已執行的請求復原,這在大部分的文字編輯軟件和繪圖編輯軟件中是很常見的
19.2.2 命令模式(Command)的說明
GoF參與者的說明以下:
Command(命令界面): 定義命令封裝後具有的操做界面
ConcreteCommand(命令實現): 實現命令封裝和界面,會包含每個命令的參數和Receiver(功能執行者)
Receiver(功能執行者): 被封裝在ConcreteCommand(命令實現)類中,真正執行功能的類對象
Client(客戶端/命令發起者): 產生命令的客戶端,能夠視狀況設置命令給Receiver(功能執行者)
Invoker(命令管理者): 命令對象的管理容器或是管理類,並負責要求每一個Command執行其功能
19.2.3 命令模式(Command)的實現範例
public class Receiver1 { public Receiver1() { } public void Action(string Command) { Debug.Log("Receiver1.Action:Command[" + Command +"]"); } } public class Receiver2 { public Receiver2() { } public void Action(int Param) { Debug.Log("Receiver2.Action:Param[" + Param.ToString() + "]"); } } public abstract class Command { public abstract void Execute(); } public class ConcreteCommand1: Command { Receiver1 m_Receiver = null; string m_Command = ""; public ConcreteCommand1(Receiver1 Receiver, string Command) { m_Receiver = Receiver; m_Command = Command; } public override void Execute() { m_Receiver.Action(m_Command); } } public class ConcreteCommand2: Command { Receiver2 m_Receiver = null; int m_Param = 0; public ConcreteCommand2(Receiver2 Receiver, int Param) { m_Receiver = Receiver; m_Param = Param; } public override void Execute() { m_Receiver.Action(m_Param); } } public class Invoker { List<Command> m_Commands = new List<Command>(); public void AddCommand(Command theCommand) { m_Commands.Add(theCOmmand); } public void ExecuteCommand() { foreach (Command theCommand in m_Commands) { theCommand.Execute(); } m_Commands.Clear(); } } void UnitTest() { Invoker theInvoker = new Invoker(); Command theCommand = null; theCommand = new ConcreteCommand1(new Receiver(), "你好"); theInvoker.AddCommand(theCommand); theCommand = new ConcreteCommand2(new Receiver2(), 999); theInvoker.AddCommand(theCommand); theInvoker.ExecuteCommand(); }
上述範例看似頗爲簡單,也正由於如此,讓命令模式在實現上的彈性很是大,也出現許多變化的形式.在實際分析時,能夠着重在"命令對象"的"操做行爲"加以分析:
若是但願讓"命令對象"能包含最多可能的執行方法數量,那麼就增強在命令類羣組的設計分析.以餐廳點餐的例子來看,就是要思考,是否將餐點與飲料的點餐單合併爲一張
若是但願能讓命令能夠任意地執行和撤銷,那麼就須要着重在命令管理者(Invoker)的設計實現上.以餐廳點餐的例子來看,就是要思考這些點餐單是要用人工管理仍是要使用計算機系統來輔助管理
此外,若是讓命令具有任意撤銷或不執行的功能,那麼系統對於命令的"反向操做"的定義也必須加以實現,或者將反向操做的執行參數,也一併封裝在命令類中
19.3 使用命令模式(Command)實現兵營訓練角色
19.3.1 訓練命令的實現
參與者的說明以下:
ITrainCommand: 訓練命令界面, 定義了《P級陣地》中訓練一個做戰單位應有的命令格式和執行方法
TrainSoldierCommand: 封裝訓練玩家角色的命令,將要訓練角色的參數定義爲成員,並在執行時調用"功能執行類"去執行指定的命令
ICharacterFactory: 角色工廠,實際產生角色單位的"功能執行類"
ICamp: 兵營界面,包含"管理訓練做戰單位的命令"的功能,即擔任Invoker(命令管理者)的角色,使用泛型來暫存全部的訓練命令,而且使用相關的操做方法來添加,刪除訓練命令
SoldierCamp: Soldier兵營界面,負責玩家角色的做戰單位訓練.當收到訓練命令時,會產生命令對象,並按照當前兵營的狀態來設置命令對象的參數,最後使用ICamp類提供的界面,將命令加入管理器內
19.3.2 實現說明
public abstract class ITrainCommand { public abstract void Execute(); } public class TrainSoldierCommand: ITrainCommand { ENUM_Soldier m_emSoldier; ENUM_Weapon m_wemWeapon; int m_Lv; Vector3 m_Position; public override void Execute() { ICharacterFacotry factory = PBDFactory.GetCharacterFactory(); ISoldier soldier = factory.CreateSolider(m_emSoldier, m_emWeapon, m_Lv, m_Position); } } public abstract class ICamp { protected List<ITrainCommand> m_TrainCommands = new List<ITrainCommand>(); protected void AddTrainCommand() { } public void RunCommand() { } } public class CampSystem: IGameSystem { private Dictionary<ENUM_Solder, ICamp> m_SoldierCamps = new Dictionary<ENUM_Soldier, ICamp>(); } public class SoldierCamp: ICamp { public override void Train() { TrainSoldierCommand NewCommand = new TrainSoldierCommand(m_emSoldier, m_emWeapon, m_Lv, m_Position); AddTrainCommand(NewCommand); } } public class CampInfoUI: IUserInterface { private void OnTrainBtnClick() { m_camp.Train(); } }
19.3.3 執行流程
19.3.4 實現命令模式(Command)時的注意事項
命令模式實現上的選擇
《P級陣地》的兵營界面上,除了與訓練單位有關的兩個命令(訓練,取消訓練)以外,另外還有兩個與升級有關的命令按鈕(兵營升級,武器升級).但針對這兩個界面命令,《P級陣地》並無運用命令模式來實現
public class CampInfoUI: IUserInterface { private void OnLevelUpBtnClick() { m_Camp.LevelUp(); } private void OnWeaponLevelUpBtnClick() { m_Camp.WeaponLevelUp(); } } public class SoldierCamp: ICamp { public override void LevelUp() { } public override void WeaponLevelUp() { } }
不運用命令模式的主要緣由在於:
類過多
若是遊戲的每個功能請求都運用命令模式,那麼就有能會出現類過多的問題.每個命令都將產生一個類來負責封裝,大量的類會形成項目不易維護
請求對象並不須要被管理
指的是兵營升級和武器升級兩個命令,在執行上並無任何延遲或須要被暫存的需求,也就是當請求發出時,功能就要被當即執行.所以,在實現上,只要經過界面類提供的方法(ICamp.LevelUp,ICamp.WeaponLevelUp)來執行便可,讓功能的實現類(SoldierCamp)與客戶端(CampInfoUI)分離,就能夠達成這些功能的設計目標了
所以,在《P級陣地》中,選擇實現命令模式(Command)的標準在於:
」當請求被對象化後,對於請求對象是否有'管理'上的需求.若是有,則以命令模式實現"
須要實現大量的請求命令時
一箇中小型規模的多人在線遊戲,Server與Client之間的請求命令可能多達上千個,若每個請求命令都需產生類的話,那麼就真的會發生"類過多"的問題.爲了不這樣的問題發生,能夠改用下列的方式來實現
1. 使用註冊回調函數
一樣將全部的命令以管理容器組織起來,並針對每個命令,註冊一個回調函數,並將功能執行者(Receiver)改成一個函數/方法,而非對象.最後,將多個相同功能的回調函數以一個類封裝在一塊兒
2. 使用泛型程序設計
將命令界面以泛型方式來設計,將功能執行者(Receiver)定義爲泛型類,命令執行時調用泛型類中的固定方法.但以這種方式實現時,限制會比較大,必須限定每一個命令能夠封裝的參數數量,並且封裝參數的名稱比較不直觀,也就是將參數以Param1, Param2的方式命名
由於固定調用功能執行者(Receiver)中的某一個方法,因此方法名稱會固定,比較不容易與實際功能聯想
話雖如此,但若是系統中的每一個命令都很"單純"時,使用泛型程序設計能夠省去重複定義類或回調函數的麻煩
19.4 命令模式(Command)面對變化時
private void OnAddSolider() { TrainSoldierCommand NewCommand = new TrainSoldierCommand(emSolider, empWeapon, Lv, Position); NewCommand.Execute(); }
19.5 結論
命令模式的優勢是,將請求命令封裝成對象後,對於命令的執行,可加上額外的操做和參數化.但由於命令模式的應用普遍,在分析時須要針對系統需求加以分析,以免產生過多的命令類
其餘應用方式
實現網絡在線型遊戲時,對於Client/Server間數據封包的傳遞,大多數會使用命令模式來實現.但對於數據封包命令的管理,可能不會實現撤銷操做,通常比較側重於執行和記錄上.而"記錄"則是網絡在線型遊戲的另外一個重點,經過記錄,能夠分析玩家與遊戲服務器之間的互動,瞭解玩家在操做遊戲時的行爲,另外也有防黑客預警的做用
第20章 關卡設計----責任鏈模式(Chain of Responsibility)
20.1 關卡設計
public class StageSystem: IGameSystem { private int m_NowStageLv = 1; public override void Update() { // 是否要開啓新關卡 if (m_bCreateStage) { CreateStage(); m_bCreateStage = false; } // 是否要切換下一個關卡 if (m_PBDGame.GetEnemyCount() == 0) { if (CheckNextStage()) { m_NowStageLv++; } m_bCreateStage = true; } } // 產生關卡 private void CreateStage() { } // 確認是否要切換到下一個關卡 private bool CheckNextStage() { } // 獲取出生點 private Vector3 GetSpawnPosition() { } // 獲取攻擊點 private Vector3 GetAttackPosition() { } }
按期更新Update方法中,判斷當前是否須要產生新的關卡,若是須要,則先調用產生關卡CreateStage方法.而關卡是否結束,則是直接判斷當前敵方陣營的角色數量,若是爲0,表明關卡結束能夠進入下一個關卡.確認開始下一個關卡CheckNextStage方法中,會先判斷當前得分來判斷是否能夠進入下一個關卡
仔細分析兩個與關卡產生有關的方法: CreateStage, CheckNextStage, 其中都按照當前關卡(m_NowStageLv)的值,來決定接下來要產生哪些地方角色以及是否切換到下一個關卡.上述的程序代碼只產生了3個關卡而已,但《P級陣地》的目標是但願能設置數十個以上的關卡讓玩家挑戰,因此若以上述的寫法來設計的話,程序代碼將變得很是冗長,並且彈性不足,沒法讓企劃人員快速設置和調整,因此咱們須要使用新的設計來從新編寫程序
重構的目標是,但願能將關卡數據使用類加以封裝.而封裝的信息包含: 要出場的敵方角色的設置,通關條件,下一關的記錄等.也就是讓每一關都是一個對象並加以管理.而關卡系統則是在這羣對象中尋找"條件符合"的關卡,讓玩家進入挑戰.等到關卡完成後,再進入到下一個條件符合的關卡
20.2 責任鏈模式(Chain of Responsibility)
當有問題須要解決,並且能夠解決問題的人還不止一個時,就有不少方式能夠獲得想要的答案.例如能夠將問題同時交給能夠解決問題的人,請他們都回答,但這個方式比較浪費資源,也會形成浪費,也有可能回答的人有等級之分,不適合太簡單和太複雜的問題.另外一個方式就是將能夠回答問題的人,按照等級或專業一個個串接起來.責任鏈模式(Chain of Responsibility)就是提供了一個能夠將這些回答問題的人,一個個連接起來的設計方法
20.2.1 責任鏈模式(Chain of Responsibility)的定義
在運用責任鏈模式解決問題時,只要將下列的幾個重點列出來分別實現,便可知足模式的基本要求
能夠解決請求的接收者對象: 這些類對象可以瞭解"請求"信息的內容,並判斷自己可否解決
接收者對象間的串接: 利用一個串接機制,將每個可能能夠解決問題的接收者對象給串接起來,對於被串接的接收者對象來講,當自己沒法解決這個問題時,就利用這個串接機制,讓請求能不斷地傳遞下去;或使用其餘管理方式,讓接收者對象得以連接
請求自動轉移傳遞: 發出請求後,請求會自動往下轉移傳遞,過程之中,發送者不需特別轉換接口
20.2.2 責任鏈模式(Chain of Responsibility)的說明
GoF參與者的說明以下:
Handler(請求接收者接口):
定義能夠處理客戶端請求事項的接口
可包含「可連接下一個一樣能處理請求"的對象引用
ConcreteHandler1, ConcreteHandler2(實現請求接收者接口)
實現請求處理接口,並判斷對象自己是否能處理此次的請求
不能完成請求的話,交由後繼者(下一個)來處理
Client (請求發送者)
將請求發送給第一個接收者對象,並等待請求的回覆
20.2.3 責任鏈模式(Chain of Responsibility)的實現範例
public abstract class Handler { protected Handler m_NextHandler = null; public Handler(Handler theNextHandler) { m_NextHandler = theNextHandler; } public virtual void HandleRequest(int Cost) { if (m_NextHandler != null) { m_NextHandler.HandleRequest(Cost); } } } public class ConcreteHandler1: Handler { private int m_CostCheck = 10; public ConcreteHandler1(Handler theNextHandler): base(theNextHandler) { } public override void HandleRequest(int Cost) { if (Cost <= m_CostCheck) { Debug.Log("ConcreteHandler1.HandleRequest 覈准"); } else { base.HandleRequest(Cost); } } } public class ConcreteHandler2: Handler { private int m_CostCheck = 10; public ConcreteHandler2(Handler theNextHandler): base(theNextHandler) { } public override void HandleRequest(int Cost) { if (Cost <= m_CostCheck) { Debug.Log("ConcreteHandler2.HandleRequest 覈准"); } else { base.HandleRequest(Cost); } } } public class ConcreteHandler3: Handler { public ConcreteHandler3(Handler theNextHandler): base(theNextHandler) { } public override void HandleRequest(int Cost) { Debug.Log("ConcreteHandler1.HandleRequest 覈准"); } } void UnitTest() { ConcreteHandler3 theHandler3 = new ConcreteHandler3(null); ConcreteHandler2 theHandler2 = new ConcreteHandler2(theHandler3); ConcreteHandler1 theHandler1 = new ConcreteHandler1(theHandler2); theHandler1.HandleRequest(10); theHandler1.HandleRequest(15); theHandler1.HandleRequest(20); theHandler1.HandleRequest(30); theHandler1.HandleRequest(100); }
20.3 使用責任鏈模式(Chain of Responsibility)實現關卡系統
遊戲中的關卡都是一關關的串接,完成了這一關以後就進入下一關,因此在實現上使用責任鏈模式來串接每個關卡是很是合適的.可是對於每個關卡的通關判斷規則,則要按照各個遊戲的需求來設計.
20.3.1 關卡系統的設計
對於《P級陣地》關卡系統的修改需求上,關卡可能須要的信息包含要出場的敵方角色設置,通關條件及鏈接下一關卡對象的引用,封裝成一個"接收者類",並增長可以判斷通關與否的方法,做爲是否前進到下一關的判斷依據.
每一個關卡對象都會判斷"當前的遊戲狀態"是否符合關卡的"通關條件"
若是符合通關條件,則將關卡通關與否的判斷交由下一個關卡對象判斷,直到有一個關卡對象負責接下來的"關卡開啓"工做
若是不符合,則將"當前關卡"維持在這一個關卡對象上,繼續讓如今的關卡對象負責"關卡開啓"工做
參與者說明以下:
IStageHandler: 定義能夠處理"過關判斷"和"關卡開啓"的接口,也包含指向下一個關卡對象的應用
NormalStageHandler: 實現關卡接口, 負責"常規"關卡的開啓和過關條件判斷
IStageScore: 定義判斷通關與否的操做接口
StageScoreEnemyKilledCount: 使用當前的"擊殺敵方角色數",做爲通關與否的判斷
IStageData: 定義關卡內容的操做接口,在《P級陣地》中,關卡內容指的是:
這一關會出現攻擊玩家陣營的敵方角色數據之設置
關卡開啓
關卡是否結束的判斷
NormalStageData: 實現"常規"關卡內容,實際將產生的的敵方角色放入戰場上攻擊玩家陣營,以及實現判斷關卡是否結束的方法
此外,IStageScore和IStageData這兩個類也是應用策略模式(Strategy)的類,讓關卡系統在"過關判斷"和"產生敵方單位"這兩個設計需求上,能更具有靈活性,不限制只有一種玩法
20.3.2 實現說明
// 關卡接口 public abstract class IStageHandler { protected IStageData m_StageData = null; // 關卡的內容(敵方角色) protected IStageScore m_StageScore = null;// 關卡的分數(通關條件 protected IStageHandler m_NextHandler = null; // 下一關 // 設置下一個關卡 public IStageHandler SetNextHandler(IStageHandler NextHandler) { m_NextHandler = NextHandler; return m_NextHandler; } public abstract IStageHandler CheckStage(); public abstract void Update(); public abstract void Reset(); public abstract bool IsFinished(); } // 常規關卡 public class NormalStageHandler: IStageHandler { // 設置分數和關卡數據 public NormalStageHandler(IStageScore StageScore, IStageData StageData) { m_StageScore = StageScore; m_StageData = StageData; } // 設置下一個關卡 public IStageHandler SetNextHandler(IStageHandler NextHandler) { m_NextHandler = NextHandler; return m_NextHandler; } // 確認關卡 public override IStageHandler CheckStage() { // 分數是否足夠 if (m_StageScore.CheckScore() == false) { return this; } // 已是最後一關了 if (m_NextHandler == null) { return this; } // 確認下一個關卡 return m_NextHandler.CheckState(); } public override void Update() { m_stageData.Update(); } public override void Reset() { m_StageData.Reset(); } public override bool IsFinished() { return m_StageData.IsFinished(); } } // 關卡分數確認 public abstract class IStageScore { public abstract bool CheckScore(); } // 關卡分數確認:敵人陣亡數 public class StageScoreEnemyKilledCount: IStageScore { private int m_EnemyKilledCount = 0; private StageSystem m_StageSystem = null; public StageScoreEnemyKilledCount(int KilledCount, StageSystem theStageSystem) { m_EnemyKilledCount = KilledCount; m_StageSystem = theStageSystem; } public override bool CheckScore() { return (m_StageSystem.GetEnemyKilledCount() >= m_EnemyKilledCount); } } // 關卡內容接口 public abstract class IStageData { public abstract void Update(); public abstract bool IsFinished(); public abstract void Reset(); } public class NormalStageData: IStageData { private List<StageData> m_StageData = new List<StageData>(); class StageData { ... } public NormalStageData(float CoolDown, Vector3 SpawnPosition, Vector3 AttackPosition) { } public void AddStageData(ENUM_Enemy emEnemy, ENUM_Weapon emWeapon, int Count) { } public override void Reset() { } public override void Update() { ICharacterFactory Factory = PBDFactory.GetCharacterFactory(); Factory.CreateEnemy(theNewEnemy.emEnemy, theNewEnemy.emWeapon, m_SpawnPosition, m_AttackPosition); } } // 關卡控制系統 public class StageSystem: IGameSystem { ... private IStageHandler m_NowStageHandler = null; private IStageHandler m_RootStageHandler = null; public override void Initialize() { InitializeStageData(); m_NowStageHandler = m_RootStageHandler; m_NowStageLv = 1; } public override void Release() { } public override void Update() { // 更新當前的關卡 m_NowStageHandler.Update(); // 是否要切換下一個關卡 if (m_PBDGame.GetEnemyCount() == 0) { // 是否結束 if (m_NowStageHandler.IsFinished() == false) { return; } // 獲取下一關 IStageHandler NewStageData = m_NowStageHandler.CheckeStage(); // 是否爲舊的關卡 if (m_NowStageHandler = NewStageData) { m_NowStageHandler.Reset(); } else { m_NowStageHandler = NewStageData; } NotifyNewStage(); } } private void NotifyNewStage() { m_PBDGame.ShowGameMsg("新的關卡"); m_NowStageLv++; m_PBDGame.ShowNowStageLv(m_NowStageLv); m_PBDGame.UpdateSoldier(); m_PBDGame.NotifyGameEvent(ENUM_GameEvent.NewStage, null); } // 初始化全部關卡 private void InitializeStageData() { if (m_RootStageHandler != null) { return; } Vector3 AttackPosition = GetAttackPosition(); NormalStageData StageData = null; IStageScore StageScore = null; IStageHandler NewStage = null; // 第1關 StageData = new NormalIStageData(3f, GetSpawnPosition(), AttackPosition); StageData.AddStageData(ENUM_Enemy.Elf, ENUM_Weapon.Gun, 3); StageScore = new StageScoreEnemyKilledCount(3, this); NewStage = new NormalStageHandler(StageScore, StageData); // 設置爲起始關卡 m_RootStageHandler = NewStage; // 第2關 // 第10關
"關卡內容"通常指的就是玩家要挑戰的項目,這些項目多是出現3個敵方角色,讓玩家擊退;也多是出現3個道具讓玩家能夠去搜索獲取;或是設計特殊任務關卡讓玩家去完成.而這些設置內容都會放進IStageData的子類中,而且經過Game Loop更新機制,讓關卡內容能夠順利產生給玩家挑戰
修正的關鍵點在於
1. 按期更新方法中,將切換關卡的判斷交給一羣關卡對象串接起來的鏈表來負責,因此須要切換關卡時,詢問關卡對象鏈表,就能夠獲取當前能夠進行的關卡
2. 在初始化關卡系統時,將全部關卡的數據一次設置完成,包含關卡要出現的敵方角色等級,數量,武器等級,過關的判斷及鏈接的下一關
20.3.3 使用責任鏈模式(Chain of Responsibility)的優勢
將舊方法中的CreateStage,CheckNextStage兩個方法的內容,使用關卡對象來代替,這樣一來,本來可能出現的冗長式寫法就得到了改善.而且將關卡內容(IStageData),過關條件(IStageScore)類化,可以使得《P級陣地》中關卡的類型有多種形式的組合.而關卡設計的數據未來也能夠搭配"企劃工具"來設置,增長關卡設計人員的調整靈活度
20.3.4 實現責任鏈模式(Chain of Responsibility)時的注意事項
不用從頭判斷
使用泛型容器來管理關卡對象
public class StageSystem: IGameSystem { private List<IStageHandler> m_StageHandlers; private int m_NowStageLv = 1; ... }
20.4 責任鏈模式(Chain of Responsibility)面對變化時
public abstract class IStageHandler { ... public abstract int LoseHeart(); } public class NormalStageHandler: IStageHandler { ... public override int LoseHeard() { return 1; } } public class StageSystem: IGameSystem { public void LoseHeader() { m_NowHeader -= m_NowStageHandler.LoseHeart(); m_PBDGame.ShowHeart(m_NowHeart); } } public class BossStageHandler: NormalStageHandler { public BossStageHandler(IStageScore StageScore, IStageData StageData): base(StateScore, StageData) { } public override int LoseHeader() { return StageSystem.MAX_HEART; } StageData = new NormalStageData(3f, GetSpawnPosition(), AttackPosition); StageData.AddStageData(ENUM_Enemy.Ogre,ENUM_Weapon.Rocket, 3); StageScore = new StageScoreEnemyKilledCount(13, this); NewStage = NewStage.SetNextHandler(new BossStageHandler(StageScore, StageData));
20.5 結論
責任鏈模式讓一羣信息接收者可以一塊兒被串聯起來管理,讓信息判斷上能有一致的操做接口,沒必要由於不一樣的接收者而必須執行"類轉換操做",而且讓全部的信息接收者都有機會能夠判斷是否提供服務或將需求移往下一個信息接收者,在後續的系統維護上,也能夠輕易地增長接收者類
與其餘模式的合做
在通關判斷上,能夠配合策略模式,讓通關的規則具備其餘的變化形式,而不僅是單純地擊退全部進攻的敵方角色
第21章 成就係統----觀察者模式(Observer)
21.1 成就係統
成就係統(AchievementSystem),是早期單機遊戲就出現的一種系統。
成就係統(AchievementSystem)中的項目,都會和遊戲自己有關,而且在玩家遊玩的過程當中,就能順便收集,或是反覆進行某種操做就能實現目標.通常能夠先將成就項目分門別類
實現上,會先定義"遊戲事件",如敵方角色陣亡,玩家角色陣亡,玩家角色升級等.當遊戲進行過程當中,有任何"遊戲事件"被觸發時,系統就要通知對應的"成就項目",進行累積或條件判斷,若是達到,則完成"成就項目"並通知玩家或直接給予獎勵
一個簡單的設計方式是,咱們能夠把通知成就係統的程序代碼加入到"成就事件觸發"的方法中.
事件觸發後,調用成就係統(AchievementSystem)中的NotifyGameEvent方法,並將觸發的遊戲事件及觸發時的敵方角色傳入.上述範例中,使用枚舉(ENUM)的方式來定義"遊戲事件",並將事件從參數行傳入,而不是針對每個遊戲事件定義特定的調用方法,這樣組能夠避免成就係統定義過多的接口方法.而成就係統的NotifyGameEvent方法,可根據參數傳入的"遊戲事件"參數,來決定後續的處理流程
由於"遊戲事件"很是多,因此在NotifyGameEvent方法中判斷emGameEvent的參屬性,再分別調用對應的私有成員方法
若是讓成就係統(AchievementSystem)負責每個遊戲事件的方法,並針對每個單獨的遊戲事件,去進行"成就項目的累積或判斷", 會讓成就係統的擴充被限制在每一個遊戲事件處理方法中,當之後須要針對某一個遊戲事件增長成就項目時,就必須經過修改原有"遊戲事件處理方法"中的程序代碼才能達成.
此外,"遊戲事件"發生時可能不是隻有成就係統會被影響,其餘系統也可能須要追蹤相關的遊戲事件,所以,若是都是在"遊戲事件"的觸發點進行每一個系統調用的話,那麼觸發點的程序代碼將會變得很是複雜
因此要將"遊戲事件"與"成就係統"分開,讓成就係統僅關注於某些遊戲事件的發生,而遊戲事件的發生,也不僅是提供給成就係統使用.這樣的設計纔是適當的設計
要如何完成這樣的設計呢?若是能將"遊戲事件的產生與通知"獨立成爲一個系統,而且讓其餘系統能經過"訂閱"或"關注"的方式,來追蹤遊戲事件系統發生的事.也就是,當遊戲事件系統發生事件時,會負責去通知全部"訂閱"了遊戲事件的系統,此時被通知的系統,再根據本身的系統邏輯自行決定後續的處理操做.若是能按照上述流程來進行設計,就是一個極爲適當的設計.上述的流程,其實就是觀察者模式(Observer)所要表達的內容
IEnemy.cs public abstract class IEnemy: ICharacter { public override void UnderAttack(ICharacter Attacker) { AchievementSystem.NotifyGamEvent(ENUM_GameEvent.EnemyKilled, this, null); // 通知B系統 // 通知C系統 // 通知D系統 } } AchievementSystem.cs public class AchievementSystem { public void NotifyGameEvent(ENUM_GameEvent emGameEvent, System.Object Param1, System.Object Param2) { switch (emGameEvent) { case ENUM_GameEvent.EnemyKilled: break; ... } } private void Notify_EnemyKilled(IEnemy theEnemy) { } private void Notify_SoldierKilled(ISoldier theSodiler) { } ... }
21.2 觀察者模式(Observer)
觀察者模式(Observer)與命令模式(Command)是很類似的模式,二者都是但願"事件發生"與"功能執行"之間不要有太多的依賴性,不過,仍是能夠按照系統的使用需求,分析出應該運用哪一個模式
21.2.1 觀察者模式(Observer)的定義
GoF對觀察者模式(Observer)的定義爲:
"在對象之間定義一個一對多的鏈接方法,當一個對象變換狀態時,其餘關聯的對象都會自動收到通知"
社交網站就是最佳的觀察者模式(Observer)實現範例,當咱們在社交網站上,與另外一位用戶成爲好友,加入一個粉絲團或關注另外一位用戶的狀態,那麼當這些好友,粉絲團,用戶有任何的新的動態或狀態變更時,就會在咱們動態頁面上"主動"看到這些對象更新的狀況,而沒必要到每一位好友或粉絲團中查看
在早期社交網站尚未普遍流行以前,說明觀察者模式(Observer)常以"報社-訂戶"來作說明:多爲訂戶向報社"訂閱(Subscribe)"了一份報紙,而報社針對昨天的新聞整理編輯以後,在今天一早進行"發佈(Publish)"的工做,接着送報生會主動按照訂閱的信息,將每份報紙送到訂戶手上
在上面的案例中,都存在"主題目標"與其餘"訂閱者/關注者"之間的關係(一對多),當主題變化時,就會經過以前創建的"關係",將更動態的信息傳遞給"訂閱者/關注者".所以,實現上可分爲如下幾點:
主題者,訂閱者的角色
如何創建訂閱者與主題者的關係
主題者發佈信息時,如何通知全部訂閱者
21.2.2 觀察者模式(Observer)的說明
GoF參與者的說明以下:
Subject(主題接口)
定義主題的接口
讓觀察者經過接口方法,來訂閱,解除訂閱主題,這些觀察者在主題內部可以使用泛型容器加以管理
在主題更新時,通知全部觀察者
ConcreteSubject(主題實現)
實現主題接口
設置主題的內容及更新,當主題變化時,使用父類的通知方法告知全部的觀察者
Observer(觀察者接口)
定義觀察者的接口
提供更新通知方法,讓主題能夠通知更新
ConcreteObserver(觀察者實現)
實現觀察者接口
針對主題的更新,按需求向主題獲取更新狀態
21.2.3 觀察者模式(Observer)的實現範例
public abstract class Subject { List<Observer> m_Observers = new List<Observer>(); public void Attach(Observer theObserver) { m_Observers.Add(theObserver); } public void Detach(Observer theObserver) { m_Observers.Remove(theObserver); } public void Notify() { foreach (Observer theObserver in m_Observers) { theObserver.Update(); } } } public class ConcreteSubject: Subject { string m_SubjectState; public void SetState(string State) { m_SubjectState = State; Notify(); } public string GetState() { return m_subjectState; } } public abstract class Observer { public abstract void Update(); } public class ConcreteObserver1: Observer { string m_ObjectState; ConcreteSubject m_Subject = null; public ConcreteObserver1(ConcreteSubject theSubject) { m_Subject = theSubject; } public override void Update() { Debug.Log("ConcreteObserver1.Update"); m_ObjectState = m_Subject.GetState(); } public void ShowState() { Debug.Log("ConcreteObserver1:Subject 當前的主題:" + m_ObjectState); } } public class ConcreteObserver2: Observer { ConcreteSubject m_Subject = null; public ConcreteObserver2(ConcreteSubject theSubject) { m_Subject = theSubject; } public override void Update() { Debug.Log("ConcreteObserver2.Update"); Debug.Log("ConcreteObserver2:Subject 當前的主題:" + m_Subject.GetState()); } } void UnitTest() { ConcreteSubject theSubject = new ConcreteSubject(); ConcreateObserver1 theObserver1 = new ConcreteObserver1(theSubject); theSubject.Attach(theObjserver1); theSubject.Attach(new ConcreteObserver2(theSubject)); theSubject.SetState("Subject狀態1"); theObserver1.ShowState(); }
信息的推與拉
主題(Subject)改變時,改變的內容要如何讓觀察者(Observer)得知,運行方式可分爲推(Push)信息與拉(Pull)信息兩種模式:
推信息:
主題(Subject)將變更的內容主動"推"給觀察者(Observer).通常會在調用觀察者(Observer)的通知(Update)方法時,同時將更新的內容當成參數傳給觀察者(Observer).例如傳統的報社,雜誌社的模式,每一次的發行都會將全部的內容一次發送給訂閱者,全部的訂閱者接到的信息都是一致的,而後訂閱者再從獲取須要的信息來進行處理:
優勢: 全部的內容都一次傳給觀察者,省去觀察者再向主題查詢的操做,主題類也不須要定義太多的查詢方式供觀察者來查詢
缺點: 若是推送的內容過多,容易使觀察者收到沒必要要的信息或形成查詢上的困難,不當的信息設置也可能形成系統性能的下降
拉信息:
主題內容變更時,只是先通知觀察者當前內容有變更,而觀察者則是按照系統需求,再向主題查詢(拉)所需的信息
優勢: 主題只通知當前內容有更新,再由觀察者本身去獲取所需的信息,由於觀察者本身更知道須要哪些信息,因此太會去獲取沒必要要的信息
缺點: 由於觀察者須要向主題查詢更新的內容,因此主題必須提供查詢方式,這樣一來,就容易形成主題類的接口方法過多
而再實現設計上,必須根據系統所須要的最佳狀況來判斷,是要使用"推信息"仍是"拉信息"的方式
21.3 使用觀察者模式(Observer)實現成就係統
重構成就係統,可按照下面的步驟來進行:
1. 實現遊戲事件系統(GameEventSystem);
2. 完成各個遊戲事件的主題及其觀察者
3. 實現成就係統(AchievementSystem)及訂閱遊戲事件
4. 重構遊戲事件觸發點
21.3.1 成就係統的新架構
對於解決《P級陣地》成就係統的需求,首先應該完成的是遊戲事件系統.在遊戲事件系統中,會將每一個遊戲事件當成主題,讓其餘系統可針對感興趣的遊戲事件進行"訂閱".當遊戲事件被觸發時,遊戲事件系統會去通知全部的系統,再讓各個系統針對所須要的信息進行查詢
而成就係統將是遊戲事件系統的一個訂閱者/觀察者.它將針對成就項目所須要的遊戲事件進行訂閱的操做,等到遊戲事件系統發佈遊戲事件時,成就係統再去獲取所需的信息來累積成就項目或判斷成就項目是否達到
參與者的說明以下:
GameEventSystem: 遊戲事件系統,用來管理遊戲中發生的事件,針對每個遊戲事件產生一個"遊戲事件主題(Subject)",並提供接口方法讓其餘系統能訂閱指定的遊戲事件
IGameEventSubject: 遊戲事件主題接口,負責定義《P級陣地》中"遊戲事件"內容的接口,並延伸出下列的遊戲事件主題:
EnemyKilledSubject: 敵方角色陣亡
SoldierKilledSubject: 玩家角色陣亡
SoldierUpgateSubject: 玩家角色升級
NewStageSubject: 新關卡
IGameEventObserver: 遊戲事件觀察者接口,負責《P級陣地》中游戲事件觸發時被通知的操做接口
EnemyKilledObserver 觀察者們: 訂閱"敵方角色陣亡「主題(EnemyKilledSubject)的觀察者類,共有:
EnemyKilledObserverUI: 將敵方角色陣亡信息顯示在界面上
EnemyKilledObserverStageScore: 將敵方角色陣亡信息提供給關卡系統(StageSystem)
EnemyKilledObserverAchievement: 將敵方角色提供給成就係統(AchievementSystem)
同一個遊戲事件能夠提供給不一樣的系統一塊兒訂閱,並能同時接收到更新信息
21.3.2 實現說明
實現遊戲事件系統
public enum ENUM_GameEvent { Null, EnemyKilled, SoldierKilled, SoldierUpgate, NewStage } public class GameEventSystem: IGameSystem { private Dictionary<ENUM_GameEvent, IGameEventSubject> m_GameEvents = new Dictionary<ENUM_GameEvent, IGameEventSubject>(); public GameEventSystem(PBaseDefenseGame PBDGame): base(PBDGame) { Initialize(); } public override void Release() { m_GameEvents.Clear(); } public void RegisterObserver(ENUM_GameEvent emGameEvent, IGameEventObserver Observer) { IGameEventSubject Subject = GetGameEVentSubject(emGameEvent); if (Subject != null) { Subject.Attach(Observer); Observer.SetSubject(Subject); } } private IGameEventSubject GetGameEventSubject(ENUM_GameEvent emGameEvent) { if (m_GameEVents.ContainsKey(emGameEvent)) { return m_GameEvents[emGameEvent]; } IGameEventObject pSubject = null; switch (emGameEvent) { case ENUM_GameEvent.EnemyKilled: pSubject = new EnemyKilledSubject(); break; case ENUM_GameEvent.SoldierKilled: pSubject = new SoldierKilledSubject(); break; case ENUM_GameEvent.SoldierUpgate: pSubject = new SoldierUpgateSubject(); break; case ENUM_GameEvent.NewStage: pSubject = new NewStageSubject(); break; default: Debug.LogWarning("尚未針對[" + emGameEvent + "]指定要產生的Subject類"); return null; } m_GameEvents.Add(emGameEvent, pSubject); return pSubject; } public void NotifySubject(ENUM_GameEvent emGameEvent, System.Object Param) { if (m_gameEvents.ContainsKey(emGameEvent) == false) { return; } m_GameEvents[emGameEvent].SetParam(Param); } }
完成各個遊戲事件主題及其觀察者
public abstract class IGameEventSubject { private List<IGameEventObserver> m_Observers = new List<IGameEventObserver>(); private System.Object m_Param = null; public void Attach(IGameEventObserver theObserver) { m_Observers.Add(theObserver); } public void Detach(IGameEventObserver theObserver) { m_Observers.Remove(theObserver); } public void Notify() { foreach (IGameEventObserver theObserver in m_Observers) { theObserver.Update(); } } public virtual void SetParam(System.Object Param) { m_Param = Param; } }
1. 敵人角色陣亡
public class EnemyKilledSubject: IGameEventSubject { private int m_KilledCount = 0; private IEnemy m_Enmey = null; public IEnemy GetEnemy() { return m_Enemy; } public int GetKilledCount() { return m_KilledCount; } public override void SetParam(System.Object Param) { base.SetParam(Param); m_Enmey = Param as IEnemy; m_KilledCount++; Notify(); } }
2. 玩家角色陣亡
public class SoldierKilledSubject: IGameEventSubject { private int m_KilledCount = 0; private ISoldier m_Soldier = null; public SoldierKilledSubject() { } public ISoldier GetSoldier() { return m_Soldier; } public int GetKilledCount() { return m_KilledCount; } public override void SetParam(System.Object Param) { base.SetParam(Param); m_Soldier = Param as ISoldier; m_KilledCount++; Notify(); } }
3. 玩家角色升級
public class SoldierUpgateSubject: IGameEventSubject { private int m_UpgateCount = 0; private ISoldier m_Soldier = null; public SoldierUpgateSubject() { } public int GetUpgateCount() { return m_UpgateCount; } public override void SetParam(System.Object Param) { base.SetParam(Param); m_Soldier = Param as ISoldier; m_UpgateCount++; Notify(); } public ISoldier GetSoldier() { return m_Soldier; } }
4. 進入新關卡
public class NewStageSubject: IGameEventSystem { private int m_StageCount = 1; public NewStageSubject() { } public int GetStageCount() { return m_StageCount; } public override void SetParam(System.Object Param) { base.SetParam(Param); m_StageCount = (int)Param; Notify(); } }
1. 敵方角色陣亡 主題的觀察者
public class EnemyKilledObserverUI: IGameEventObserver { private EnemyKilledSubject m_Subject = null; private PBaseDefenseGame m_PBDGame = null; public EnemyKilledObserverUI(PBaseDefenseGame PBDGame) { m_PBDGame = PBDGame; } public override void SetSubject(IGameEventSubject Subject) { m_Subject = Subject as EnemyKilledSubject; } public override void Update() { m_PBDGame.ShowGameMsg("敵方單位陣亡"); } } public class EnemyKilledObserverAchievement: IGameEventObserver { private EnemyKilledSubject m_Subject = null; private AchievementSystem m_AchievementSystem = null; public EnemyKilledObserverAchievement(AchievementSystem theAchievementSystem) { m_AchievementSystem = theAchievementSystem; } public override void SetSubject(IGameEventSubject Subject) { m_Subject = Subject as EnemyKilledSubject; } public override vodi Update() { m_AchievementSystem.AddEnemyKilledCount(); } } public class EnemyKilledObserverStageScore: IGameEventObserver { private EnemyKilledSubject m_Subject = null; private StageSystem m_StageSystem = null; public EnemyKilledObserverStageScore(StageSystem theStageSystem) { m_StageSystem = theStageSystem; } public override void SetSubject(IGameEventSystem Subject) { m_Subject = Subject as EnemyKilledSubject; } public override void Update() { m_StageSystem.SetEnemyKilledCount(m_Subject.GetKilledCount()); } }
2. 玩家角色陣亡 主題的觀察者
public class SoldierKilledObserverAchievement: IGameEventObserver { private SoldierKilledSubject m_Subject = null; private AchievementSystem m_AchievementSystem = null; public SoldierKilledObserverAchievement(AchievementSystem theAchievementSystem) { m_AchievementSystem = theAchievementSystem; } public override void SetSubject(IGameEventSubject Subject) { m_Subject = Subject as SoldierKilledSubject; } public override void Udpate() { m_AchievementSystem.AddSoldierKilledCount(); } } public class SoldierKilledObserverUI: IGameEventObserver { private SoldierKilledSubject m_Subject = null; private SoldierInfoUI m_InfoUI = null; public SoldierKilledObserverUI(SoldierInfoUI InfoUI) { m_InfoUI = InfoUI; } public override void SetSubject(IGameEventSubject Subject) { m_Subject = Subject as SoldierKilledSubject; } public override void Update() { m_InfoUI.RefreshSoldier(m_Subject.GetSoldier()); } }
3. 玩家角色升級 主題的觀察者
public class SoldierUpgateObserverUI: IGameEventObserver { private SoldierUpgateSubject m_Subject = null; private SoldierInfoUI m_InfoUI = null; public SoldierUpgateObserverUI(SoldierInfoUI InfoUI) { m_InfoUI = InfoUI; } public override void SetSubject(IGameEventSubject Subject) { m_Subject = Subject as SoldierUpgateSubject; } public override void Update() { m_InfoUI.RefreshSoldier(m_Subject.GetSoldier()); } }
4. 進入新關卡 主題的觀察者
public class NewStageObserverAchievement: IGameEventObserver { private NewStageSubject m_Subject = null; private AchievementSystem m_AchieevmentSystem = null; public NewStageObserverAchievement(AchievementSystem theAchievementSystem) { m_AchievementSystem = theAchievementSystem; } public override void SetSubject(IGameEventSystem Subject) { m_Subject = Subject as NewStageSubject; } public override void Update() { m_AchievementSystem.SetNowStageLevel(m_Subject.GetStageCount()); } }
到了這個階段,遊戲事件系統算是構建完成,讓咱們回到本章開始討論的成就係統.配合遊戲事件系統的訂閱機制,新的成就係統被重構爲:只記錄相關的成就事項,並提供相關的接口方法,讓與成就事項相關的觀察者們(Observer)使用
實現成就係統及訂閱遊戲事件
public class AchievementSystem: IGameSystem { private int m_EnemyKilledCount = 0; private int m_SoldierKilledCount = 0; private int m_StageLv = 0; public AhicevementSystem(PBaseDefenseGame PBDGame): base(PBDGame) { Initialize(); } public override void Initialize() { base.Initialize(); m_PBDGame.RegisterGameEvent(ENUM_GameEvent.EnemyKilled, new EnemyKilledObserverAchievement(this)); m_PBDGame.RegisterGameEvent(ENUM_GameEvent.SoldierKilled, new SoldierKilledObserverAchievement(this)); m_PBDGame.RegisterGameEvent(ENUM_GameEvent.NewStage, new NewStageObserverAchievement(this)); } public void AddEnemyKilledCount() { m_EnemyKilledCount++; } public void AddSoldierKilledCount() { m_SoldierKilledCount++; } public void SetNowStageLevel(int NowStageLevel) { m_StageLv = NowStageLevel; } }
重構遊戲事件觸發點
public class CharacterSystem: IGameSystem { public void RemoveCharacter() { RemoveCharacter(m_Soldiers, m_Enemys, ENUM_GameEvent.SoldierKilled); RemoveCharacter(m_Enemys, m_Soldiers, ENUM_GameEvent.EnemyKilled); } public void RemoveCharacter(List<ICharacter> Characters, List<ICharacter> Opponents, ENUM_GameEvent emEvent) { List<ICharacter> CanRemoves = new List<ICharacter>(); foreach( ICharacter Character in Characters) { if (Character.IsKilled() == false) { continue; if (Character.CheckKilledEvent() == false) { m_PBDGame.NotifyGamEvent(emEvent, Character); } if (Character.CanRemove()) { CanRemoves.Add(Character); } } } }
21.3.3 使用觀察者模式(Observer)的優勢
成就係統以遊戲事件爲基礎,記錄每一個遊戲事件發生的次數及時間點,做爲成就項目的判斷依據.可是當同一遊戲事件被觸發後,可能不僅是隻有一個成就係統會被觸發,系統中也可能存在着其餘系統須要使用同一個遊戲事件.所以,加入了以觀察者模式爲基礎的遊戲事件系統,就能夠有效地解除遊戲事件的發生與有關的系統功能調用之間的綁定,這樣在遊戲事件發生時,沒必要理會後續的處理工做,而是交給遊戲事件主題負責調用觀察者/訂閱者.此外,也能同時調用多個系統同時處理這個事件引起的後續操做
21.3.4 實現觀察者模式(Observer)時的注意事項
雙向與單向信息通知
社交網頁上的"粉絲團"比較像是觀察者模式:當粉絲團發佈了一則新的動態後,全部訂閱的用戶均可以看到新增的動態,而用戶與用戶之間則是同時扮演"主題(Subject)"與」觀察者(Observer)"的角色,除了同時收到其餘好友的動態信息,當本身有任何的動態消息時,也會同時廣播給好友們(觀察者)
類過多的問題
遊戲事件,遊戲事件主題 會隨着項目的開發不斷地增長,於此同時,這些主題的觀察者的數量也會隨之上升.從當前的《P級陣地》內容來看,已經產生了7個遊戲事件觀察者類(IGameEventObserver), 因此不難想象,在大型項目可能會產生很是多的觀察者類(IGameEventObserver).固然,在某些狀況下類過多,反而是個缺點.所以,若是想要減小類的產生,能夠考慮向遊戲的主題註冊時間,不要使用"類對象"而是使用"回調函數",以後再將功能類似的"回調函數"以同一個類來管理,就能減小過多類的問題.這一部分的解決方式與解決大量請求命令時的想法是同樣的
比較命令模式與觀察者模式
這兩個模式都是着重在於將"發生"與"執行"這兩個操做消除耦合(或減小依賴性)的模式,當觀察者模式中的主題只存在一個觀察者時,就很是像是命令模式的基本架構,但仍是有一些差別能夠分辨出兩個模式應用的時機:
命令模式: 該模式的另外一個重點是"命令的管理",應用的系統對於發出的命令有新增,刪除,記錄,排序,撤銷等等的需求
觀察者模式: 對於"觀察者/訂閱者"可進行管理,意思是觀察者能夠在系統運行時間決定訂閱或退訂等操做,讓"執行者(觀察者/訂閱者)"能夠被管理
因此,二者在應用上仍是有明確的目標.固然,若是有須要將兩個模式整合應用並不是不可能,像是讓命令模式(Command)的執行者能夠動態新增,刪除;或是讓觀察者模式(Observer)的"每一次發佈"均可以被管理等等.而這也是本書所要呈現的重點----模式之間的交互合做,會產生更大的效果
21.4 觀察者模式(Observer)面對變化時
public class ComboObserver: IGameEventObserver { } public class PBaseDefenseGame { private void RegisterGameEvent() { m_GameEventSystem.RegisterObserver(ENUM_GameEvent.EnemyKilled, theComboObserver); m_GameEventSystem.RegisterObserver(ENUM_GameEvent.SoldierKilled, theComboObserver); } }
21.5 結論
觀察者模式的設計原理是,新設置一個主題,讓這個主題發佈時可同時通知關係這個主題的觀察者/訂閱者,而且主題沒必要理會觀察者/訂閱者接下來會執行哪些操做.觀察者模式的主要功能和優勢,就是將主題發生和功能執行這兩個操做解除綁定----即消除依賴性,並且對於"執行者(觀察者/訂閱者)"來講,仍是能夠動態決定是否要執行後續的功能
觀察者模式(Observer)的缺點是可能形成過多的觀察者類.不過利用註冊"回調函數"來取代"註冊類對象"可有效減小類的產生
其餘應用方式
在遊戲場景中,設計者一般會擺放一些所謂的"事件觸發點",這些事件觸發點會在玩家角色進入時,觸發對應的遊戲功能,例如忽然會出現一羣怪物NPC來攻擊玩家角色;或是進入劇情模式演出一段遊戲故事劇情等等.並且遊戲一般不會限制一個事件觸發點只能執行一個操做,所以實現時能夠將每個事件觸發點當成一個"主題",而每個要執行的功能,都成爲"觀察者",當事件被觸動發佈時,全部的觀察者都能當即反應
第22章 存盤功能----備忘錄模式(Memento)
22.1 存儲成就記錄
22.2 備忘錄模式(Memento)
22.2.1 備忘錄模式(Memento)的定義
22.2.2 備忘錄模式(Memento)的說明
22.2.3 備忘錄模式(Memento)的實現範例
22.3 使用備忘錄模式(Memento)實現成就記錄的保存
22.3.1 成就記錄保存的功能設計
22.3.2 實現說明
22.3.3 使用備忘錄模式(Memento)的優勢
22.3.4 實現備忘錄模式(Memento)的注意事項
22.4 備忘錄模式(Memento)面對變化時
22.5 結論
第23章 角色信息查詢----訪問者模式(Visitor)
23.1 角色信息的提供
23.2 訪問者模式(Visitor)
23.2.1 訪問者模式(Visitor)的定義
23.2.2 訪問者模式(Visitor)的說明
23.2.3 訪問者模式(Visitor)的實現範例
23.3 使用訪問者模式(Visitor)實現角色信息查詢
23.3.1 角色信息查詢的實現設計
23.3.2 實現說明
23.3.3 使用訪問者模式(Visitor)的優勢
23.3.4 實現訪問者模式(Visitor)時的注意事項
23.4 訪問者模式(Visitor)面對變化時
23.5 結論
第24章 前綴字尾----裝飾模式(Decorator)
24.1 前綴後綴系統
24.2 裝飾模式(Decorator)
24.2.1 裝飾模式(Decorator)的定義
24.2.2 裝飾模式(Decorator)的說明
24.2.3 裝飾模式(Decorator)的實現範例
24.3 使用裝飾模式(Decorator)實現前綴後綴的功能
24.3.1 前綴後綴功能的架構設計
24.3.2 實現說明
24.3.3 使用裝飾模式(Decorator)的優勢
24.3.4 實現裝飾模式(Decorator)時的注意事項
24.4 裝飾模式(Decorator)面對變化時
24.5 結論
第25章 俘兵----適配器模式(Adapter)
25.1 遊戲的寵物系統
25.2 適配器模式(Adapter)
25.2.1 適配器模式(Adapter)的定義
25.2.2 適配器模式(Adapter)的說明
25.2.3 適配器模式(Adapter)的實現範例
25.3 使用適配器模式(Adapter)實現俘兵系統
25.3.1 俘兵系統的架構設計
25.3.2 實現說明
25.3.3 與俘兵相關的新增部分
25.3.4 使用適配器模式(Adapter)的優勢
25.4 適配器模式(Adapter)面對變化時
25.5 結論
第26章 加載速度的優化----代理模式(Proxy)
26.1 最後的系統優化
26.2 代理模式(Proxy)
26.2.1 代理模式(Proxy)的定義
26.2.2 代理模式(Proxy)的說明
26.2.3 代理模式(Proxy)的實現範例
26.3 使用代理模式(Proxy)測試和優化加載速度
26.3.1 優化加載速度的架構設計
26.3.2 實現說明
26.3.3 使用代理模式(Proxy)的優勢
26.3.4 實現代理模式(Proxy)時的注意事項
26.4 代理模式(Proxy)面對變化時
26.5 結論
第27章 迭代器模式(Iterator), 原型模式(Prototype) 和解釋器模式(Interpreter)
27.1 迭代模式(Iterator)
27.2 原型模式(Prototype)
27.3 解釋器模式(Interpretor)
第28章 抽象工廠模式(Abstract Factory)
28.1 抽象工廠模式(AbstractFactory)的定義
28.2 抽象工廠模式(AbstractFactory)的實現
28.3 可應用抽象工廠模式的場合
參考文獻
[1] Design Patterns: Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Addison-Wesly 1994
[2] Refactoring to Patterns, Joshua Kerievsky, Addison-Wesley 2004
[3] Head First Design Patterns, Elisabeth Freeman, Eric Freeman, Bert Bates, Kathy Sierra, O'Reilly 2004
[4] Game Programming Patterns, Robert Nystrom, Genever Benning 2014
[5] Game Coding Complete, 4/e, Mike McShaffry, David Graham, Course Technology 2012
[6] Pattern Hatching: Design Patterns Applied, John Vlissides, Addison-Wesley 1998
[7] Refactoring: Improving the Design of Existing Code, Martin Fowler, Kent Beck, John Brant, William Opdyke, don Roberts, Addison-Wesley 1999
[8] A Pattern Language: Towns, Buildings, Construction(Center for Environmental Structure), Christopher Alexander, Sara Ishikawa, Murray Silverstein, Max Jacobson, Ingrid Fiksdahl-King, Shlomo Angel, Oxford University Press(1977)
[9] Agile Software Development: Principles, Patterns, and Practices, Robert C. Martin, Pearson 2002
[10] Large-Scale C++ Software Design, John Lakos, Addison-Wesley 1996
[11] Design Patterns Explained: A New Perspective on Object-Oriented Design, 2/e, Alan Shalloway, James Trott, Addison-Wesley 2004