前言

最近也看了不少关于游戏 AI 的相关文章,对这方面挺感兴趣,就打算尝试实现各种 AI,然后总结,写成一个系列文章,这篇文章将介绍状态机 AI,并给出在 Unity 中的相关实现。
去年 5 月份学校的动画与游戏程序设计这门课程布置了个大作业,用 Unity 做坦克大战(虽说就是照着教程做罢了),当时就试着用状态机写了 AI。这次跟几个小伙伴参加 CUSGA(第一届中国大学生游戏开发创作大赛),做了个简化版的 RTS 游戏,其中的 AI 也是通过状态机实现的。将在这边文章中做个总结。

注:游戏目前实现的功能还并不是很多,如果看了演示视频,希望能给些相关的改进建议,等这段时间毕设搞完之后就开始继续改进。

有限状态机(FSM)

这段时间看了不少大佬的分析和文章,了解到状态机大致有有限状态机和分层状态机两种,这里将先介绍有限状态机。

定义

状态机这个概念对大家来说应该并不陌生,比如编译原理中的有穷自动机(FA),字符串算法中的自动机(AC自动机、后缀自动机、回文自动机等)等。

这里将给出定义:

  1. 包含有限个状态
  2. 可以表示为一张图,节点为状态,边为状态的改变

其形式大致与 Unity 的 Animation Controller 类似

【细说游戏 AI】这个 AI 好厉害,给我也整一个之状态机 AI-LMLPHP

实现

关于有限状态机 AI 的实现,最简单的当然就是 if... else 或是 switch 判断当前状态,然后执行相应的 action,但是这样的实现会导致当状态和动作空间变大时,代码难以维护。因此,我在实现的时候都是采用的状态模式。

关于目前在项目中使用的状态机架构,主要是五个类,分别为 Action、State、Decision、Transition、StateController,代码如下:

Action

public abstract class Action : ScriptableObject
{
    public abstract void Act(StateController controller);
}

Decision

public abstract class Decision : ScriptableObject
{
    public abstract bool Decide(StateController controller);
}

Transition

[System.Serializable]
public class Transifition
{
    public Decision decision;
    public State trueState;
    public State falseState;
}

State

[CreateAssetMenu (menuName = "AI/State")]
public class State : ScriptableObject
{
    public Action[] actions;
    public Transifition[] transitions;

    public void UpdateState(StateController controller)
    {
        DoActions(controller);
        CheckTransitions(controller);
    }

    private void DoActions(StateController controller)
    {
        for(int i = 0; i < actions.Length; i++)
        {
            actions[i].Act(controller);
        }
    }

   private void CheckTransitions(StateController controller)
    {
        for(int i = 0; i < transitions.Length; i++)
        {
            bool decisionSucceeded = transitions[i].decision.Decide(controller);

            State transitionState = decisionSucceeded ? transitions[i].trueState : transitions[i].falseState;

            controller.TransitionToState(transitionState);

        }
    }
}

StateController

public class StateController : MonoBehaviour
{
    public State currentState;
    public State remainState;
    /*
    	其他成员变量
    */

    private bool aiActive = true;

    private void Awake()
    {
        // 自身成员变量设置
    }

    // 用于 Manager 类来设置 AI
    public void SetupAI(/* 参数 */)
    {
        // 需要设置的东西
    }

    // 用于状态的改变
    public void TransitionToState(State nextState)
    {
        if(nextState != remainState)
        {
            //Debug.Log(transform.gameObject.name + " start" + nextState);
            currentState = nextState;
            OnExitState();
        }
    }

    // 用于判断进入该状态是否经过 duration 时间
    public bool CheckifCountDownElapsed(float duration)
    {
        stateTimeElapsed += Time.deltaTime;
        return (stateTimeElapsed >= duration);
    }

    public void OnExitState()
    {
        stateTimeElapsed = 0;
    }

    void Update()
    {
        if (!aiActive)
            return;
        currentState.UpdateState(this);
    }
}

使用

以上便是这几个类的相关代码,下面来分别讲一讲如何使用

  1. Action 作为抽象类,表示在某一 State 下,AI 将会执行的动作,通过继承的方式实现需要执行的动作,比如要实现 AI 的移动的话

    [CreateAssetMenu(menuName = "AI/Actions/Player/Move")]
    public class MoveAction : Action
    {
        public State standby;
    
        public override void Act(StateController controller)
        {
            Move(controller);
        }
    
        private void Move(StateController controller)
        {
            controller.navMeshAgent.SetDestination(controller.targetPoint + controller.RelativePosition);
            controller.navMeshAgent.isStopped = false;
    
            if (controller.navMeshAgent.remainingDistance <= controller.navMeshAgent.stoppingDistance && !controller.navMeshAgent.pathPending)
            {
                // 到目标点后,改变状态为 standby
                controller.navMeshAgent.isStopped = true;
                controller.TransitionToState(standby);
            }
        }
    }
    
  2. Decision 也是抽象类,在某一 State 下,AI 通过 Decision 的 Decide 来判断是否满足某一条件,比如下面场景:当 AI 在移动过程中遇到敌人则需要进入战斗状态,那么此时需要实现的 Decision 如下

    [CreateAssetMenu (menuName = "AI/Decisions/Player/Encounter")]
    public class EncounterDecision : Decision
    {
        // 用于玩家 AI 在前往目的地的过程中做决策
        public override bool Decide(StateController controller)
        {
            bool encounter = Encounter(controller);
            return encounter;
        }
    
        private bool Encounter(StateController controller)
        {
            Collider[] objects = Physics.OverlapSphere(controller.transform.position, controller.stats.attackRange * controller.stats.visionRange, 1 << 9);
            foreach (Collider c in objects)
            {
                // TODO: 设置攻击目标
                controller.attackObject = c;
                return true;
            }
            return false;
        }
    }
    
  3. Transition 则用于 State 之间的转换,代表了能从当前状态可以转到的状态,其包含了 Decision、true State 和 false State。

  4. State 则表示状态,其包含了 Actions 和 Transitions。

  5. StateController 则表示当前 AI,所有有关 AI 的属性都可放置在其中,比如在我们做的游戏 Demo 中,StateController 就表示了兵团中的士兵,其包含了 Health、Attack、NavemeshAgent、AnimationController 等属性,我们在游戏中可通过 Manager 相关的类来操作这些 StateController,从而控制 AI 的启动、暂停等。

当实现了相关的类之后,便可在 Unity 中通过创建资源的方式来创建 State、Decision 和 Action,然后拼成我们需要的资源。

【细说游戏 AI】这个 AI 好厉害,给我也整一个之状态机 AI-LMLPHP

小节

本打算五一期间就把这篇文章写出来的,但是中间出去玩了两天,然后又咸鱼了几天,就拖到了现在。中间也在一直思考几个问题:

  1. 要如何将现在实现的这个状态机改进成分层状态机,目前暂时还想不到,感觉这个分层的设计可能需要结合实际场景(希望大佬们能够提供一些思路 orz
  2. 如何在当前状态机框架中更好地实现动画的表现,我目前是在实现 Action 的时候通过 Animation Controller 的状态,以及执行时间来改变当前的动画,但总感觉不是很好。(主要最近在知乎上看到林爷的文章之后感觉可以设计一个专门控制动画的类)

继续整毕设去了 orz,想想还有一个月多一点就要毕业了,好快- -

05-08 02:20