这是我们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附加到我们之前创建的空游戏对象,然后在编辑器中按“播放”。
最后,看看自动化发生了!
现在让我们把它带到另一个层面。让我们制作一个移动的对象,同时在控制台中打印出字符串。但是我们不会在一个行动中做到这一点。我们将创建另一个将添加到同一状态的操作。
让我们创建一个名为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包的链接。
这是我们在整个过程中创建的脚本的链接。