作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Garegin Tadevosyan的头像

Garegin Tadevosyan

Garegin是一名精通Unity和c#的游戏开发者. 他为游戏化的操场设备创建了一个网络协议, 曾担任一家教育游戏初创公司的首席技术官, 他是一家跨国社交赌场团队的游戏开发者.

Previously At

微软亚美尼亚创新中心
Share

在竞争激烈的游戏世界中, 开发者努力为那些与我们创造的非玩家角色(npc)互动的人提供有趣的用户体验. 开发人员可以通过使用有限状态机(fsm)进行创建来交付这种交互性 AI 模拟npc智能的解决方案.

人工智能的趋势已经转向行为树,但fsm仍然具有相关性. 他们以这样或那样的方式融入了几乎所有的电子游戏中.

Anatomy of an FSM

FSM是一种计算模型,在这种模型中,在有限的假设状态中,一次只能有一种状态是活跃的. FSM根据条件或输入从一种状态转换到另一种状态. 其核心组成部分包括:

ComponentDescription
StateOne of a finite set of options indicating the current overall condition of an FSM; any given state includes an associated set of actions
Action当FSM查询状态时,状态会做什么
Decision发生转换时建立的逻辑
Transition状态变化的过程

虽然我们将从人工智能实施的角度关注fsm,但诸如 animation state machines and general game states 也在FSM的保护伞下.

Visualizing an FSM

让我们以经典街机游戏《欧博体育app下载》为例. 在游戏的初始状态(“追逐”状态), npc是追逐并最终超过玩家的彩色幽灵. 每当玩家吃下能量球并获得能量提升时,幽灵就会进入逃避状态, 获得吃鬼的能力. The ghosts, now blue in color, 躲避玩家,直到升级时间结束,幽灵转换回追逐状态, 它们原本的行为和颜色被恢复.

吃豆人的幽灵总是处于两种状态之一:追逐或逃避. 当然,我们必须提供两种转换——一种是从追逐到逃避,另一种是从逃避到追逐:

图:左边是追逐状态. 一个箭头(表明玩家吃了能量球)导致右边的逃避状态. 第二个箭头(表示能量球超时)指向左边的追逐状态.
《欧博体育app下载》幽灵状态之间的转换

The finite-state machine, by design, 查询当前状态。, 哪个查询该状态的决策和操作. 下图代表了我们的《欧博体育app下载》例子,并展示了检查玩家升级状态的决策. 如果升级开始了,npc就会从追逐变成逃避. 如果升级结束,npc会从逃避转变为追逐. 最后,如果没有升级改变,就不会发生过渡.

菱形图表示一个循环:从左边开始, 有一个追逐状态暗示着一个相应的动作. 然后追逐状态指向顶部, 这里有一个决定:如果玩家吃了能量球, 我们继续逃避状态,逃避右边的动作. 逃避状态指向底部的一个决定:如果能量球超时, 我们继续回到起点.
吃豆人幽灵FSM组件

Scalability

FSMs让我们可以自由地构建模块化的AI. 例如,通过一个新动作,我们可以创造一个具有新行为的NPC. Thus, 我们可以将一个新动作——吃掉能量球——归因于我们的《欧博体育app下载》幽灵, 让它能够在躲避玩家的同时吃掉能量球. 我们可以重用现有的操作、决策和转换来支持这种行为.

因为开发一个独特的NPC所需的资源很少, 我们能够很好地满足多个独特npc不断发展的项目需求. 另一方面,过多的状态和转换会让我们陷入 spaghetti-state machine- FSM连接过多,难以调试和维护.

在Unity中实现FSM

来演示如何实现有限状态机 Unity,让我们创造一款简单的潜行游戏. 我们的架构将包含 ScriptableObjects, 哪些是可以在整个应用程序中存储和共享信息的数据容器, 这样我们就不需要复制它了. ScriptableObjectS能够进行有限的处理,例如调用操作和查询决策. In addition to Unity的官方文档, the older 使用可脚本对象的游戏架构 talk 仍然是一个很好的资源,如果你想深入研究.

Before we add AI to this 初始准备编译项目,考虑建议的架构:

图:七个相互连接的盒子, 按外观顺序描述的, 从左/上:标记basestatemmachine的框包括+ CurrentState: BaseState. basestatemmachine用一个双向箭头连接到BaseState. 标记BaseState的框包括+ Execute(basestatemmachine): void. BaseState用一个双向箭头连接到basestatemmachine. 单向箭头从State和RemainInState连接到BaseState. 标签为State的框包括+ Execute(basestatemmachine): void, + Actions: List<Action&,和+ Transition: List<Transition>. State用单向箭头连接到BaseState, 使用标记为“1”的单向箭头进行操作,,并使用标记为“1”的单向箭头进行过渡.标记为RemainInState的方框包括+ Execute(basestatemmachine): void. RemainInState用单向箭头连接到BaseState. 标记为Action的框包括+ Execute(basestatemmachine): void. 从State方向标记为“1”的单向箭头连接到Action. 标记为Transition的框包括+ Decide(basestatemmachine): void, + TransitionDecision:决定, + TrueState: BaseState, and + false: BaseState. Transition用单向箭头连接到Decision. 从State方向标记为“1”的单向箭头连接到Transition. 标记为Decision的框包含+ Decide(basestatemmachine): bool.
FSM架构建议

在我们的样本游戏中,敌人(一个由蓝色胶囊代表的NPC)在巡逻. 当敌人看到玩家时(用灰色胶囊表示), 敌人开始跟着玩家:

图:五个相互连接的盒子, 按外观顺序描述的, 从左/上:标有“巡逻”的方框连接到标有“如果玩家在视线范围内”的方框, 并在标有“巡逻行动”的方框上加上一个标有“状态”的单向箭头.“标有IF播放器的盒子在视线范围内, 附加一个标签“决定”," just below the box. 标记为“如果玩家在视线内”的方框与标记为“追逐”的方框用单向箭头连接. 从标有“巡逻”的方框发出的单向箭头连接到标有“玩家是否在视线范围内”的方框. 标记为Chase的框连接到标记为Chase Action的框,并带有一个单向箭头,标记为“状态”.“从标记为‘如果玩家在视线范围内’的方框发出的单向箭头连接到标记为‘追逐’的方框. 一个单向箭头从标有“巡逻”的框连接到标有“巡逻行动”的框. 单向箭头从标记为Chase的框连接到标记为Chase Action的框.
我们的样本潜行游戏FSM的核心组件

In contrast with Pac-Man, 我们游戏中的敌人一旦跟随玩家便不会回到默认状态(“巡逻”).

Creating Classes

让我们从创建类开始. In a new scripts 文件夹中,我们将以c#脚本的形式添加所有建议的架构构建块.

Implementing the BaseStateMachine Class

The BaseStateMachine class is the only MonoBehavior 我们将添加它来访问启用ai的npc. 为简单起见,我们的 BaseStateMachine will be bare-bones. If we wanted to, however, 我们可以添加一个继承自定义FSM,它存储额外的参数和对额外组件的引用. 注意,代码将无法正确编译,直到我们添加了 BaseState 类,我们将在稍后的教程中进行.

The code for BaseStateMachine 引用并执行当前状态以执行操作,并查看是否有必要进行转换:

using UnityEngine;

namespace Demo.FSM
{
    公共类basestatemmachine: MonoBehaviour
    {
        [SerializeField] private BaseState _initialState;

        private void Awake()
        {
            CurrentState = _initialState;
        }

        public BaseState CurrentState { get; set; }

        private void Update()
        {
            CurrentState.Execute(this);
        }
    }
}

Implementing the BaseState Class

Our state is of the type BaseState, which we derive from a ScriptableObject. BaseState 包含一个方法, Execute, taking BaseStateMachine 作为它的参数并传递给它动作和转换. This is how BaseState looks:

using UnityEngine;

namespace Demo.FSM
{
    公共类BaseState: ScriptableObject
    {
        public virtual void Execute(BaseStateMachine) {}
    }
}

Implementing the State and RemainInState Classes

派生两个类 BaseState. First, we have the State class, 哪些存储对操作和转换的引用, 包括两个列表(一个用于操作), 另一个用于过渡), 覆盖并呼叫基地 Execute 关于动作和转换:

using System.Collections.Generic;
using UnityEngine;

namespace Demo.FSM
{
    [CreateAssetMenu(menuName = "FSM/State")]
    公共密封类状态:BaseState
    {
        public List Action = new List();
        public List Transitions = new List();

        Execute(BaseStateMachine)
        {
            foreach (var action in action)
                action.Execute(machine);

            foreach(在Transitions中添加var transition)
                transition.Execute(machine);
        }
    }
}

Second, we have the RemainInState 类,它告诉FSM何时不执行转换:

using UnityEngine;

namespace Demo.FSM
{
    [CreateAssetMenu(menuName = "FSM/Remain InState", fileName = "RemainInState")]
    公共密封类RemainInState: BaseState
    {
        
    }
}

注意,这些类将不会编译,直到我们添加了 FSMAction, Decision, and Transition classes.

Implementing the FSMAction Class

In the FSM架构建议图, the base FSMAction class is labeled “Action.“然而,我们将创建基地 FSMAction class and use the name FSMAction (since Action is already in use by the .NET System namespace).

FSMAction, a ScriptableObject,不能独立处理函数,所以我们将其定义为抽象类. 随着开发的进行,我们可能需要一个操作来服务多个状态. 幸运的是,我们可以联想 FSMAction 我们希望有多少州就有多少州.

The FSMAction 抽象类是这样的:

using UnityEngine;

namespace Demo.FSM
{
    公共抽象类FSMAction: ScriptableObject
    {
        执行BaseStateMachine (BaseStateMachine);
    }
}

Implementing the Decision and Transition Classes

为了完成我们的FSM,我们将定义另外两个类. First, we have Decision,一个抽象类,所有其他决策都将从中定义它们的自定义行为:

using UnityEngine;

namespace Demo.FSM
{
    公共抽象类决策:ScriptableObject
    {
        public abstract bool决定(basestatemmachine state);
    }
}

The second class, Transition, contains the Decision object and two states:

  • 要过渡到的状态 Decision yields true.
  • 另一个要过渡到的状态 Decision yields false.

It looks like this:

using UnityEngine;

namespace Demo.FSM
{
    [CreateAssetMenu(menuName = "FSM/Transition")]
    公共密封类Transition: ScriptableObject
    {
        公共决策;决策;
        public baseestate;
        public BaseState;

        执行BaseStateMachine (BaseStateMachine)
        {
            if(Decision.Decide(stateMachine) && !(不动产为保留状态))
                stateMachine.CurrentState = TrueState;
            else if(!(FalseState是RemainInState))
                stateMachine.CurrentState = FalseState;
        }
    }
}

Everything we have built 到目前为止,编译应该没有任何错误. 如果你遇到问题,检查你的Unity编辑器版本,如果过时可能会导致错误. 确保所有文件都从原始项目文件夹中正确地克隆出来,并且所有公开访问的变量都没有被声明为私有.

创建自定义动作和决策

现在,随着繁重的工作的完成,我们准备在一个新的 scripts folder.

Implementing the Patrol and Chase Classes

When we analyze the 我们的样本潜行游戏FSM图的核心组件,我们看到我们的NPC可能处于两种状态之一:

  1. Patrol state —与状态相关的有:
    • 一个行动:NPC访问世界各地的随机巡逻点.
    • 一个过渡:NPC检查玩家是否在视线范围内,如果是,则过渡到追逐状态.
    • 一个决定是:NPC检查玩家是否在视线范围内.
  2. Chase state —与状态相关联的是:
    • 一个动作:NPC追逐玩家.

我们可以通过Unity的GUI重用我们现有的过渡实现,我们将在后面讨论. 这就剩下了两个动作(PatrolAction and ChaseAction)和我们编码的决定.

巡逻国家行动(源于基地) FSMAction) overrides the Execute 方法得到两个分量:

  1. PatrolPoints该公司追踪巡逻点.
  2. NavMeshAgent, Unity在3D空间中的导航实现.

然后重写检查人工智能代理是否已经到达目的地, if so, 移动到下一个目的地. It looks like this:

using Demo.Enemy;
using Demo.FSM;
using UnityEngine;
using UnityEngine.AI;

namespace Demo.MyFSM
{
    [CreateAssetMenu(menuName = "FSM/Actions/Patrol")]
    公共类PatrolAction: FSMAction
    {
        执行BaseStateMachine (BaseStateMachine)
        {
            var navMeshAgent = statemmachine . var.GetComponent();
            var patrolPoints = statemmachine.GetComponent();

            if (patrolPoints.HasReached (navMeshAgent))
                navMeshAgent.SetDestination (patrolPoints.GetNext().position);
        }
    }
}

我们可能需要考虑缓存 PatrolPoints and NavMeshAgent components. 缓存将允许我们共享 ScriptableObjectS用于代理之间的操作,而不会对运行产生性能影响 GetComponent 在有限状态机的每个查询中.

控件中不能缓存组件实例 Execute method. 因此,我们将添加一个自定义 GetComponent method to BaseStateMachine. Our custom GetComponent 会在第一次调用实例时缓存它吗, 在连续调用时返回缓存的实例. 供参考,这是实现 BaseStateMachine with caching:

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Demo.FSM
{
    公共类basestatemmachine: MonoBehaviour
    {
        [SerializeField] private BaseState _initialState;
        private Dictionary _cachedComponents;
        private void Awake()
        {
            CurrentState = _initialState;
            _cachedComponents = new Dictionary();
        }

        public BaseState CurrentState { get; set; }

        private void Update()
        {
            CurrentState.Execute(this);
        }

        public new T GetComponent() where T : Component
        {
            if(_cachedComponents.ContainsKey(typeof(T)))
                return _cachedComponents[typeof(T)] as T;

            var component = base.GetComponent();
            if(component != null)
            {
                _cachedComponents.添加(typeof (T)组件);
            }
            return component;
        }

    }
}

Like its counterpart PatrolAction, the ChaseAction class overrides the Execute method to get PatrolPoints and NavMeshAgent components. 相反,在检查AI agent是否到达目的地后 ChaseAction 集体诉讼将目标设置为 Player.position:

using Demo.Enemy;
using Demo.FSM;
using UnityEngine;
using UnityEngine.AI;

namespace Demo.MyFSM
{
    [CreateAssetMenu(menuName = "FSM/Actions/Chase")]
    ChaseAction: FSMAction
    {
        执行BaseStateMachine (BaseStateMachine)
        {
            var navMeshAgent = statemmachine . var.GetComponent();
            var enemySightSensor = statemmachine.GetComponent();

            navMeshAgent.SetDestination (enemySightSensor.Player.position);
        }
    }
}

Implementing the InLineOfSightDecision Class

The final piece is the InLineOfSightDecision 类,它继承基类 Decision and gets the EnemySightSensor 组件来检查玩家是否在NPC的视线范围内:

using Demo.Enemy;
using Demo.FSM;
using UnityEngine;
namespace Demo.MyFSM
{
    [CreateAssetMenu(menuName = "FSM/Decisions/In Line Of Sight")]
    公共类InLineOfSightDecision:决定
    {
        public override bool决定(BaseStateMachine)
        {
            var eneminlineofsight = statemmachine.GetComponent();
            返回enemyInLineOfSight.Ping();
        }
    }
}

将行为附加到状态

我们终于准备好将行为附加到 Enemy agent. 这些都是在Unity编辑器的项目窗口中创建的.

Adding the Patrol and Chase States

让我们创建两个状态,分别命名为“Patrol”和“Chase”:

  • Right Click > Create > FSM > State

在这里,我们也创建一个 RemainInState object:

  • Right Click > Create > FSM > Remain In State

现在,是时候创建我们刚刚编写的动作了:

  • Right Click > Create > FSM > Action > Patrol
  • Right Click > Create > FSM > Action > Chase

To code the Decision:

  • Right Click > Create > FSM > Decisions > In Line of Sight

启用从的转换 PatrolState to ChaseState,让我们首先创建转换脚本对象:

  • Right Click > Create > FSM > Transition
  • Choose a name you like. 我叫我的斑点敌人.

我们将按如下方式填充检查器窗口:

Spotted Enemy (Transition) screen includes four lines: Script's value is set to "Transition" and is grayed out. Decision's value is set to "LineOfSightDecision (In Line Of Sight)." True State's value is set to "ChaseState (State)." False State's value is set to "RemainInState (Remain In State)."
填写被发现的敌人(过渡)检查窗口

然后我们将完成如下的Chase State检查器对话框:

Chase State (State)屏幕以一个标签“Open”开始.在“Script”标签旁边选择了“State”. 在“Action”标签旁边,选择“1”. 从“Action”下拉菜单中选择“Element 0 Chase Action (Chase Action)”. 后面有一个正号和一个负号. 在“过渡”标签旁边,选择“0”. 在“过渡”下拉菜单中,显示“列表为空”. 后面有一个正号和一个负号.
填写Chase State Inspector Window

接下来,我们将完成巡逻状态对话框:

巡逻状态(State)屏幕以一个标签“Open”开始.在“Script”标签旁边选择了“State”. 在“Action”标签旁边,选择“1”. 从“行动”下拉菜单中,选择“元素0巡逻行动(巡逻行动)”. 后面有一个正负号. 在“Transitions”标签旁边,选择“1”. 在“过渡”下拉菜单中,显示“Element 0 SpottedEnemy(过渡)”. 后面有一个正号和一个负号.
填写巡逻州督察窗口

Finally, we’ll add the BaseStateMachine 组件到敌人对象:在Unity编辑器的项目窗口, 打开SampleScene资源, 从层次面板中选择敌人对象, and, in the Inspector window, select Add Component > Base State Machine:

基本状态机(脚本)屏幕:在灰色的“脚本”标签旁边, “basestatemmachine”被选中并显示为灰色. 在“初始状态”标签旁边,选择了“PatrolState (State)”.
添加基本状态机(脚本)组件

对于任何问题,请再次检查你的游戏对象是否配置正确. 例如,确认敌人对象包含 PatrolPoints 脚本组件和对象 Point1, Point2, etc. 错误的编辑器版本可能会丢失此信息.

现在你已经准备好玩样例游戏,并观察到当玩家进入敌人的视线时敌人会跟着玩家.

使用FSMs创造有趣的交互式用户体验

在这个有限状态机教程中,我们创建了一个高度模块化的基于fsm的AI(以及相应的 GitHub repo),我们可以在未来的项目中重复使用. 由于这种模块化,我们总是可以通过引入新组件来增加AI的能力.

但是我们的架构也为图形优先的FSM设计铺平了道路, 这将把我们的开发者体验提升到一个新的专业水平. 这样我们就可以更快地为我们的游戏创造fsm,并且具有更好的创意准确性.

关于总博客的进一步阅读:

Understanding the basics

  • 什么是有限状态机?

    有限状态机(FSM)是一种计算模型. 在一个有限状态机中,在任何给定的时间,只有有限个假设状态中的一个可以是活动的.

  • 有限状态机是如何工作的?

    有限状态机在响应输入或条件时从一种状态转换到另一种状态.

  • 计算机是有限状态机吗?

    计算机不是有限状态机. 计算机是一个物理对象,而有限状态机是一个计算模型.

  • 有限状态机是如何实现的?

    有限状态机是通过编码和添加AI编码类来实现的, 创建自定义操作和决策, and attaching behaviors.

聘请Toptal这方面的专家.
Hire Now
Garegin Tadevosyan的头像
Garegin Tadevosyan

Located in Yerevan, Armenia

Member since July 1, 2021

About the author

Garegin是一名精通Unity和c#的游戏开发者. 他为游戏化的操场设备创建了一个网络协议, 曾担任一家教育游戏初创公司的首席技术官, 他是一家跨国社交赌场团队的游戏开发者.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

微软亚美尼亚创新中心

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.