为了让读者对本文知识点有一个比较清晰的了解,我制作了一张结构图,如下图,图中以移动为例子简单的描述了状态机的基本结构,本文不对角色控制系统做全面的讲解,只对状态机的在角色控制系统中是如何运用做出讲解。
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; } }
后面有时间的话,我会基于这篇文章再写一篇简单帧同步的文章。