有限状态机[第3部分]
这是我们针对FSM的最终博客教程。我们将回顾我们在第一部分中讨论的内容,并实现我们之前所做的FSM系统。
只是回顾一下前面的部分:
这将是我们FSM的循环。首先,我们初始化FSM,创建状态,创建动作并将它们全部映射在一起。映射它们之后,我们现在将启动FSM并指示AI将开始的状态。现在,AI将更改为特定状态,FSM将初始化动作,对其进行更新,直到动作完成,并发送一个指示动作完成的事件。最后,FSM将返回并更改状态。
现在是实现它们的时候了。
文字动作
让我们创建一个TextAction,以便我们第一手看到FSM系统。
首先,导入Common.FSM,以便可以使用我们的FSM系统,并让此类继承FSMAction类。
using UnityEngine;
using System.Collections;
using Common.FSM;
public class TextAction : FSMAction
{
}
现在让我们为该特定操作编写变量和构造函数。
private string textToShow;
private float duration;
private float cachedDuration;
private string finishEvent;
public TextAction (FSMState owner) : base (owner)
{
}
public void Init (string textToShow, float duration, string finishEvent)
{
this.textToShow = textToShow;
this.duration = duration;
this.cachedDuration = duration;
this.finishEvent = finishEvent;
}
textToShow是更新时将在控制台中打印出的字符串。Duration是action的持续时间,而finishEvent是过渡到另一个状态的调用。
让我们重写FSMAction的虚函数,以便FSM可以调用它们。
public override void OnEnter ()
{
if (duration <= 0) {
Finish ();
return;
}
}
public override void OnUpdate ()
{
duration -= Time.deltaTime;
if (duration <= 0) {
Finish ();
return;
}
Debug.Log (textToShow);
}
public override void OnExit ()
{
}
public void Finish ()
{
if (!string.IsNullOrEmpty (finishEvent)) {
GetOwner ().SendEvent (finishEvent);
}
duration = cachedDuration;
}
OnEnter将处理我们在开始操作时想要做的所有事情。就像MonoBehaviour中的Start()一样,我们将在此阶段初始化动作。FSM的动作处理器将调用Update,FSM的Update函数将调用该更新,而AI的类将调用该更新。当然,在退出时,将是完成动作后将要执行的操作。就个人而言,我宁愿有一个完成功能来发送事件。
人工智能实施
现在我们已经采取了行动,让我们创建一个人工智能。
让我们创建一个包含AI脚本的空白。
创建一个空文件后,让我们创建一个脚本,该脚本将实现我们的FSM系统以及我们执行的操作。
让我们为AITest创建两个FSMStates和两个FSMAction。不要忘记导入Common.FSM,否则我们将无法使用我们制造的FSM系统。
using UnityEngine;
using System.Collections;
using Common.FSM;
public class AITest : MonoBehaviour
{
private FSM fsm;
private FSMState PatrolState;
private FSMState IdleState;
private TextAction PatrolAction;
private TextAction IdleAction;
}
同样,fsm将成为我们状态机的引擎,我们将拥有两种状态,即PatrolState和IdleState。命名方式由您决定。最后,我们将有两个文本操作,每个状态一个,分别是PatrolAction和IdleAction。
现在,让我们在Start()函数中创建FSM,状态和操作。
private void Start ()
{
fsm = new FSM ("AITest FSM");
IdleState = fsm.AddState ("IdleState");
PatrolState = fsm.AddState ("PatrolState");
PatrolAction = new TextAction (PatrolState);
IdleAction = new TextAction (IdleState);
}
首先,我们将初始化FSM,然后添加新状态。请记住,FSM.AddState()返回FSMStates,这样我们就不必声明新的FSMState并将其添加到FSM中。
现在我们有了状态和动作,现在让我们映射所有内容,并为每个转换添加事件ID。
//This adds the actions to the state and add state to it's transition map
PatrolState.AddAction (PatrolAction);
IdleState.AddAction (IdleAction);
PatrolState.AddTransition ("ToIdle", IdleState);
IdleState.AddTransition ("ToPatrol", PatrolState);
当事件被发送到状态时,我们将PatrolState映射到IdleState,反之亦然。现在让我们初始化动作。
//This initializes the actions
PatrolAction.Init ("AI on patrol", 3.0f, "ToIdle");
IdleAction.Init ("AI on Idle", 2.0f, "ToPatrol");
第一个属性是字符串,将是操作的输出,第二个属性将确定操作的持续时间,最后一个属性是操作完成后发出的事件。
请记住,这里的所有内容都在启动功能内。现在让我们通过调用FSM.Start()和FSM.Update()来使FSM工作。
//Starts the FSM
fsm.Start ("IdleState");
private void Update ()
{
fsm.Update ();
}
同样,在Start()下进行所有初始化之后,将调用fsm.Start()。现在,将AITest附加到我们之前创建的空白游戏对象上,然后在编辑器中按“播放”。
现在终于可以看到自动化发生了!
将每2秒钟打印一次Idle,然后每3秒钟巡逻一次。
运动动作
现在让我们进入另一个层次。让我们制作一个移动的对象,同时在控制台中打印出字符串。但是我们不会一蹴而就。我们将创建另一个将添加到相同状态的动作。
让我们制作另一个名为AITestTwo的脚本。
让我们创建另一个动作,称为MoveAction。这将是移动物体的一般动作。
打开MoveAction.cs,让我们编写动作。
MoveAction.cs是不同的,我们必须引用对象的变换,以便动作可以移动它。
using UnityEngine;
using System.Collections;
using Common.FSM;
public class MoveAction : FSMAction
{
private Transform transform;
private Vector3 positionFrom;
private Vector3 positionTo;
private float duration;
private float cachedDuration;
private string finishEvent;
private float journeyLength;
private float polledTime;
public MoveAction (FSMState owner) : base (owner)
{
}
public void Init (Transform transform, Vector3 from, Vector3 to, float duration, string finishEvent = null)
{
this.transform = transform;
this.positionFrom = from;
this.positionTo = to;
this.duration = duration;
this.cachedDuration = duration;
this.finishEvent = finishEvent;
this.journeyLength = Vector3.Distance (this.positionFrom, this.positionTo);
this.polledTime = 0;
}
}
让我们讨论属性。如前所述,我们需要在此操作中引用对象的变换,因为我们希望此操作移动对象,而不是AI类。同样很容易理解,从Vector3到操作的持续时间和完成事件。
如您所见,Init()属性中没有包含两个变量,即tripageLength和polledTime。我们稍后将在Vector3.Lerp函数中使用它们。稍后将把轮询时间包括在我们的计算中。
public override void OnEnter ()
{
if (duration <= 0) {
Finish ();
return;
}
SetPosition (this.positionFrom);
}
public override void OnUpdate ()
{
polledTime += Time.deltaTime;
duration -= Time.deltaTime;
if (duration <= 0) {
Finish ();
return;
}
SetPosition (Vector3.Lerp (this.positionFrom, this.positionTo, Mathf.Clamp (polledTime / cachedDuration, 0, 1)));
}
private void Finish ()
{
if (!string.IsNullOrEmpty (finishEvent)) {
GetOwner ().SendEvent (finishEvent);
}
SetPosition (this.positionTo);
this.polledTime = 0;
duration = cachedDuration;
this.journeyLength = Vector3.Distance (this.positionFrom, this.positionTo);
}
private void SetPosition (Vector3 position)
{
this.transform.position = position;
}
现在,我们为位置添加了一个辅助函数,该函数是SetPosition(),它使我们更容易进行操作。再次,OnEnter()初始化动作,OnUpdate()将是执行Lerp函数的函数。这也是我们将计算Lerp函数所需比率的地方。操作完成后,我们将对象设置到所需的最终位置,并重置变量。
多动作实施
现在,让我们开始设置第二个AI。
using UnityEngine;
using System.Collections;
using Common.FSM;
public class AITestTwo : MonoBehaviour
{
private FSM fsm;
private FSMState MoveLeftState;
private FSMState MoveRightState;
private TextAction MoveLeftTextAction;
private TextAction MoveRightTextAction;
private MoveAction MoveLeftAction;
private MoveAction MoveRightAction;
private void Start ()
{
fsm = new FSM ("AITest FSM Two");
MoveLeftState = fsm.AddState ("MoveLeftState");
MoveRightState = fsm.AddState ("MoveRightState");
MoveLeftTextAction = new TextAction (MoveLeftState);
MoveRightTextAction = new TextAction (MoveRightState);
MoveLeftAction = new MoveAction (MoveLeftState);
MoveRightAction = new MoveAction (MoveRightState);
//This adds the actions to the state and add state to it's transition map
MoveLeftState.AddAction (MoveLeftTextAction);
MoveLeftState.AddAction (MoveLeftAction);
MoveRightState.AddAction (MoveRightTextAction);
MoveRightState.AddAction (MoveRightAction);
MoveLeftState.AddTransition ("ToRight", MoveRightState);
MoveRightState.AddTransition ("ToLeft", MoveLeftState);
//This initializes the actions
MoveLeftTextAction.Init ("AI moving left", 1.0f, "");
MoveRightTextAction.Init ("AI moving right", 1.0f, "");
MoveLeftAction.Init (this.transform, new Vector3 (1, 0, 0), new Vector3 (-1, 0, 0), 1.0f, "ToRight");
MoveRightAction.Init (this.transform, new Vector3 (-1, 0, 0), new Vector3 (1, 0, 0), 1.0f, "ToLeft");
//Starts the FSM
fsm.Start ("MoveLeftState");
}
private void Update ()
{
fsm.Update ();
}
}
现在,每个状态有两个动作。FSM现在将在一种状态下更新两个动作。我们在这里所做的与在TextAction.cs上所做的相同。我们创建了类的实例,将操作添加到状态,并添加了对这些状态的转换和调用,并初始化了操作。
现在,让我们开始游戏,并希望它能正常运行。
如您所见,AI现在正在同时执行操作。您可以混合使用不同的操作,然后将其动态添加到所需的状态。
这样做的好处是,您可以添加许多动作并将它们包含在一个状态中并同时执行。尽管如果使用移动设备,这会导致一些性能问题。
至此,我们已经完成了Dynamic FSM教程。我希望这将对您的游戏有所帮助。您可以在任何游戏中以任何方式使用此功能。无论是在UI,GameObjects还是任何适合您的需求上使用它。
FSM是一个非常大的主题,分为三个部分,因此,如果您想更好地理解它,那么这里很少有与FSM相关的文章和实现。我相信这些将进一步解释FSM:
来自Unity3D Wiki的FSM
Unity3d-Finite-State-Machine
维基的FSM
Unity Gem
如果您想查看整个项目,这是unity软件包的链接。