关于游戏架构设计(二)


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 窗体,利用这些状态枚举表示:

扫描二维码关注公众号,回复: 6099839 查看本文章
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);
    }
    
    //根据技能类型,换算出满足指定技能的准确id
    private 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;                    //特效对应的技能id

    public 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,服务器没有设置生成本地id
        CheckProjectId();
        //获取特效模板名称
        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;
        //否则使用美术时间
        else
            lifeTime = 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)
    {
        //判断enTarget
        IEntity 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");
        }
    }

猜你喜欢

转载自blog.csdn.net/THIOUSTHIOUS/article/details/87806459