日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 综合教程 >内容正文

综合教程

关于游戏架构设计(二)

發布時間:2023/12/3 综合教程 31 生活家
生活随笔 收集整理的這篇文章主要介紹了 关于游戏架构设计(二) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

架構設計目錄二

    • UI架構
      • 基類BaseWindow
      • Control控制類
      • State狀態接口
      • 窗口管理類
      • 狀態管理類
    • 角色系統
      • 角色系統設計的框架圖
      • 角色實體類父類 IEntity
      • 有限狀態機接口EntityFSM
      • 功能實現類Entity
      • FSM具體實現
      • IEntity的后續細分
      • Entity管理類
      • 管理類的擴展
    • 技能系統
      • 父類IEffect
      • 子類中的部分實現
      • 管理類EffectManager

UI架構

先看一下模塊架構設計思路圖:

UI 的設計框架有很多的,這次編寫的 UI 框架不能說是最好的,它最大的優點就是實現了資源和代碼的徹底分離,真正實現了程序和美術人員的分工合作。 UI 框架也是采用了 MVC 設計模式,但是在外面加了一層 State 狀態變換,因為每個 UI 切換是在不同的狀態之間的變換,這樣整個 UI 架構設計就完成了。

游戲的每個 UI 都是使用 Panel 面板制作完成的,每個面板就是一個窗體,用 MVC 中的 View 表示,而用代碼表示就是 Window 窗體類,MVC 中的 Control 也同樣對應窗體的 Control 類。MVC 中的 Model 表示的是數據的傳輸,在當前可以直接使用配置文件進行讀取加載,或者通過網絡進行傳輸。


基類BaseWindow

先來看 Window 窗體類的設計,每個 UI 對應自己的 Window 窗體類。下面先進行 Window 類模塊的代碼編寫,在寫某個模塊時,首先做的事情是搞清楚這個模塊包括哪些內容,再考慮一下編寫此類時擴展是否方便。這些都是編寫時需要注意的問題,有的程序員拿過策劃需求就開始編寫代碼,這樣導致的后果是一旦策劃需求改變,代碼中就要重新加功能,搞的很麻煩,這對于程序員來說是大忌。

以 Window 類編寫為例,游戲中顯示窗體首先要創建窗體,還有窗體可以隱藏、銷毀。另外,創建窗體首先要知道窗體的資源名字,還有這個窗體是在哪個場景中創建的,是登錄場景還是游戲戰斗場景等。因為窗體類不繼承 Mono,為了方便使用窗體中的控件,所以還要做初始化窗體控件的功能以及做監聽處理。

這些功能對于游戲中的任何 UI 窗體都是適用的,換句話說,所有的 UI 這些功能都是必備的,也就是 UI 的共性。這讓人自然而然想到創建一個父類,如果不建父類,每個 Window 類都要寫一套邏輯,這會使得代碼很亂,而且如果是多個人合作的話,每人都來一套邏輯,后期代碼無法維護。所以必須要建一個父類,代碼如下:

 public abstract class BaseWindow
{protected Transform mRoot; //UI根結點protected EScenesType mScenesType; //場景類型protected string mResName;         //資源名protected bool mResident;          //是否常駐 protected bool mVisible = false;   //是否可見//類對象初始化public abstract void Init();//類對象釋放public abstract void Realse();//窗口控制初始化protected abstract void InitWidget();//窗口控件釋放protected abstract void RealseWidget();//游戲事件注冊protected abstract void OnAddListener();//游戲事件注消protected abstract void OnRemoveListener();//顯示初始化public abstract void OnEnable();//隱藏處理public abstract void OnDisable();//每幀更新public virtual void Update(float deltaTime) { }//取得所以場景類型public EScenesType GetScenseType(){return mScenesType;}//是否已打開public bool IsVisible() { return mVisible;  }//是否常駐public bool IsResident() { return mResident; }//顯示public void Show(){if (mRoot == null){if (Create()){InitWidget();}}if (mRoot && mRoot.gameObject.activeSelf == false){mRoot.gameObject.SetActive(true);mVisible = true;OnEnable();OnAddListener();}}//隱藏public void Hide(){if (mRoot && mRoot.gameObject.activeSelf == true){OnRemoveListener();OnDisable();if (mResident){mRoot.gameObject.SetActive(false);}else{RealseWidget();Destroy();}}mVisible = false;}//預加載public void PreLoad(){if (mRoot == null){if (Create()){InitWidget();}}}//延時刪除public void DelayDestory(){if (mRoot){RealseWidget();Destroy();}}//創建窗體private bool Create(){if (mRoot){Debug.LogError("Window Create Error Exist!");return false;}if (mResName == null || mResName == ""){Debug.LogError("Window Create Error ResName is empty!");return false;}if (GameMethod.GetUiCamera.transform== null){return false;}GameObject obj = LoadUiResource.LoadRes(GameMethod.GetUiCamera.transform, mResName);if (obj == null){return false;}mRoot = obj.transform;mRoot.gameObject.SetActive(false);return true;}//銷毀窗體protected void Destroy(){if (mRoot){LoadUiResource.DestroyLoad(mRoot.gameObject);mRoot = null;}}//取得根節點public Transform GetRoot(){return mRoot;}}

該父類設計成了一個抽象類并提供了一些接口便于子類的實現,這些接口所要實現的具體內容是不同的,父類無法具體一一實現,但是顯示、隱藏、破壞這些都是通用的函數,可以在父類中實現。再看一下子類的實現方式,以 LoginWindow 類為例,制作一個簡單的 UI 界面,如下圖所示:


我們就以這兩個按鈕的界面為例來編寫代碼:

public class LoginWindow : BaseWindow
{//開始Transform mBtnStart;enum LOGINUI{None = -1,Login,SelectServer,}public LoginWindow() {//場景類型mScenesType = EScenesType.EST_Login;//場景資源mResName = GameConstDefine.LoadGameLoginUI;//是否常駐內存mResident = false;}繼承接口///類對象初始化監聽顯示和隱藏,為了解耦合public override void Init(){EventCenter.AddListener(EGameEvent.eGameEvent_LoginEnter, Show);EventCenter.AddListener(EGameEvent.eGameEvent_LoginExit, Hide);}//類對象釋放public override void Realse(){EventCenter.RemoveListener(EGameEvent.eGameEvent_LoginEnter, Show);EventCenter.RemoveListener(EGameEvent.eGameEvent_LoginExit, Hide);}//窗口控件初始化以及控件監聽protected override void InitWidget(){mBtnStart = mRoot.Find("BtnStart");mBtnStart.GetComponent<Button>().onClick.AddListener(OnClickAddButton);DestroyOtherUI();}//消息回調函數private void OnClickAddButton(){//在這里監聽按鈕的點擊事件LoginCtrl.Instance.StartGame();}//刪除Login外其他控件,例如public static void DestroyOtherUI(){Canvas canvas = GameMethod.GetCanvas;for (int i = 0; i < canvas.transform.childCount; i++){if (canvas.transform.GetChild(i) != null && canvas.transform.GetChild(i).gameObject != null){GameObject obj = canvas.transform.GetChild(i).gameObject;if (obj.name != "Login(Clone)"){GameObject.DestroyImmediate(obj);}                    }}}//窗口控件釋放protected override void RealseWidget(){}//游戲事件注冊protected override void OnAddListener(){}//游戲事件注消protected override void OnRemoveListener(){}//顯示public override void OnEnable(){}//隱藏public override void OnDisable(){}
}

構造函數對資源文件和 UI 所在的場景類型初始化,以及該 UI 是否常住內存。后面函數是繼承自父類,并對它們的具體實現,在函數中有 LoginCtrl 類接口調用,這個跟 LoginWidow 窗體息息相關,它是 MVC 中的 Control。下面再編寫 LoginCtrl 類。


Control控制類

控制類的主要作用是播放消息,然后在 Loginwindow 中觸發已設置監聽的函數,如 Show、顯示窗體,控制等:

public class LoginCtrl : Singleton<LoginCtrl>
{public void Enter(){EventCenter.Broadcast(EGameEvent.eGameEvent_LoginEnter);   }public void Exit(){EventCenter.Broadcast(EGameEvent.eGameEvent_LoginExit);}//登陸public void Login(string account, string pass){}//登陸錯誤反饋public void LoginError(int code){}//登陸失敗public void LoginFail(){}//開始游戲public void StartGame(){SceneManager.LoadScene("Play");WindowManager.Instance.ChangeScenseToPlay(EScenesType.EST_Login);GameStateManager.Instance.ChangeGameStateTo(GameStateType.GST_Play);}}

將其設置成單例模式,在 Enter 函數中進行消息廣播,另外在函數 StartGame 中使用了 WindowManager 類接口和 GameStateManager 類接口。這兩個類是很關鍵的,后面會貼出。當然如果只有 Loginwindow 和 LoginCtrl 還是無法執行的,我們還缺少 State 狀態類,狀態類是負責窗體 UI 之間的切換,每個 UI 窗體對應著自己的狀態,為了區分不同的 UI 窗體,利用這些狀態枚舉表示:

public enum GameStateType
{GST_Continue,GST_Login,GST_Role,GST_Loading,GST_Play,//***
}

State狀態接口

通常來說,我們會定義設置某個狀態、獲取狀態、進入狀態、停止狀態、更新狀態這些方法等。我們在設計每個模塊時,都會先明確這個模塊具體要做哪些事情,其實在設計一個類的內容時,可以先多想想,這樣類的函數定義自然就有了。另外,提到的這些方法,每個 UI 狀態都會包含,這樣我們就可以將其抽離出來,定義成一個接口模塊供程序使用:

public interface IGameState
{GameStateType GetStateType();void SetStateTo(GameStateType gsType);void Enter();GameStateType Update(float fDeltaTime);void FixedUpdate(float fixedDeltaTime);void Exit();
}

抽象類實現了 oop 中的一個原則,把可變的與不可變的分離,所以 BaseWindow 采用了抽象的定義。UI 通用函數方法在父類中先實現出來,而不通用的函數方法只提供接口。再說接口類,好的接口類定義應該是具有專一功能性的,而不是多功能的,否則會造成接口污染。我們的狀態類 IGameState 功能是單一的,所以 IGameState 采用了接口的定義。窗體中的狀態類繼承 IGameState 類:

class LoginState : IGameState
{private  GameStateType _stateTo;//構造函數public LoginState(){}//獲取狀態public GameStateType GetStateType(){return GameStateType.GST_Login;}//設置狀態public void SetStateTo(GameStateType gs){_stateTo = gs;}//進入狀態public void Enter(){SetStateTo(GameStateType.GST_Continue);LoginCtrl.Instance.Enter();        }//停止狀態public void Exit(){LoginCtrl.Instance.Exit();}public void FixedUpdate(float fixedDeltaTime){}//更新狀態public GameStateType Update(float fDeltaTime){return _stateTo;}}

在這個 Login UI 窗體狀態類中實現了 IGameState 中定義的接口函數。

總的來說一個 UI 窗體主要是包含三個代碼模塊:Window、Ctl、State。Window 和 Ctrl 之間是通過 Event 和接口連接起來的,而 State 和 Ctrl 是通過調用接口連接起來的。模塊之間的關系框架:


窗口管理類

現在需要一個窗體管理類來進行統一調度,這讓人會想到工廠模式。那么 WindowMananger 管理類要管理這么多窗體,首先要做的事情是將它們進行注冊,也就是將其存儲到字典中:

 public enum EScenesType{EST_None,EST_Login,EST_Play,}public enum EWindowType{EWT_LoginWindow, //登錄EWT_RoleWindow, //用戶EWT_PlayWindow,//戰斗}public class WindowManager : Singleton<WindowManager>{public WindowManager(){mWidowDic = new Dictionary<EWindowType, BaseWindow>();mWidowDic[EWindowType.EWT_LoginWindow] = new LoginWindow();mWidowDic[EWindowType.EWT_RoleWindow] = new RoleWindow();mWidowDic[EWindowType.EWT_PlayWindow] = new PlayWindow();}}

定義了窗體所對應自己的類型后,便于區分不同的窗體,構造函數實現了窗體的注冊,注冊好了窗體后,下面開始窗體方法的實現,比如游戲中不同的窗體之間進行 UI 切換,從一個 UI 切換到另一個 UI。再比如切換窗體到游戲場景、切換窗體返回到登錄場景等等,這些方法對應的實現函數如下:

//切換到游戲場景public void ChangeScenseToPlay(EScenesType front){foreach (BaseWindow pWindow in mWidowDic.Values){if (pWindow.GetScenseType() == EScenesType.EST_Play){pWindow.Init();if(pWindow.IsResident()){pWindow.PreLoad();}}else if ((pWindow.GetScenseType() == EScenesType.EST_Login) && (front == EScenesType.EST_Login)){pWindow.Hide();pWindow.Realse();if (pWindow.IsResident()){pWindow.DelayDestory();}}}}//切換到登錄場景public void ChangeScenseToLogin(EScenesType front){foreach (BaseWindow pWindow in mWidowDic.Values){if (front == EScenesType.EST_None && pWindow.GetScenseType() == EScenesType.EST_None){pWindow.Init();if (pWindow.IsResident()){pWindow.PreLoad();}}if (pWindow.GetScenseType() == EScenesType.EST_Login){pWindow.Init();if (pWindow.IsResident()){pWindow.PreLoad();}}else if ((pWindow.GetScenseType() == EScenesType.EST_Play) && (front == EScenesType.EST_Play)){pWindow.Hide();pWindow.Realse();if (pWindow.IsResident()){pWindow.DelayDestory();}}}}

上面的兩個方法,就是遍歷窗體,看看它是對窗體隱藏還是顯示,還是對其進行預加載操作。這兩個方法會經常使用。另外,還需要提供隱藏所有窗體和更新窗體的接口:

public void HideAllWindow(EScenesType front){foreach (var item in mWidowDic){if (front == item.Value.GetScenseType()){Debug.Log(item.Key);item.Value.Hide();}}}public void Update(float deltaTime){foreach (BaseWindow pWindow in mWidowDic.Values){if (pWindow.IsVisible()){pWindow.Update(deltaTime);}}}

狀態管理類

每個 UI 窗體都對應三者:Window、Ctrl、State,這樣每個窗體對應的 State 跟 Window 也是同樣多的,有這么多狀態,就需要一個管理類來進行管理。將其都放到 StateManager 管理類中,這樣既管理了 State,也做了狀態切換處理:

        public GameStateManager(){gameStates = new Dictionary<GameStateType, IGameState>();IGameState gameState;gameState = new LoginState();gameStates.Add(gameState.GetStateType(), gameState);gameState = new RoleState();gameStates.Add(gameState.GetStateType(), gameState);gameState = new PlayState();gameStates.Add(gameState.GetStateType(), gameState);}

管理類主要是對于這些同類型的類模塊的操作,比如要獲取到當前狀態以及切換狀態,設置游戲剛開始的默認狀態,還有游戲狀態的更新函數 Update / FixedUpdate,關于管理類的方法,為了邏輯的編寫,肯定需要一個統一的接口調用。狀態管理類 GameStateManager 的其他方法:

// 獲取當前狀態public IGameState GetCurState(){return currentState;}//改變狀態public void ChangeGameStateTo(GameStateType stateType){if (currentState != null && currentState.GetStateType() != GameStateType.GST_Loading && currentState.GetStateType() == stateType) return;if (gameStates.ContainsKey(stateType)){if (currentState != null){currentState.Exit();}currentState = gameStates[stateType];currentState.Enter();}}//進入默認狀態public void EnterDefaultState(){ChangeGameStateTo(GameStateType.GST_Login);}public void FixedUpdate(float fixedDeltaTime){if (currentState != null){currentState.FixedUpdate(fixedDeltaTime);}}//更新狀態public void Update(float fDeltaTime){GameStateType nextStateType = GameStateType.GST_Continue;if (currentState != null){nextStateType = currentState.Update(fDeltaTime);}if (nextStateType > GameStateType.GST_Continue){ChangeGameStateTo(nextStateType);}}//獲取狀態public IGameState getState(GameStateType type){if (!gameStates.ContainsKey(type)){return null;}return gameStates[type];}

UI 架構設計基本完整了。


角色系統

大部分游戲都有自己的角色系統,角色系統設計要考慮的問題比較多,下面開始搭建一個角色系統,首先要加載一個角色,這個角色要包括這些屬性:資源路徑、資源名字、角色擁有的裝備、血條、經驗值、攻擊距離、防御、BUFF、角色等級、重生時間等,另外,我們的角色還會有自己的動作和技能等。

定義好角色屬性后,就要考慮實現它們的方法了,角色會設計不同動作和技能狀態,這些狀態之間會不停的切換,因為角色的狀態是有限個的,動作不可能是無限的,這自然會讓人想到 FSM 有限狀態機的使用。


角色系統設計的框架圖


角色實體類父類 IEntity

游戲中的每個角色都會有這些屬性和方法,當然也包括怪物和 NPC。了解了角色的屬性和方法后,接下來就要設計代碼去實現它們,作為共同擁有的屬性,我們可以將其抽離出來作為父類編寫。首先定義角色的共有屬性,因為角色屬性太多,這里只寫一下重要的,下面是定義的角色實體類的父類 IEntity 中的片段:

//物理攻擊public float PhyAtk{set;get;}//魔法攻擊public float MagAtk{set;get;}//物理防御public float PhyDef{set;get;}//魔法防御public float MagDef{set;get;}//血量恢復public float HpRecover{set;get;}//魔法恢復public float MpRecover{set;get;}// 血量public float Hp{set;get;}//魔法public float Mp{private set;get;}//等級public int Level{private set;get;}

以上是列出了一部分的屬性定義,另外使用有限狀態機對動作和技能進行狀態的變換,有限狀態機的設計首先要知道狀態機的類型,通過類型去區分不同的狀態,這個一般使用枚舉值表示:

    //狀態枚舉public enum FSMState
{FSM_STATE_FREE,FSM_STATE_RUN,FSM_STATE_SING,FSM_STATE_RELEASE,FSM_STATE_LEADING,FSM_STATE_LASTING,FSM_STATE_DEAD,FSM_STATE_ADMOVE,FSM_STATE_FORCEMOVE,FSM_STATE_RELIVE,FSM_STATE_IDLE,
}

有限狀態機接口EntityFSM

該枚舉值定義了動作的一些狀態,然后開始狀態接口的封裝,有限狀態機跟我們前面的 State 比較類似:

    //有限狀態機接口public interface EntityFSM
{bool CanNotStateChange{set;get;}FSMState State { get; }void Enter(IEntity entity , float stateLast);bool StateChange(IEntity entity , EntityFSM state);void Execute(IEntity entity);void Exit(IEntity Ientity);
}

有限狀態機定義了執行的接口和方法,進入狀態、改變狀態、執行、停止狀態這些方法,該類只提供了接口,并沒有實現,這樣它的實現就會放到其子類中。由于 FSM 有限狀態機的切換是通用的,我們還是將它們放到父類 IEntity 類中進行:

    public void OnFSMStateChange(EntityFSM fsm){if (this.FSM != null && this.FSM.StateChange(this, fsm)){return;}if (this.FSM == fsm && this.FSM != null && (this.FSM.State == FSMState.FSM_STATE_DEAD)){return;}if (this.FSM != null){this.FSM.Exit(this);}this.FSM = fsm;if (this.FSM != null)this.RealEntity.FSMStateName = fsm.ToString();this.FSM.Enter(this, 0.0f);}

在該函數的執行過程中,首先判斷狀態是否為空,如果不為空,將停止當前狀態,執行下一個狀態。設計思想是通用的接口可以將其放到某個類中定義,比如實體類的移動、待機、技能等接口,這里將其放在父類 IEntity 中,當然這些函數具體可以在子類中實現,而通用的可以在父類中實現,就比如:

  //狀態機改變數據public void EntityFSMChangeDataOnSing(Vector3 mvPos, Vector3 mvDir, int skillID, IEntity targetID){EntityFSMPosition = mvPos;EntityFSMDirection = mvDir;EntitySkillID = skillID;entitySkillTarget = targetID;}       public void EntityFSMChangedata(Vector3 mvPos, Vector3 mvDir){EntityFSMPosition = mvPos;EntityFSMDirection = mvDir;}    public virtual void OnEntitySkill(){}//進入Idle狀態public virtual void OnEnterIdle(){}//進入Move狀態public virtual void OnEnterMove(){}

功能實現類Entity

基本上角色所有的共同屬性和方法都定義在 IEntity 類中,也就是核心功能設計。再說一下 Entity 類,核心思想就是播放角色的動作以及設置技能的播放點,它是繼承 Mono 的,需要動態的掛接到對象上。新動畫狀態機或者老動畫狀態播放都是在此類中實現的,另外,角色身上的通過文本讀取的基礎屬性也是在此類中定義,這樣可以方便策劃調試運行代碼:

public class EntityAttrubte
{public float Hp;public float HpMax;public float Mp;public float MpMax;public float Speed;public float PhyAtk;public float MagAtk;public float PhyDef;public float MagDef;public float AtkSpeed;public float AtkDis;public float HpRecover;public float MpRecover;public float RebornTime;public void AttribbuteUpdate(IEntity entity){Hp = entity.Hp;HpMax = entity.HpMax;Mp = entity.Mp;MpMax = entity.MpMax;Speed = entity.EntityFSMMoveSpeed;PhyAtk = entity.PhyAtk;MagAtk = entity.MagAtk;PhyDef = entity.PhyDef;MagDef = entity.MagDef;AtkSpeed = entity.AtkSpeed;AtkDis = entity.AtkDis;HpRecover = entity.HpRecover;MpRecover = entity.MpRecover;RebornTime = entity.RebornTime;}
}

在此定義了基礎屬性,然后看一下 Entity 類中的核心設計:

    //初始化掛載點public virtual void Awake(){objAttackPoint = transform.Find("hitpoint");objBuffPoint = transform.Find("buffpoint");objPoint = transform.Find("point");if (objAttackPoint == null || objBuffPoint == null || objPoint == null){}skeletonAnimation = GetComponentInChildren<SkeletonAnimation>();EntityAttribute = new EntityAttrubte();}// 設置移動速度public void SetMoveAnimationSpd(float spd){if (GetComponent<Animation>() == null){return;}AnimationState aState = GetComponent<Animation>()["walk"];if (aState != null){aState.speed = spd;}}//播放動作public void PlayerAnimation(string name){if (name == "" || name == "0"){return;}if (this.GetComponent<Animation>() == null){return;}GetComponent<Animation>().CrossFade(name);}//播放移動動作public void PlayeMoveAnimation(){if (skeletonAnimation != null){var track = skeletonAnimation.AnimationState.SetAnimation(0, "run", true);}}//播放待機動作public void PlayeIdleAnimation(){if (skeletonAnimation != null){var track = skeletonAnimation.AnimationState.SetAnimation(0, "idle", true);}}//播放攻擊動作internal void PlayAttackAnimation(){if (skeletonAnimation != null){var track = skeletonAnimation.AnimationState.SetAnimation(1, "shoot", false);}}

在 Awake 函數中實現掛載節點的初始化操作,這里使用的是老動畫播放,原理跟新動畫是類似的,如果是使用新動畫只是將其改成 SetTrigger 觸發就可以了。


FSM具體實現

下面再說一下有限狀態機 FSM 的具體實現,在前面已經介紹了類 EntityFSM,它只是提供了接口,沒有具體實現。下面以角色待機動作為例介紹FSM的使用:

    public class EntityIdleFSM : EntityFSM{public static readonly EntityFSM Instance = new EntityIdleFSM();public FSMState State{get{return FSMState.FSM_STATE_IDLE;}}public bool CanNotStateChange{set;get;}            public bool StateChange(IEntity entity , EntityFSM fsm){return CanNotStateChange;}public void Enter(IEntity entity , float last){entity.OnEnterIdle();}public void Execute(IEntity entity){}public void Exit(IEntity entity){}}

這樣角色系統的基礎類設計完成了


IEntity的后續細分

下面再回到父類 IEntity,已經在其中實現了角色的通用方法以及角色使用的屬性,我們還需要繼續實現它的子類,在此將其進行了細分:

在 IEntity 類下面分了兩級,之所以分兩級是因為角色還有與網絡有關的方法和屬性,如果只是放到一個類 IEntity 中,這樣 IEntity 就顯得很臃腫,代碼量會很龐大,不利于維護。

IPlayer 類模塊也是 IEntity 的子類,之所以劃分出這個類,是因為游戲需要聯網,開房間,而此類就涉及到開房間陣營的設置,它不包含具體的數值,它還是一個抽象的玩家實體。因為玩家都具有陣營、房間這些屬性,所以也是公有的,抽離出來的。

首先將角色身上的一些函數方法都放在 IPlayer 類里面,它做的事情都是與角色相關的,會比較雜,比如角色身上的相機位置更新、角色的重生、創建角色陰影效果,以及在角色身上動態的添加多個碰撞體,改變角色的移動速度、鎖定目標、創建血條等,這些方法都是圍繞角色身上展開的:

// 設置鎖定對象public virtual void SetSyncLockTarget(IEntity entity){if (SyncLockTarget == entity){return;}this.DeltLockTargetXuetiao(entity);this.SyncLockTarget = entity;}// Entity移動屬性變化public override void OnEntityMoveSpeedChange(int value){base.OnEntityMoveSpeedChange(value);HeroConfigInfo heroConfig;if (ConfigReader.HeroXmlInfoDict.TryGetValue(NpcGUIDType, out heroConfig)){float speed = value / heroConfig.HeroMoveSpeed * heroConfig.n32BaseMoveSpeedScaling / 1000;if (speed == 0){return;}this.RealEntity.SetMoveAnimationSpd(speed);}}

以上代碼可以根據需求去編寫,重點是掌握思想,具體實現可以根據需求去做。接下來再看 IPlayer 類的子 Player 類的設計,前面兩個都是角色公有的屬性和方法,Player 這個類涉及玩家的具體操作,比如玩家的游戲物品獲取、技能相關的釋放和冷卻、玩家的經驗值或者血量、自身角色的移動、動作的播放以及動作事件的回調等,就比如下面的示例方法:

    public Player(UInt64 sGUID, EntityCampType campType): base(sGUID, campType){UserGameItems = new Dictionary<int, int>();UserGameItemsCount = new Dictionary<int, int>();UserGameItemsCoolDown = new Dictionary<int, float>();for (int ct = 0; ct < 6; ct++){UserGameItems.Add(ct, 0);UserGameItemsCount.Add(ct, 0);UserGameItemsCoolDown.Add(ct, 0f);}}//準備釋放技能//技能類型public void SendPreparePlaySkill(SkillType skType){int skillID = GetSkillIdBySkillType(skType);//沉默了if (Game.Skill.BuffManager.Instance.isSelfHaveBuffType(1017)){return;}if (skillID == 0){MsgInfoManager.Instance.ShowMsg((int)ERROR_TYPE.eT_AbsentSkillNULL);return;}}//初始化技能升級列表private void InitSkillDic(Dictionary<SkillType, int> skillDic){int id = (int)ObjTypeID;HeroConfigInfo heroInfo = ConfigReader.GetHeroInfo(id);skillDic.Add(SkillType.SKILL_TYPE1, heroInfo.HeroSkillType1);skillDic.Add(SkillType.SKILL_TYPE2, heroInfo.HeroSkillType2);skillDic.Add(SkillType.SKILL_TYPE3, heroInfo.HeroSkillType3);skillDic.Add(SkillType.SKILL_TYPE4, heroInfo.HeroSkillType4);skillDic.Add(SkillType.SKILL_TYPEABSORB1, 0);skillDic.Add(SkillType.SKILL_TYPEABSORB2, 0);}//根據技能類型,換算出滿足指定技能的準確idprivate void SetSkillUpdate(SkillType skillType, int lv){int baseId = 0;if (!BaseSkillIdDic.TryGetValue(skillType, out baseId)) return;//技能id不存在SkillManagerConfig info = ConfigReader.GetSkillManagerCfg(baseId);if (baseId == 0 || info == null){return;//不存在技能信息 }for (int i = baseId + SKILL_UPDATE_TOTAL_LEVEL - 1; i >= 0; i--){SkillManagerConfig infoNew = ConfigReader.GetSkillManagerCfg(i);if (i == 0 || infoNew == null || infoNew.n32UpgradeLevel > lv)continue;SkillIdDic[skillType] = i;break;}}

Entity管理類

至此角色系統底層的封裝就結束了,接下來再封裝一個管理類————EntityManager 類,因為游戲中會生成很多的實體對象,這些實體對象包括玩家自身、其他玩家、怪物 NPC 等,所以需要一個對外提供接口的管理類,還要創建一個對外提供的創建角色的接口,可以用它創建我們的角色。在其中,首先需要創建存儲我們所定義實體對象的字典 Dictionary,用于保存游戲中的所有生成的實體對象:

    public static EntityManager Instance{private set;get;}public static Dictionary<UInt64, IEntity> AllEntitys = new Dictionary<UInt64, IEntity>();public enum CampTag{SelfCamp = 1,EnemyCamp = 0,}static int[] HOME_BASE_ID = { 21006, 21007, 21020, 21021 };private static List<IEntity> homeBaseList = new List<IEntity>();public EntityManager(){Instance = this;}

這里將 EntityManager 管理類作為單例模式使用,有了存儲實體對象的字典后,就可以對實體對象進行諸如顯示和隱藏操作以及刪除所有實體的操作,其實就是對保存在 Dictionary 中的對象進行操作:

    //顯示實體public void ShowEntity(UInt64 sGUID, Vector3 pos, Vector3 dir){if (!AllEntitys.ContainsKey(sGUID) || AllEntitys[sGUID].realObject == null){return;}AllEntitys[sGUID].realObject.transform.position = pos;AllEntitys[sGUID].realObject.transform.rotation = Quaternion.LookRotation(dir);AllEntitys[sGUID].realObject.SetActive(true);if (AllEntitys[sGUID].FSM != null && AllEntitys[sGUID].FSM.State != Game.FSM.FSMState.FSM_STATE_DEAD){AllEntitys[sGUID].ShowXueTiao();}else if (AllEntitys[sGUID].FSM != null && AllEntitys[sGUID].FSM.State == Game.FSM.FSMState.FSM_STATE_DEAD){AllEntitys[sGUID].HideXueTiao();}}//隱藏實體public void HideEntity(UInt64 sGUID){if (!AllEntitys.ContainsKey(sGUID)){return;}IEntity entity = null;if (EntityManager.AllEntitys.TryGetValue(sGUID, out entity) && entity.entityType == EntityType.Player){}AllEntitys[sGUID].HideXueTiao();AllEntitys[sGUID].realObject.SetActive(false);}//刪除實體public void DestoryAllEntity(){List<UInt64> keys = new List<UInt64>();foreach (IEntity entity in AllEntitys.Values){if (entity.entityType != EntityType.Building){keys.Add(entity.GameObjGUID);}}foreach (UInt64 gui in keys){HandleDelectEntity(gui);}}

另外,最重要的操作就是創建模型實體,創建的過程,還包括將其放到對象池的過程,避免頻繁的創建和刪除,產生內存碎片,同時需要將其在場景中顯示出來,并且添加組件代碼:

    public GameObject CreateEntityModel(IEntity entity, UInt64 sGUID, Vector3 dir, Vector3 pos){if (entity != null){int id = (int)entity.ObjTypeID;this.SetCommonProperty(entity, id);entity.ModelName = "Jinglingnan_6";if (entity.ModelName == null || entity.ModelName == ""){return null;}string path = GameDefine.GameConstDefine.LoadModelPath;//創建GameObject    string resPath = path + entity.ModelName;entity.realObject = GameObjectPool.Instance.GetGO(resPath);if (entity.realObject == null){Debug.LogError("entity realObject is null");}//填充Entity信息entity.resPath = resPath;entity.objTransform = entity.realObject.transform;entity.realObject.transform.localPosition = pos;entity.realObject.transform.localRotation = Quaternion.LookRotation(dir);if (entity.NPCCateChild != ENPCCateChild.eNPCChild_BUILD_Shop){entity.CreateXueTiao();}AddEntityComponent(entity);return entity.realObject;}return null;}

在函數中,entity 是父類中的對象,它并不是實際意義上的模型實體,它內部定義了 GameObject,這樣就可以對其進行賦值操作,使用的是對象池中的函數接口。

在函數中還調用了接口 AddEntityComponent,它用于動態添加角色的腳本組件,因為資源和代碼是徹底分離的:

    public static void AddEntityComponent(IEntity entity){//沒有,添加Entity組件if (entity.realObject.GetComponent<Entity>() == null){Entity syncEntity = entity.realObject.AddComponent<Entity>() as Entity;entity.RealEntity = syncEntity;}//直接取else{Entity syncEntity = entity.realObject.GetComponent<Entity>() as Entity;entity.RealEntity = syncEntity;}}

以上函數能夠動態的添加 Entity 代碼組件,如果在游戲中,我們要在場景中增加某個對象呢,還要獲取某個對象呢,這些也是這個實體管理類來提供的:

    public void AddEntity(UInt64 sGUID, IEntity entity){if (AllEntitys.ContainsKey(sGUID)){Debug.LogError("Has the same Guid: " + sGUID);return;}AllEntitys.Add(sGUID, entity);}public virtual IEntity GetEntity(UInt64 id){IEntity entity;if (AllEntitys.TryGetValue(id, out entity)){return entity;}return null;}

不論增加還是獲取都是通過它的 ID 來進行的,相比字符串查找,效率更高,這樣 EntityManager 類就完成了。如果我們還需要在 EntityManager 類的基礎上擴展,就需要自己實現管理類去繼承 EntityManager。


管理類的擴展

在此又實現了一個 PlayerManager 用于生成 Player,該類繼承 EntityManager,它的核心思想就是創建 Player:

public class PlayerManager : EntityManager{public static new PlayerManager Instance {private set;get;}public Dictionary<UInt64, IPlayer> AccountDic = new Dictionary<UInt64, IPlayer>();public PlayerManager(){Instance = this;}public Player LocalPlayer {set;get;}		 public IPlayer LocalAccount{set;get;}public override Ientity HandleCreateEntity (UInt64 sGUID , EntityCampType campType){    Iplayer player;if (GameUserModel.Instance.IsLocalPlayer(sGUID)){player = new Player(sGUID, campType);                }else{player =  new Iplayer(sGUID, campType);}player.GameUserId = sGUID;return player;}public void AddAccount(UInt64 sGUID, IPlayer entity){if (AccountDic.ContainsKey (sGUID)) {		return;}AccountDic.Add (sGUID , entity);}public override void SetCommonProperty(IEntity entity, int id){base.SetCommonProperty(entity, id);IPlayer mpl = (IPlayer)entity;if (mpl.GameUserNick == "" || mpl.GameUserNick == null){}}protected override string GetModeName (int id){}public bool IsLocalSameType(IEntity entity){if(PlayerManager.Instance.LocalPlayer.EntityCamp != entity.EntityCamp)return false;return true;}public void CleanAccount(){for (int i = AccountDic.Count - 1; i >= 0; i--) {AccountDic.Remove (AccountDic.ElementAt(i).Key);}					 }public void RemoveAccountBySeat(uint seat){for (int i = AccountDic.Count - 1; i >= 0; i--) {if (AccountDic.ElementAt(i).Value.GameUserSeat != seat)continue;	AccountDic.Remove (AccountDic.ElementAt(i).Key);break;}					 }public void CleanPlayerWhenGameOver() {foreach (var item in AccountDic.Values) { item.CleanWhenGameOver();}}}

以上的例子,說明了可以在此框架的基礎上繼續擴展,當然也可以修改。以上實現了對外接口的編寫,這樣整個角色系統就完成了。


技能系統

在游戲中,技能特效是伴隨著角色動作播放的,角色動作可以使用 FSM 播放,技能就是將動作和特效合在一起播放,看一下技能模塊設計圖:


父類IEffect

IEffect 模塊,所有的技能肯定有共同的屬性,為了避免屬性被重復的定義,我們將其放到一個父類中,該父類就是 IEffect。首先游戲中會有很多技能,這么多技能如何區分,這就涉及到一個技能類型的定義,技能類型定義可以使用字符串,也可以使用枚舉。這里使用枚舉表示:

    public enum ESkillEffectType{eET_Passive,eET_Buff,eET_BeAttack,eET_FlyEffect,eET_Normal,eET_Area,eET_Link,}

技能還有一些共同的屬性和方法,先定義屬性,比如特效的運行時間、資源路徑、生命周期、技能釋放者和受擊者、播放的音效等。這些我們可以自己根據需求去定義:

    //基本信息    public GameObject obj = null;           //特效物體public Transform mTransform = null;protected float currentTime = 0.0f;     //特效運行時間public bool isDead = false;             //特效是否死亡public string resPath;                  //特效資源路徑        public string templateName;             //特效模板名稱public Int64 projectID = 0;             //特效id  分為服務器創建id 和本地生成id        public uint skillID;                    //特效對應的技能idpublic float cehuaTime = 0.0f;          //特效運動持續時間或者是特效基于外部設置的時間    策劃配置      public float artTime = 0.0f;            //美術設置的特效時間                            美術配置public float lifeTime = 0;              //特效生命周期, 0為無限生命周期public UInt64 enOwnerKey;               //技能釋放者public UInt64 enTargetKey;              //技能受擊者        public AudioSource mAudioSource = null; //聲音//運動信息public Vector3 fPosition;public Vector3 fixPosition;public Vector3 dir;public Vector3 distance;public ESkillEffectType mType;

共同屬性定義完了,下面定義它的共同方法。要使用特效,首先要創建特效:

    //特效創建接口public void Create(){//創建的時候檢查特效projectId,服務器沒有設置生成本地idCheckProjectId();//獲取特效模板名稱templateName = ResourceCommon.getResourceName(resPath);//使用特效緩存機制           obj = GameObjectPool.Instance.GetGO(resPath);if (null == obj){Debugger.LogError("load effect object failed in IEffect::Create" + resPath);return;}//創建完成,修改特效名稱,便于調試obj.name = templateName + "_" + projectID.ToString();OnLoadComplete();//獲取美術特效腳本信息                effectScript = obj.GetComponent<EffectScript>();if (effectScript == null){Debugger.LogError("cant not find the effect script in " + resPath);return;}artTime = effectScript.lifeTime;//美術配置時間為0,使用策劃時間if (effectScript.lifeTime == 0)lifeTime = cehuaTime;//否則使用美術時間elselifeTime = artTime;//特效等級不同,重新設置EffectLodLevel effectLevel = effectScript.lodLevel;EffectLodLevel curLevel = EffectManager.Instance.mLodLevel;if (effectLevel != curLevel){//調整特效顯示等級AdjustEffectLodLevel(curLevel);}}

該函數的實現流程是先加載特效,也是從對象池生成,并且將其重新命名,然后調用函數 OnLoadComplete() 去設置特效的發射點,因為特效發射點要根據不同的技能去設置,所以在 IEffect 類中只是定義了一個它的虛函數:

    public virtual void OnLoadComplete(){}

愛他的具體的功能要在特效子類中去實現,當然 IEffect 類并不是只有這一個函數實現,其他的功能函數可以根據需求自己去定義了。


子類中的部分實現

特效父類 IEffect 已定義完成,接下來就要編寫具體的子類了,根據需求可以擴展下去。我們先拿出 BeAttackEffect 被動技能舉例說明,具體實現一下 OnLoadComplete:

    public override void OnLoadComplete(GameObject obj){//判斷enTargetIEntity enTarget;EntityManager.AllEntitys.TryGetValue(enTargetKey, out enTarget);if (enTarget != null && obj != null){//擊中點Transform hitpoit = enTarget.RealEntity.transform.FindChild("hitpoint");if (hitpoit != null){//設置父類和位置GetTransform().parent = hitpoit;GetTransform().localPosition = new Vector3(0.0f, 0.0f, 0.0f);}}if (skillID == 0){return;}}

該函數是先在表里查找,如果找到了,則去查找對象的虛擬點,然后將該虛擬點設置給特效。


管理類EffectManager

這么多的技能,我們同樣也需要一個管理器 EffectManager 類,用于對外提供創建特效接口:

        //創建基于時間的特效public BeAttackEffect CreateTimeBasedEffect(string res, float time, IEntity entity){if (res == "0")return null;BeAttackEffect effect = new BeAttackEffect();//加載特效信息effect.skillID = 0;             //技能id=0     effect.cehuaTime = time;effect.enTargetKey = entity.GameObjGUID;effect.resPath = res;           //創建effect.Create();AddEffect(effect.projectID, effect);return effect;}

該函數對應的是 BeAttackEffect,將它初始化后,調用 Create 創建加載特效,為了便于管理我們調用了函數 AddEffect,目的是將特效加到表中:

    //添加特效到EffectMap表public void AddEffect(Int64 id, IEffect effect){if (!m_EffectMap.ContainsKey(id)){m_EffectMap.Add(id, effect);}else{Debug.LogError("the id: " + id.ToString() + "effect: " + effect.resPath + "has already exsited in EffectManager::AddEffect");}}

總結

以上是生活随笔為你收集整理的关于游戏架构设计(二)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。