Unity引擎2D游戏开发,有限状态机&抽象类多态

发布时间 2023-12-27 15:41:20作者: 心霖の雨

状态机与抽象类

观察如下代码:

public class AttackFinish : StateMachineBehaviour
{
    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.GetComponent<PlayerController>().isAttack = true;
    }

    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    //override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    //{
    //    
    //}

    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        animator.GetComponent<PlayerController>().isAttack = false;
    }

    // OnStateMove is called right after Animator.OnAnimatorMove()
    //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    //{
    //    // Implement code that processes and affects root motion
    //}

    // OnStateIK is called right after Animator.OnAnimatorIK()
    //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    //{
    //    // Implement code that sets up animation IK (inverse kinematics)
    //}
}

这继承的就是一个状态机,在此类实现了父类的部分方法。当在不同的状态时,执行着不同的流程。比如,当状态进入时OnStateEnter(),当状态正在持续刷新时OnStateUpdate(),当状态退出时OnStateExit()等等

接着,查看继承的父类StateMachineBehaviour
(一部分)

public abstract class StateMachineBehaviour : ScriptableObject
{
    public virtual void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    }

    public virtual void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    }

    public virtual void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    }

    public virtual void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    }

    public virtual void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    }
}

可以看到这个状态机类,是一个抽象方法(abstract class),特征是内部只有方法的声明,没有方法体

创建一个状态机

在Scripts的Enemy文件夹下面创建一个名为BaseState的C#脚本,作为一个基本的状态机抽象类
image

在内部创建四个状态方法

public abstract class BaseState
{
    public abstract void OnEnter();
    public abstract void LogicUpdate();
    public abstract void PhysicsUpdate();
    public abstract void OnExit();
}

状态进入OnEnter(),逻辑更新LogicUpdate(),物理更新PhysicsUpdate(),状态退出OnExit()

再在Enemy文件夹下面创建一个BoarPatrolState的C#脚本,继承上面所创建的BaseState
image

public class BoarPatrolState : BaseState
{
    public override void LogicUpdate()
    {
        throw new System.NotImplementedException();
    }

    public override void OnEnter()
    {
        throw new System.NotImplementedException();
    }

    public override void OnExit()
    {
        throw new System.NotImplementedException();
    }

    public override void PhysicsUpdate()
    {
        throw new System.NotImplementedException();
    }
}

在Enemy代码中引用创建的状态机

Enemy中创建三个变量

// 当前状态
private BaseState currentState;
// 巡逻状态
protected BaseState patrolState;
// 追击状态
protected BaseState chaseState;

创建一个OnEnable()方法,为对象启用时执行某个状态

private void OnEnable()
{
    currentState = patrolState;
    currentState.OnEnter();
}

Update()方法中添加一行代码,调用逻辑状态LogicUpdate()

currentState.LogicUpdate();

FixedUpdate()方法中添加一行代码,调用物理状态PhysicsUpdate()

currentState.PhysicsUpdate();

创建一个OnDisable()方法,在对象销毁时执行某种状态

private void OnDisable()
{
    currentState.OnExit();
}

这样就做好了敌人对象在创建完成后的一系列状态。敌人若发现了玩家,会进入追击状态,则可以在BoarPatrolState类中的LogicUpdate()更改状态为chaseState,紧接着执行相关一系列的方法

有限状态机,在一定的条件下,只会执行一种状态。例如追击只会执行追击状态,巡逻只会执行巡逻状态,它执行的状态是有限的,并不会执行多种状态。

实现敌人的有限状态机

先清空Boar类中的Move()方法,稍后会在创建的BoarPatrolState类中的PhysicsUpdate()进行编写

public class Boar : Enemy
{

}

需要知道是什么敌人在执行状态(会有多个敌人的种类),因此在BaseState中创建一个全局变量,用于标记敌人的种类

protected Enemy currentEnemy;

并在OnEnter()方法上,添加一个形参。这样也能够在调用的位置传入敌人种类

public abstract void OnEnter(Enemy enemy);

在Enemy的OnEnable()方法中,调用OnEnter()的位置传入this当前敌人

private void OnEnable()
{
    currentState = patrolState;
    currentState.OnEnter(this);
}

在BoarPatrolState中的OnEnter()方法中,将传入的enemy赋值给currentEnemy,以便后续对其进行逻辑编写

public override void OnEnter(Enemy enemy)
{
    currentEnemy = enemy;
}

将Enemy中Update()方法内部撞墙等待的代码移植到BoarPatrolState类LogicUpdate()方法中

public override void LogicUpdate()
{
    if (!currentEnemy.physicsCheck.isGround || (currentEnemy.physicsCheck.touchLeftWall && currentEnemy.faceDirect.x < 0) || (currentEnemy.physicsCheck.touchRightWall && currentEnemy.faceDirect.x > 0))
    {
          currentEnemy.wait = true;
          currentEnemy.animator.SetBool("walk", false);
    } else
    {
          currentEnemy.animator.SetBool("walk", true);
    }
}

会有无法找到变量的问题,在各自变量前,添加“currentEnemy.”即可正确引用。还需要注意,这些变量的访问修饰符需要改为public。与此同时,这些变量会显示在Unity的Inspector窗口。如果不需要显示,在变量前添加“[HideInInspector]”,即:

[HideInInspector] public Animator animator;
[HideInInspector] public PhysicsCheck physicsCheck;

现在需要进入其他的状态要怎么进入呢?比如在Enemy中创建的三个全局变量的状态?

准备在Boar类中重写Enemy中的Awake()方法。在此之前,先将EnemyAwake()方法修改为protected virtual

protected virtual void Awake()
{
    rb = GetComponent<Rigidbody2D>();
    animator = GetComponent<Animator>();
    physicsCheck = GetComponent<PhysicsCheck>();
    waitingTimeCounter = waitingTime;
    currentSpeed = normalSpeed;
}

接着Boar中重写Awake()方法,并将patrolState引用为BoarPatralState()

protected override void Awake()
{
    base.Awake();
    patrolState = new BoarPatrolState();
}

现在patrolState引用为BoarPatrolState对象,那么Enemy内OnEnable()中的OnEnter就可以正常执行野猪敌人的状态

将Update()中TimeCounter()移动至currentState.LogicUpdate()下方,因为LogicUpdate()中执行了撞墙等待,所以在此之后开始计时

private void Update()
{
    // 实时方向
    faceDirect = new Vector3(-transform.localScale.x, 0, 0);
    currentState.LogicUpdate();
    TimeCounter();
}

在OnExit()方法,也设置walk的动画为false

public override void OnExit()
{
    currentEnemy.animator.SetBool("walk", false);
}