上篇文章讲了通用状态机的实现(文末),这次讲讲如何在实际情况中去使用。这次先做一个简单的控制Animator
的状态机。
鉴于我用来做动画的模型只有Idle、Run、Die三种状态,所以就较为简单,这次只是教基本的使用,下次讲如何做AI巡逻。
动画机不介绍了,这个不会的先去学前面的教程,搭建过程略过。
如果还有走路的动画可以用x做个简单的混合树,还有这里做的不太严谨,用于练习的例子就不那么严谨了。x表示当前速度,IsDie表示是否死亡,动画机部分完成。
角色部分的话需要挂载character controller
和Animator
组件,同时记得挂载好脚本组件。
先声明枚举,这里有三种状态,实际可划分为两种:Movement和Die。枚举完工。
public enum FSMCharacterState
{
Movement,
Die
}
为了严谨先创建一个鹿的状态机基类,继承自状态机基类(具体看我上次文章,末尾有传送门),因为用animator
控制动画,character controller
控制移动,所以需要声明这两个字段,同时在构造函数里传递进来并赋值,鹿的状态机基类也完成了。
public abstract class FSMDeer : FSMBase<FSMCharacterState>
{
protected Animator deerAnimator;
protected CharacterController characterController;
protected FSMDeer(FSMManagerBase<FSMCharacterState> manager,GameObject deer) : base(manager)
{
deerAnimator = deer.GetComponent<Animator>();
characterController = deer.GetComponent<CharacterController>();
}
}
接下来该实现FSMDeerMove
与FSMDeerDie
这两个类了,移动要用character controller
实现,所以就需要重新声明OnAnimatorMove
这个方法来覆盖动画片段里的位移,这里先说一下构造函数和OnEnter两个的区别,构造函数是new时只生成一次,而OnEnter是每次进入此状态时就调用一次,与OnEnable函数相似,每次激活时调用,所以不用每次赋值的全局变量就写在构造函数里就行。
移动时肯定需要速度来控制,所以声明一个常量speed,而.characterController.Move方法需要传递一个Vec3类型的参数,为了避免gc,故声明全局变量moveStepSize,在构造函数里new,在使用时调用set方法。
public class FSMDeerMove : FSMDeer
{
private static readonly int X = Animator.StringToHash("X");
private Vector3 _moveStep;
private sbyte _speed = 8;
public FSMDeerMove(FSMManagerBase<FSMCharacterState> manager,GameObject deer) : base(manager,deer)
{
ClassState = FSMCharacterState.Movement;
_moveStep = new Vector3();
}
public sealed override FSMCharacterState ClassState { get; protected set; }
public override void OnEnter()
{
}
public override void OnUpdate()
{
}
public override void OnFixedUpdate()
{
}
public override void OnExit()
{
}
public void OnAnimatorMove()
{
}
这里不好转换死亡状态,所以使用按按键来代替。
public override void OnUpdate()
{
if (Input.GetKeyDown(KeyCode.L))
{
manager.SetCurrentState(manager.GetState(FSMCharacterState.Die));
}
}
OnAnimatorMove不需要操心状态转换,所以只做移动和动画就好,这里用虚拟轴来控制移动转换。
var h = Input.GetAxisRaw("Horizontal");
var v = Input.GetAxisRaw("Vertical");
_moveStep.Set(h * Time.deltaTime * _speed, 0, v * Time.deltaTime * _speed);
characterController.Move(_moveStep);
if (h != 0 || v != 0)
{
deerAnimator.SetFloat(X, _speed);
}
else
{
deerAnimator.SetFloat(X, 0);
}
到这里Move类代码写好了,完整类为:
public class FSMDeerMove : FSMDeer
{
private static readonly int X = Animator.StringToHash("X");
private Vector3 _moveStep;
private sbyte _speed = 8;
public FSMDeerMove(FSMManagerBase<FSMCharacterState> manager,GameObject deer) : base(manager,deer)
{
ClassState = FSMCharacterState.Movement;
_moveStep = new Vector3();
}
public sealed override FSMCharacterState ClassState { get; protected set; }
public override void OnEnter()
{
}
public override void OnUpdate()
{
if (Input.GetKeyDown(KeyCode.L))
{
manager.SetCurrentState(manager.GetState(FSMCharacterState.Die));
}
}
public override void OnFixedUpdate()
{
}
public override void OnExit()
{
}
public void OnAnimatorMove()
{
var h = Input.GetAxisRaw("Horizontal");
var v = Input.GetAxisRaw("Vertical");
_moveStep.Set(h * Time.deltaTime * _speed, 0, v * Time.deltaTime * _speed);
characterController.Move(_moveStep);
if (h != 0 || v != 0)
{
deerAnimator.SetFloat(X, _speed);
}
else
{
deerAnimator.SetFloat(X, 0);
}
}
下面开始写Die类,这个就更简单了,只用在死亡动画播放完后重新回到Movement状态即可,逻辑太少了我直接贴出来了。
public class FSMDeerDie : FSMDeer
{
private static readonly int IsDie = Animator.StringToHash("IsDie");
public FSMDeerDie(FSMManagerBase<FSMCharacterState> manager, GameObject deer) : base(manager, deer)
{
ClassState = FSMCharacterState.Die;
}
public sealed override FSMCharacterState ClassState { get; protected set; }
public override void OnEnter()
{
deerAnimator.SetBool(IsDie,true);
}
public override void OnUpdate()
{
if (deerAnimator.GetCurrentAnimatorStateInfo(0).normalizedTime > 0.95f)
{
manager.SetCurrentState(manager.GetState(FSMCharacterState.Movement));
}
}
public override void OnFixedUpdate()
{
}
public override void OnExit()
{
deerAnimator.SetBool(IsDie, false);
}
}
鹿的状态类写完了,该写管理类了,基类为abstract类型所以要继承后实现。因为鹿的move类里有OnAnimatorMove
方法,所以需要管理类内也逐帧调用,继承后声明OnAnimatorMove
方法,此时只有FSMDeerMove
中调用此方法,再声明一个全局变量_fsmDeer
,调用get方法时使用as转换当前状态,如果不为空则调用,或者使用getType来获取类型也是可以的,所以manager代码为:
public class FSMManagerDeer : FSMManagerBase<FSMCharacterState>
{
private FSMDeerMove _fsmDeer;
public FSMDeerMove FsmDeer
{
get
{
_fsmDeer = CurrentState as FSMDeerMove;
return _fsmDeer;
}
}
public FSMManagerDeer(Dictionary<FSMCharacterState, FSMBase<FSMCharacterState>> dictionary) : base(dictionary)
{
}
public void OnAnimatorMove()
{
FsmDeer?.OnAnimatorMove();
}
}
再创建一个新脚本挂载到鹿身上,只需要创建管理者类,在管理者内添加状态,设置一个入口状态就能愉快的使用啦!彻底完工!
public class DeerController : MonoBehaviour
{
private FSMManagerDeer _manager;
private void OnAnimatorMove()
{
_manager.OnAnimatorMove();
}
private void Update()
{
_manager.Update();
}
private void FixedUpdate()
{
_manager.FixedUpdate();
}
private void Awake()
{
var list = new Dictionary<FSMCharacterState, FSMBase<FSMCharacterState>>();
_manager = new FSMManagerDeer(list);
_manager.AddState(new FSMDeerDie(_manager,gameObject));
_manager.AddState(new FSMDeerMove(_manager,gameObject));
_manager.SetCurrentState(_manager.GetState(FSMCharacterState.Movement));
}
}
下节预告:AI巡逻,顺便捞捞上节文章
手把手教你做Unity中的FSM状态机(一)