作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Garegin是一名精通Unity和c#的游戏开发者. 他为游戏化的操场设备创建了一个网络协议, 曾担任一家教育游戏初创公司的首席技术官, 他是一家跨国社交赌场团队的游戏开发者.
在竞争激烈的游戏世界中, 开发者努力为那些与我们创造的非玩家角色(npc)互动的人提供有趣的用户体验. 开发人员可以通过使用有限状态机(fsm)进行创建来交付这种交互性 AI 模拟npc智能的解决方案.
人工智能的趋势已经转向行为树,但fsm仍然具有相关性. 他们以这样或那样的方式融入了几乎所有的电子游戏中.
FSM是一种计算模型,在这种模型中,在有限的假设状态中,一次只能有一种状态是活跃的. FSM根据条件或输入从一种状态转换到另一种状态. 其核心组成部分包括:
Component | Description |
---|---|
State | One 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的保护伞下.
让我们以经典街机游戏《欧博体育app下载》为例. 在游戏的初始状态(“追逐”状态), npc是追逐并最终超过玩家的彩色幽灵. 每当玩家吃下能量球并获得能量提升时,幽灵就会进入逃避状态, 获得吃鬼的能力. The ghosts, now blue in color, 躲避玩家,直到升级时间结束,幽灵转换回追逐状态, 它们原本的行为和颜色被恢复.
吃豆人的幽灵总是处于两种状态之一:追逐或逃避. 当然,我们必须提供两种转换——一种是从追逐到逃避,另一种是从逃避到追逐:
The finite-state machine, by design, 查询当前状态。, 哪个查询该状态的决策和操作. 下图代表了我们的《欧博体育app下载》例子,并展示了检查玩家升级状态的决策. 如果升级开始了,npc就会从追逐变成逃避. 如果升级结束,npc会从逃避转变为追逐. 最后,如果没有升级改变,就不会发生过渡.
FSMs让我们可以自由地构建模块化的AI. 例如,通过一个新动作,我们可以创造一个具有新行为的NPC. Thus, 我们可以将一个新动作——吃掉能量球——归因于我们的《欧博体育app下载》幽灵, 让它能够在躲避玩家的同时吃掉能量球. 我们可以重用现有的操作、决策和转换来支持这种行为.
因为开发一个独特的NPC所需的资源很少, 我们能够很好地满足多个独特npc不断发展的项目需求. 另一方面,过多的状态和转换会让我们陷入 spaghetti-state machine- FSM连接过多,难以调试和维护.
来演示如何实现有限状态机 Unity,让我们创造一款简单的潜行游戏. 我们的架构将包含 ScriptableObject
s, 哪些是可以在整个应用程序中存储和共享信息的数据容器, 这样我们就不需要复制它了. ScriptableObject
S能够进行有限的处理,例如调用操作和查询决策. In addition to Unity的官方文档, the older 使用可脚本对象的游戏架构 talk 仍然是一个很好的资源,如果你想深入研究.
Before we add AI to this 初始准备编译项目,考虑建议的架构:
在我们的样本游戏中,敌人(一个由蓝色胶囊代表的NPC)在巡逻. 当敌人看到玩家时(用灰色胶囊表示), 敌人开始跟着玩家:
In contrast with Pac-Man, 我们游戏中的敌人一旦跟随玩家便不会回到默认状态(“巡逻”).
让我们从创建类开始. In a new scripts
文件夹中,我们将以c#脚本的形式添加所有建议的架构构建块.
BaseStateMachine
ClassThe 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);
}
}
}
BaseState
ClassOur 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) {}
}
}
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.
FSMAction
ClassIn 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);
}
}
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.
Patrol
and Chase
ClassesWhen we analyze the 我们的样本潜行游戏FSM图的核心组件,我们看到我们的NPC可能处于两种状态之一:
我们可以通过Unity的GUI重用我们现有的过渡实现,我们将在后面讨论. 这就剩下了两个动作(PatrolAction
and ChaseAction
)和我们编码的决定.
巡逻国家行动(源于基地) FSMAction
) overrides the Execute
方法得到两个分量:
PatrolPoints
该公司追踪巡逻点.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. 缓存将允许我们共享 ScriptableObject
S用于代理之间的操作,而不会对运行产生性能影响 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);
}
}
}
InLineOfSightDecision
ClassThe 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编辑器的项目窗口中创建的.
Patrol
and Chase
States让我们创建两个状态,分别命名为“Patrol”和“Chase”:
在这里,我们也创建一个 RemainInState
object:
现在,是时候创建我们刚刚编写的动作了:
To code the Decision
:
启用从的转换 PatrolState
to ChaseState
,让我们首先创建转换脚本对象:
我们将按如下方式填充检查器窗口:
然后我们将完成如下的Chase State检查器对话框:
接下来,我们将完成巡逻状态对话框:
Finally, we’ll add the BaseStateMachine
组件到敌人对象:在Unity编辑器的项目窗口, 打开SampleScene资源, 从层次面板中选择敌人对象, and, in the Inspector window, select Add Component > Base State Machine:
对于任何问题,请再次检查你的游戏对象是否配置正确. 例如,确认敌人对象包含 PatrolPoints
脚本组件和对象 Point1
, Point2
, etc. 错误的编辑器版本可能会丢失此信息.
现在你已经准备好玩样例游戏,并观察到当玩家进入敌人的视线时敌人会跟着玩家.
在这个有限状态机教程中,我们创建了一个高度模块化的基于fsm的AI(以及相应的 GitHub repo),我们可以在未来的项目中重复使用. 由于这种模块化,我们总是可以通过引入新组件来增加AI的能力.
但是我们的架构也为图形优先的FSM设计铺平了道路, 这将把我们的开发者体验提升到一个新的专业水平. 这样我们就可以更快地为我们的游戏创造fsm,并且具有更好的创意准确性.
有限状态机(FSM)是一种计算模型. 在一个有限状态机中,在任何给定的时间,只有有限个假设状态中的一个可以是活动的.
有限状态机在响应输入或条件时从一种状态转换到另一种状态.
计算机不是有限状态机. 计算机是一个物理对象,而有限状态机是一个计算模型.
有限状态机是通过编码和添加AI编码类来实现的, 创建自定义操作和决策, and attaching behaviors.
Located in Yerevan, Armenia
Member since July 1, 2021
Garegin是一名精通Unity和c#的游戏开发者. 他为游戏化的操场设备创建了一个网络协议, 曾担任一家教育游戏初创公司的首席技术官, 他是一家跨国社交赌场团队的游戏开发者.
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.