为了让读者对本文知识点有一个比较清晰的了解,我制作了一张结构图,如下图,图中以移动为例子简单的描述了状态机的基本结构,本文不对角色控制系统做全面的讲解,只对状态机的在角色控制系统中是如何运用做出讲解。
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
{
///
/// debug模式,程序测试
///
public bool _debug;
///
/// 玩家id
///
public int _uid;
///
/// 玩家名字
///
public string _name;
///
/// 移动速度
///
public float _moveSpeed;
///
/// 是否正在移动
///
public bool _isMoving;
///
/// 坐标
///
public Vector3 _pos;
///
/// 当前状态
///
public ActorState _curState { set; get; }
public ActorStateType _stateType;
public Direction _direction = Direction.Front;
///
/// 状态机集合
///
public Dictionary _actorStateDic = new Dictionary();
///
/// 动画控制器
///
[HideInInspector]
public Animator _animator;
private Transform _transform;
void Awake()
{
_transform = this.transform;
_animator = GetComponent();
InitState();
InitCurState();
}
///
/// 初始化状态机
///
protected abstract void InitState();
///
/// 初始化当前状态
///
protected abstract void InitCurState();
///
/// 改变状态机
///
///
///
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;
}
}
}
///
/// 更新状态机
///
public void UpdateState()
{
if (_curState != null)
{
_curState.Update();
}
}
///
/// 移动 数据(状态)驱动表现
///
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);
}
}
///
/// 改变方向
///
///
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);
}
}
///
/// 播放动画
///
///
///
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 {
///
/// 摇杆
///
private ETCJoystick _joystick;
///
/// 初始化状态机
///
protected override void InitState()
{
_actorStateDic[ActorStateType.Idle] = new IdleState();
_actorStateDic[ActorStateType.Move] = new MoveState();
}
///
/// 初始化当前状态
///
protected override void InitCurState()
{
_curState = _actorStateDic[ActorStateType.Idle];
_curState.Enter(this);
}
void Start()
{
_joystick = GameObject.FindObjectOfType();
if (_joystick != null)
{
_joystick.onMoveStart.AddListener(StartMoveCallBack);
_joystick.onMove.AddListener(MoveCallBack);
_joystick.onMoveEnd.AddListener(EndMoveCallBack);
}
}
///
/// 开始移动
///
private void StartMoveCallBack()
{
TransState(ActorStateType.Move);
}
///
/// 正在移动
///
///
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("下");
}
}
///
/// 移动结束
///
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;
///
/// 角色状态
///
public abstract class ActorState
{
///
/// 状态机类型
///
public abstract ActorStateType StateType { get; }
///
/// 进入状态
///
///
public abstract void Enter(params object[] param);
///
/// 更新状态
///
public abstract void Update();
///
/// 退出状态
///
public abstract void Exit();
}
///
/// 角色状态类型
///
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;
///
/// 待机状态
///
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");
}
}
///
/// 移动状态
///
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里的方法。
///
/// 改变状态机
///
///
///
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
{
///
/// 所有玩家的角色列表
///
public Dictionary _actorDic = new Dictionary();
// Update is called once per frame
void Update()
{
UpdateActor();
}
///
/// 更新角色状态
///
private void UpdateActor()
{
var enumerator = _actorDic.GetEnumerator();
while (enumerator.MoveNext())
{
enumerator.Current.Value.UpdateState();
}
enumerator.Dispose();
}
///
/// 创建角色
///
/// 角色id
public void CreateActor(int uid)
{
Actor actor = null;
if (!_actorDic.TryGetValue(uid, out actor))
{
GameObject go = AppFacade.Instance.GetManager(ManagerName.Resource).CreateAsset("Prefabs/Actor/Wizard");
Camera.main.GetComponentInChildren().Follow = go.transform;
actor = go.GetComponent();
actor._uid = uid;
actor._name = uid.ToString();
actor._moveSpeed = 5;
_actorDic[uid] = actor;
}
else
{
Debug.Log("玩家" + uid + "已经存在");
}
}
///
/// 删除角色
///
/// 角色id
public void RemoveActor(int uid)
{
Actor actor = null;
if (_actorDic.TryGetValue(uid, out actor))
{
Destroy(actor.gameObject);
_actorDic.Remove(uid);
}
else
{
Debug.Log("玩家" + uid + "不存在");
}
}
///
/// 获取角色
///
/// 角色id
///
public Actor GetActor(int uid)
{
Actor actor = null;
_actorDic.TryGetValue(uid, out actor);
return actor;
}
}
后面有时间的话,我会基于这篇文章再写一篇简单帧同步的文章。