1. 程式人生 > >Unity3D用狀態機制作角色控制系統

Unity3D用狀態機制作角色控制系統

為了讓讀者對本文知識點有一個比較清晰的瞭解,我製作了一張結構圖,如下圖,圖中以移動為例子簡單的描述了狀態機的基本結構,本文不對角色控制系統做全面的講解,只對狀態機的在角色控制系統中是如何運用做出講解。


1.我們先從Actor講起。Actor作為角色指令碼的基類,承載著角色的基本屬性,包括角色id,移動速度,座標等等,因為我們這裡講的是用狀態機來控制角色,所以角色的屬性還包括角色的當前狀態,所有狀態,狀態型別等等,還有一些對狀態操作的方法,包括初始化狀態機,初始化當前狀態,改變狀態機,更新狀態機等等,當然還有一些角色表現的方法,比如移動,改變方向,播放動畫等等,這些表現方法是通過狀態機來實現,從而實現改變狀態來驅動表現,這就是狀態機的用法。

// **********************************************************************
// Copyright (C) XM
// Author: 吳肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum Direction
{
    Front = 0,
    Back,
    Left,
    Right
}

public abstract class Actor : Base
{
    /// <summary>
    /// debug模式,程式測試
    /// </summary>
    public bool _debug;

    /// <summary>
    /// 玩家id
    /// </summary>
    public int _uid;

    /// <summary>
    /// 玩家名字
    /// </summary>
    public string _name;

    /// <summary>
    /// 移動速度
    /// </summary>
    public float _moveSpeed;

    /// <summary>
    /// 是否正在移動
    /// </summary>
    public bool _isMoving;

    /// <summary>
    /// 座標
    /// </summary>
    public Vector3 _pos;

    /// <summary>
    /// 當前狀態
    /// </summary>
    public ActorState _curState { set; get; }

    public ActorStateType _stateType;

    public Direction _direction = Direction.Front;

    /// <summary>
    /// 狀態機集合
    /// </summary>
    public Dictionary<ActorStateType, ActorState> _actorStateDic = new Dictionary<ActorStateType, ActorState>();

    /// <summary>
    /// 動畫控制器
    /// </summary>
    [HideInInspector]
    public Animator _animator;

    private Transform _transform;

    void Awake()
    {
        _transform = this.transform;
        _animator = GetComponent<Animator>();
        InitState();
        InitCurState();
    }

    /// <summary>
    /// 初始化狀態機
    /// </summary>
    protected abstract void InitState();

    /// <summary>
    ///  初始化當前狀態
    /// </summary>
    protected abstract void InitCurState();
   

    /// <summary>
    /// 改變狀態機
    /// </summary>
    /// <param name="stateType"></param>
    /// <param name="param"></param>
    public void TransState(ActorStateType stateType)
    {
        if (_curState == null)
        {
            return;
        }
        if (_curState.StateType == stateType)
        {
            return;
        }
        else
        {
            ActorState _state;
            if (_actorStateDic.TryGetValue(stateType, out _state))
            {
                _curState.Exit();
                _curState = _state;
                _curState.Enter(this);
                _stateType = _curState.StateType;
            }
        }
    }

    /// <summary>
    /// 更新狀態機
    /// </summary>
    public void UpdateState()
    {
        if (_curState != null)
        {
            _curState.Update();
        }
    }

    /// <summary>
    /// 移動 資料(狀態)驅動表現
    /// </summary>
    public virtual void Move()
    {
        //TODO 移動相關狀態
        _animator.SetInteger("Dir", (int)_direction);
        if (_debug)
        {
            //資料層位置
            _transform.position = _pos;
        }
        else
        {
            //表現層位置
            _transform.position = Vector3.Lerp(_transform.position, _pos, 100 * Time.deltaTime);
        }
    }

    /// <summary>
    /// 改變方向
    /// </summary>
    /// <param name="dir"></param>
    public void ChangeDir(Direction dir)
    {
        _direction = dir;
        if (_direction == Direction.Left)
        {
            _transform.localScale = new Vector3(-1, 1, 1);
        }
        else
        {
            _transform.localScale = new Vector3(1, 1, 1);
        }
    }

    /// <summary>
    /// 播放動畫
    /// </summary>
    /// <param name="name"></param>
    /// <param name="dir"></param>
    public void PlayAnim(string name)
    {
        _animator.SetBool("Idle", false);
        _animator.SetBool("Run", false);
        _animator.SetBool(name, true);
        _animator.SetInteger("Dir", (int)_direction);
    }
}

2.我們現在有了Actor這個角色基類,那麼我們現在就可以用它來創造很多的不同角色了。我們先來創造一個自己的角色PayerActor,然後繼承Actor,因為Actor的狀態機初始化是用虛方法的,所以我們必須在子類中去實現它,來達到不同的角色有不同的狀態。

// **********************************************************************
// Copyright (C) XM
// Author: 吳肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerActor : Actor {

    /// <summary>
    /// 搖桿
    /// </summary>
    private ETCJoystick _joystick;

    /// <summary>
    /// 初始化狀態機
    /// </summary>
    protected override void InitState()
    {
        _actorStateDic[ActorStateType.Idle] = new IdleState();
        _actorStateDic[ActorStateType.Move] = new MoveState();
    }

    /// <summary>
    /// 初始化當前狀態
    /// </summary>
    protected override void InitCurState()
    {
        _curState = _actorStateDic[ActorStateType.Idle];
        _curState.Enter(this);
    }

    void Start()
    {
        _joystick = GameObject.FindObjectOfType<ETCJoystick>();
        if (_joystick != null)
        {
            _joystick.onMoveStart.AddListener(StartMoveCallBack);
            _joystick.onMove.AddListener(MoveCallBack);
            _joystick.onMoveEnd.AddListener(EndMoveCallBack);
        }
    }

    /// <summary>
    /// 開始移動
    /// </summary>
    private void StartMoveCallBack()
    {
        TransState(ActorStateType.Move);
    }


    /// <summary>
    /// 正在移動
    /// </summary>
    /// <param name="arg0"></param>
    private void MoveCallBack(Vector2 vec2)
    {
        float value = 0.02f * _moveSpeed / Mathf.Sqrt(vec2.normalized.x * vec2.normalized.x + vec2.normalized.y * vec2.normalized.y);//勾股定理得出比例,第一個值是搖桿的比例

        _pos = new Vector3(_pos.x + vec2.x * value, _pos.y + vec2.y * value, 0);

        int angle = (int)(Mathf.Atan2(vec2.normalized.y, vec2.normalized.x) * 180 / 3.14f);
        //Debug.Log(angle);
        if (angle > 45 && angle < 135)
        {
            ChangeDir(Direction.Back);
            //Debug.Log("上");
        }
        else if (angle <= 45 && angle >= -45)
        {
            ChangeDir(Direction.Right);
            //Debug.Log("右");
        }
        else if (Mathf.Abs(angle) >= 135)
        {
            ChangeDir(Direction.Left);
            //Debug.Log("左");
        }
        else
        {
            ChangeDir(Direction.Front);
            //Debug.Log("下");
        }
    }

    /// <summary>
    /// 移動結束
    /// </summary>
    private void EndMoveCallBack()
    {
        TransState(ActorStateType.Idle);
    }

    void OnDestroy()
    {
        if (_joystick != null)
        {
            _joystick.onMoveStart.RemoveListener(StartMoveCallBack);
            _joystick.onMove.RemoveListener(MoveCallBack);
            _joystick.onMoveEnd.RemoveListener(EndMoveCallBack);
        }
    }
    
}
這裡有個可以優化的地方就是動畫控制器,由於我這裡做的是一個2D的角色,並且他有4個朝向,所以我改成了用混合樹來做,通過MoveCallBack方法傳進來的二維座標直接控制混合樹的X和Y的引數,進而改變角色的朝向,所以MoveCallBack方法裡面的實現,如果你們需要可以進行優化,就是不需要通過角度去算方向了,我就不去修改了。

3.接下來我們來講講狀態機的基類ActorState,基類包括狀態機型別,進入狀態,更新狀態,退出狀態等等。

// **********************************************************************
// Copyright (C) XM
// Author: 吳肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 角色狀態
/// </summary>
public abstract class ActorState
{

    /// <summary>
    /// 狀態機型別
    /// </summary>
    public abstract ActorStateType StateType { get; }

    /// <summary>
    /// 進入狀態
    /// </summary>
    /// <param name="param"></param>
    public abstract void Enter(params object[] param);

    /// <summary>
    /// 更新狀態
    /// </summary>
    public abstract void Update();

    /// <summary>
    /// 退出狀態
    /// </summary>
    public abstract void Exit();

}


/// <summary>
/// 角色狀態型別
/// </summary>
public enum ActorStateType
{
    Idle,
    Move,
    //...
}

4.既然我們有了狀態機的基類,那我們就可以創造出很多的狀態了,比如待機,移動,攻擊,釋放技能等等。同樣的,基類ActorState的方法也是虛方法,必須通過子類來實現,所以我們每個不同的狀態就可以各自實現自己的操作了。

// **********************************************************************
// Copyright (C) XM
// Author: 吳肖牧
// Date: 2018-04-13
// Desc: 
// **********************************************************************

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 待機狀態
/// </summary>
public class IdleState : ActorState
{
    private Actor _actor;

    public override ActorStateType StateType
    { 
        get
        {
            return ActorStateType.Idle;
        }
    }

    public override void Enter(params object[] param)
    {
        //Debug.Log("IdleState Enter");
        _actor = param[0] as Actor;
        if (_actor != null)
        {
            _actor.PlayAnim("Idle");

            _actor._isMoving = false;
            //TODO 播放動畫相關
        }
    }

    public override void Update()
    {

    }

    public override void Exit()
    {
        _actor = null;
        //Debug.Log("IdleState Exit");

    }
}

/// <summary>
/// 移動狀態
/// </summary>
public class MoveState : ActorState
{
    private Actor _actor;

    public override ActorStateType StateType
    {
        get
        {
            return ActorStateType.Move;
        }
    }

    public override void Enter(params object[] param)
    {
        //Debug.Log("MoveState Enter");
        _actor = param[0] as Actor;
        if (_actor != null)
        {
            _actor.PlayAnim("Run");

            _actor._isMoving = true;
            //TODO 播放動畫相關
        }
    }

    public override void Update()
    {
        if (_actor != null)
        {
            _actor.Move();
        }
    }

    public override void Exit()
    {
        //Debug.Log("MoveState Exit");
        _actor._isMoving = false;
        _actor = null;
    }
}

那我們是如何實現切換狀態的呢?我們回到Actor,看看改變狀態機的方法,每次切換的時候都會先把當前狀態停掉,然後進入新的狀態,再把自己的Actor傳進狀態機,然後根據狀態的需要,實現Actor裡的方法。

    /// <summary>
    /// 改變狀態機
    /// </summary>
    /// <param name="stateType"></param>
    /// <param name="param"></param>
    public void TransState(ActorStateType stateType)
    {
        if (_curState == null)
        {
            return;
        }
        if (_curState.StateType == stateType)
        {
            return;
        }
        else
        {
            ActorState _state;
            if (_actorStateDic.TryGetValue(stateType, out _state))
            {
                _curState.Exit();
                _curState = _state;
                _curState.Enter(this);
                _stateType = _curState.StateType;
            }
        }
    }

5.最後我們來講講簡單的角色管理系統ActorManager,包括角色的建立,刪除,獲取等等。其中最重要的功能就是更新所有角色狀態機UpdateActor(),所有角色的持續狀態都是通過這個方法實現的。

// **********************************************************************
// Copyright (C) XM
// Author: 吳肖牧
// Date: 2018-04-14
// Desc: 角色管理器
// **********************************************************************

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ActorManager : Singleton<ActorManager>
{

    /// <summary>
    /// 所有玩家的角色列表
    /// </summary>
    public Dictionary<int, Actor> _actorDic = new Dictionary<int, Actor>();

    // Update is called once per frame
    void Update()
    {
        UpdateActor();
    }

    /// <summary>
    /// 更新角色狀態
    /// </summary>
    private void UpdateActor()
    {
        var enumerator = _actorDic.GetEnumerator();
        while (enumerator.MoveNext())
        {
            enumerator.Current.Value.UpdateState();
        }
        enumerator.Dispose();
    }

    /// <summary>
    /// 建立角色
    /// </summary>
    /// <param name="uid">角色id</param>
    public void CreateActor(int uid)
    {
        Actor actor = null;
        if (!_actorDic.TryGetValue(uid, out actor))
        {
            GameObject go = AppFacade.Instance.GetManager<ResourceManager>(ManagerName.Resource).CreateAsset("Prefabs/Actor/Wizard");
            Camera.main.GetComponentInChildren<Cinemachine.CinemachineVirtualCamera>().Follow = go.transform;
            actor = go.GetComponent<WizardActor>();
            actor._uid = uid;
            actor._name = uid.ToString();
            actor._moveSpeed = 5;
            _actorDic[uid] = actor;
        }
        else
        {
            Debug.Log("玩家" + uid + "已經存在");
        }
    }

    /// <summary>
    /// 刪除角色
    /// </summary>
    /// <param name="uid">角色id</param>
    public void RemoveActor(int uid)
    {
        Actor actor = null;
        if (_actorDic.TryGetValue(uid, out actor))
        {
            Destroy(actor.gameObject);
            _actorDic.Remove(uid);
        }
        else
        {
            Debug.Log("玩家" + uid + "不存在");
        }
    }

    /// <summary>
    /// 獲取角色
    /// </summary>
    /// <param name="uid">角色id</param>
    /// <returns></returns>
    public Actor GetActor(int uid)
    {
        Actor actor = null;
        _actorDic.TryGetValue(uid, out actor);
        return actor;
    }
}

後面有時間的話,我會基於這篇文章再寫一篇簡單幀同步的文章。