有限状态机[第3部分]
这是我们FSM的最终博客教程。我们将回顾第一部分讨论的内容,并实施我们之前所做的FSM系统。
只是回顾前面的部分:
这将是我们FSM的循环。首先,我们初始化FSM,创建状态,创建动作,并将它们全部映射在一起。在我们映射它们之后,我们现在将启动FSM并指示AI将开始的状态。现在,AI将更改为特定状态,FSM将初始化操作,更新它直到操作完成并发送指示操作已完成的事件。最后,FSM将返回并更改状态。
现在是时候实施它们了。
让我们创建一个TextAction,以便我们第一手看到FSM系统。
让我们首先导入Common.FSM,以便我们可以使用我们的FSM系统,并让这个类继承FSMAction类。
1
2
3
4
5
6
7
8
|
using UnityEngine;
using System.Collections;
using Common.FSM;
public class TextAction : FSMAction
{
}
|
现在让我们为这个特定的动作编写变量和构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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是我们将在更新时在控制台中打印出来的字符串。持续时间是此操作的长度,而finishEvent是转换到另一个状态的调用。
让我们覆盖FSMAction的虚函数,以便FSM可以调用它们。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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的动作处理器调用,该动作处理器由FSM的Update函数调用,并由我们的AI类调用。退出当然,将是我们完成一项行动后我们将要做的事情。就个人而言,我宁愿有一个完成功能,将发送事件。
现在我们已经采取了行动,让我们制作AI。
让我们创建一个空的,它将保存我们的AI脚本。
创建一个空的后,让我们创建一个脚本来实现我们的FSM系统和我们所做的动作。
让我们为AITest制作两个FSMStates和两个FSMAction。不要忘记导入Common.FSM,否则我们将无法使用我们制作的FSM系统。
1
2
3
4
5
6
7
8
9
10
11
12
|
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,States和Actions。
1
2
3
4
5
6
7
8
|
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。
1
2
3
4
5
6
|
//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,反之亦然。我们现在初始化我们的行动。
1
2
3
|
//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工作;
1
2
|
//Starts the FSM
fsm.Start ("IdleState");
|
1
2
3
4
|
private void Update ()
{
fsm.Update ();
}
|
同样,在Start()下的所有初始化之后调用fsm.Start()。现在让我们将AITest附加到我们之前创建的空游戏对象,然后在编辑器中按“播放”。
最后,看看自动化发生了!
AI将每2秒打印一次空闲,并且每3秒巡逻一次。
现在让我们把它带到另一个层面。让我们制作一个移动的对象,同时在控制台中打印出字符串。但是我们不会在一个行动中做到这一点。我们将创建另一个将添加到同一状态的操作。
让我们创建一个名为AITestTwo的脚本。
让我们创建另一个名为MoveAction的动作。这将是移动物体的通用动作。
打开MoveAction.cs,让我们编写动作。
MoveAction.cs不同,我们必须引用对象的变换,以便动作可以移动它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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()属性中的变量,即journeyLength和polledTime。稍后我们将它们用于Vector3.Lerp函数。轮询时间将包含在我们稍后的计算中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
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现在正在同时执行操作。您可以混合不同的操作并将其动态添加到所需的状态。
这有什么好处,你可以添加很多动作并将它们包含在一个状态中并同时执行它们。虽然如果你在移动设备上会导致一些性能问题。我建议的是,当你将它用于移动时,为了安全起见,只需同时做至少1或2个动作。我没有对移动设备进行过真正的测试。
就这样,我们已经完成了动态FSM教程。我希望这对你的游戏有所帮助。你可以在任何游戏中以任何方式使用它。无论您是在UI,GameObjects还是其他任何需要的地方使用它。如果您有任何疑问,请记得发表评论,我很乐意尽快回复!
FSM是一个非常重要的主题,分为三个部分,所以如果你想要更好地理解它,这里有一些与FSM相关的文章和实现。我相信这些将进一步解释FSM:
如果您想查看整个项目,这是Unity包的链接。
这是我们在整个过程中创建的脚本的链接。