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的狀態機初始化是用虛方法的,所以我們必須在子類中去實現它,來達到不同的角色有不同的狀態。
這裡有個可以優化的地方就是動畫控制器,由於我這裡做的是一個2D的角色,並且他有4個朝向,所以我改成了用混合樹來做,通過MoveCallBack方法傳進來的二維座標直接控制混合樹的X和Y的引數,進而改變角色的朝向,所以MoveCallBack方法裡面的實現,如果你們需要可以進行優化,就是不需要通過角度去算方向了,我就不去修改了。// ********************************************************************** // 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); } } }
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;
}
}
後面有時間的話,我會基於這篇文章再寫一篇簡單幀同步的文章。