本章将向您介绍使用HSM的基本知识。为了简单起见,我们将在这里学习如何编写单层次(也称为平面)状态机,并在下一章中介绍层次状态机。
让我们从我们可以编写的最简单的状态机开始。
// simplest_state_machine.cpp
#include "hsm/statemachine.h"
struct First : hsm::State
{
};
int main()
{
hsm::StateMachine stateMachine;
stateMachine.Initialize<First>();
stateMachine.ProcessStateTransitions();
}
首先,我们包括hsm/statemachine.h,它引入了整个hsm库。
我们宣布一个名为First状态。状态是继承自hsm::State的结构或类。
注意:我们更喜欢使用structs而不是类,因为默认情况下它们是公开派生的,所以不需要指定“public”关键字。
主要来说,我们初始化一个StateMachine对象,告诉它First是它的初始状态。所有StateMachine都必须有一个初始状态才能启动。
然后我们调用stateMachine.ProcessStateTransitions,它将评估必须进行的任何转换并执行它们。在这种情况下,因为我们只有一个状态,它什么都不做,所以这个调用什么也不做。
这是最简单的。现在让我们让这个状态机真正做点什么。
状态和过渡
让我们添加一些状态和转换。
// states_and_transitions.cpp
#include "hsm/statemachine.h"
using namespace hsm;
struct Third : State
{
virtual Transition GetTransition()
{
return NoTransition();
}
};
struct Second : State
{
virtual Transition GetTransition()
{
return SiblingTransition<Third>();
}
};
struct First : State
{
virtual Transition GetTransition()
{
return SiblingTransition<Second>();
}
};
int main()
{
StateMachine stateMachine;
stateMachine.Initialize<First>();
stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
stateMachine.ProcessStateTransitions();
}
让我们看看此代码中的新增内容:
我们引入了hsm命名空间。通常,我们建议在实现状态机的cpp文件中执行此操作,因为它大大减少了“hsm::”前缀噪声。
我们又增加了两个状态:第三和第二。我们还在所有3个状态中实现了虚拟GetTransition函数。此函数用于在调用StateMachine::ProcessStateTransition时,状态返回其希望进行的转换。在这种情况下,所有3个状态都是兄弟状态,这意味着它们都处于相同的层次级别(我们稍后将进入层次部分),第一个状态转换为第二个状态,然后转换为第三个状态。
总之,我们添加了一个对stateMachine.SetDebugInfo的调用,为状态机提供一个名称和详细级别,用于调试。
注意:TraceLevel枚举支持三个值:None、Basic和Diagnostic。我们建议在编写状态机时使用Basic,在调试库内部时使用Diagnostic。
最后,我们像以前一样调用stateMachine.ProcessStateTransitions。由于我们将调试级别设置为1,因此我们得到以下输出:
HSM_1_TestHsm: Init : struct First
HSM_1_TestHsm: Sibling : struct Second
HSM_1_TestHsm: Sibling : struct Third
调试输出显示正在进行的转换。初始过渡到“第一”之后是两个同级过渡,即“第一”到“第二”和“第二到第三”。
我们还来看看这个状态机的plotHsm输出:
这个状态机的图显示了我们的三个状态,虚线箭头表示可以进行的兄弟转换:第一个可以转换到第二个,第二个可以转换为第三个。
注:本章中的示例图过于简单,没有用处;然而,在下一章中,我们将广泛使用plotHsm来更好地理解所提出的层次状态机。
到目前为止很简单,对吧?显然还有很多细节缺失,但我们很快就会找到!
提高可读性
您可能已经在前面的示例中注意到,状态First、Second和Third的定义顺序相反;即:第三,然后是第二,最后是第一。这是典型的C/C++代码,因为在使用前必须始终定义或至少声明一个类型;在我们的例子中,Second在其GetTransition实现中引用了Third,类似地,First引用了Second:
struct Third : State
{
virtual Transition GetTransition()
{
return NoTransition();
}
};
struct Second : State
{
virtual Transition GetTransition()
{
return SiblingTransition<Third>(); //*** Here
}
};
struct First : State
{
virtual Transition GetTransition()
{
return SiblingTransition<Second>(); //*** And here
}
};
如果能随心所欲地命令各状态,那就太好了;在这种情况下,如果First在Second之前,Second在Third之前,则更容易理解状态机。我们可能可以通过一些前瞻性声明来做到这一点,但只声明一次我们的州也很好。事实证明,通过将我们的状态嵌套在一个结构中,我们既可以吃蛋糕,也可以吃蛋糕:
// improving_readability.cpp
#include "hsm/statemachine.h"
using namespace hsm;
struct MyStates
{
struct First : State
{
virtual Transition GetTransition()
{
return SiblingTransition<Second>();
}
};
struct Second : State
{
virtual Transition GetTransition()
{
return SiblingTransition<Third>();
}
};
struct Third : State
{
virtual Transition GetTransition()
{
return NoTransition();
}
};
};
int main()
{
StateMachine stateMachine;
stateMachine.Initialize<MyStates::First>();
stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
stateMachine.ProcessStateTransitions();
}
请注意,我们添加了一个名为MyStates的结构,并在其中以不同的顺序嵌套了我们的三个状态。我们还修改了stateMachine。初始化调用以完全限定初始状态名称(MyStates:First)。
这之所以有效,是因为当这些名称嵌套在C++中时,依赖于模板参数的名称查找(ADL)是如何工作的。在不涉及太多细节的情况下,当模板函数参数是嵌套类型时,即使它是在模板函数调用之后定义的,它也会被正确解析。在我们的例子中,SiblingTransition是一个模板函数,我们可以将状态的名称传递给它,即使它是稍后定义的,因为它嵌套在MyStates结构中。
注意:稍后,我们将展示在结构中嵌套状态的另一个优势:授予对状态机所有者的私有成员的访问权限。
状态OnEnter和OnExit
基本hsm::State在进入和退出状态时提供两个虚拟挂钩:分别为OnEnter和OnExit。这些可以用于初始化或去初始化数据、系统等。
以下是我们之前的示例代码,其中将OnEnter/OnExit对添加到三个状态:
// state_onenter_onexit.cpp
#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;
struct MyStates
{
struct First : State
{
virtual void OnEnter()
{
printf("First::OnEnter\n");
}
virtual void OnExit()
{
printf("First::OnExit\n");
}
virtual Transition GetTransition()
{
return SiblingTransition<Second>();
}
};
struct Second : State
{
virtual void OnEnter()
{
printf("Second::OnEnter\n");
}
virtual void OnExit()
{
printf("Second::OnExit\n");
}
virtual Transition GetTransition()
{
return SiblingTransition<Third>();
}
};
struct Third : State
{
virtual void OnEnter()
{
printf("Third::OnEnter\n");
}
virtual void OnExit()
{
printf("Third::OnExit\n");
}
virtual Transition GetTransition()
{
return NoTransition();
}
};
};
int main()
{
StateMachine stateMachine;
stateMachine.Initialize<MyStates::First>();
stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
stateMachine.ProcessStateTransitions();
}
运行程序的输出:
HSM_1_TestHsm: Init : struct MyStates::First
First::OnEnter
First::OnExit
HSM_1_TestHsm: Sibling : struct MyStates::Second
Second::OnEnter
Second::OnExit
HSM_1_TestHsm: Sibling : struct MyStates::Third
Third::OnEnter
我们可以看到,当源状态向目标状态进行同级转换时,在进入目标状态之前,首先退出源状态。
OnEnter/OnExit与构造函数/析构函数
既然状态只是类,为什么不使用构造函数/析构函数而不是OnEnter/OnExit函数呢?
主要原因是,当对一个状态调用OnEnter时,它的所有数据都已初始化,包括——最重要的是——拥有的状态机实例。使用默认构造函数时,此数据尚未设置,因此无法使用。状态可用的大多数函数都取决于状态机指针是否有效,因此这些函数只能在OnEnter中调用,而不能在构造函数中调用。
至于OnExit,使用它和析构函数没有太大区别;但是,为了保持一致性,我们建议使用它。
注意:使用OnEnter的另一个原因是它允许可选地使用StateArgs,这是我们稍后将介绍的功能。
过程状态转换
在迄今为止的示例中,我们已经忽略了stateMachine.ProcessStateTransitions调用的细节。在本节中,我们将仔细研究这个函数,从一些伪代码开始了解它的工作原理:
done = false
while (!done)
transition = currState.GetTransition()
if (transition != NoTransition)
currState.OnExit()
currState = transition.GetTargetState()
currState.OnEnter()
else
done = true
注意:此伪代码将在下一章中进行扩展,以处理分层状态转换。目前,我们在这里介绍的内容对于平面状态机(即只执行状态之间的同级转换)是准确的。
重要的是要注意,函数将保持状态之间的转换,直到不再进行转换为止。以下示例显示了此操作的工作方式:
// process_state_transitions.cpp
#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;
bool gStartOver = false;
struct MyStates
{
struct First : State
{
virtual void OnEnter()
{
gStartOver = false;
}
virtual Transition GetTransition()
{
return SiblingTransition<Second>();
}
};
struct Second : State
{
virtual Transition GetTransition()
{
return SiblingTransition<Third>();
}
};
struct Third : State
{
virtual Transition GetTransition()
{
if (gStartOver)
return SiblingTransition<First>();
return NoTransition();
}
};
};
int main()
{
StateMachine stateMachine;
stateMachine.Initialize<MyStates::First>();
stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
printf(">>> First ProcessStateTransitions\n");
stateMachine.ProcessStateTransitions();
printf(">>> Second ProcessStateTransitions\n");
stateMachine.ProcessStateTransitions();
gStartOver = true;
printf(">>> Third ProcessStateTransitions\n");
stateMachine.ProcessStateTransitions();
printf(">>> Fourth ProcessStateTransitions\n");
stateMachine.ProcessStateTransitions();
}
和以前一样,第一个兄弟姐妹对第二个,第二个兄弟姐妹给第三个;但只有当全局变量gStartOver为true时,状态Third才会转换回First;否则它将保持其状态。以下是该程序的输出:
>>> First ProcessStateTransitions
HSM_1_TestHsm: Init : struct MyStates::First
HSM_1_TestHsm: Sibling : struct MyStates::Second
HSM_1_TestHsm: Sibling : struct MyStates::Third
>>> Second ProcessStateTransitions
>>> Third ProcessStateTransitions
HSM_1_TestHsm: Sibling : struct MyStates::First
HSM_1_TestHsm: Sibling : struct MyStates::Second
HSM_1_TestHsm: Sibling : struct MyStates::Third
>>> Fourth ProcessStateTransitions
我们可以看到,对ProcessStateTransitions的第二次调用没有任何作用。这是因为我们处于状态Third,gStartOver为false,所以它返回NoTransition。之后,我们将gStartOver设置为true,对ProcessStateTransitions的第三个调用将third同级设置为First,将First设置为Second,并将Second设置为third,再次停止。为什么它又停在第三位?原因是First::OnEnter总是将gStartOver重置为false,所以当它再次到达Third时,它将不会转换回First。事实上,如果我们删除First::OnEnter,ProcessStateTransitions将以兄弟转换的无限循环结束:Third->First->Second->Third->First等等。
注意:当检测到无限转换时,HSM会触发断言。
因此,现在我们看到在对ProcessStateTransitions的调用之间更改一些数据会导致不同的转换。在本例中,数据是在状态机外部修改的全局变量;然而,数据更改通常是由各状态自己进行的。
ProcessStateTransitions的调用频率应该是多少?这取决于您的应用程序,但以下是几个示例:
在游戏或实时模拟中,您可能会在每一帧调用ProcessStateTransitions,因为您知道世界的状态、玩家的输入或其他数据可能自上一帧以来发生了变化。
在基于事件的系统(如UI)中,您希望在事件修改某些数据后调用ProcessStateTransition。
关于State::GetTransition的最后一点说明:此函数的作用只是返回要进行的转换,而不是执行任何特定于状态的逻辑。相反,您可以使用State::Update来实现此目的,这将在下一节中介绍。
更新状态
当您需要一个状态在该状态下执行某些操作时,可以实现虚拟更新功能。当调用StateMachine::UpdateStates时,将在当前状态下调用此函数:
// update_states.cpp
#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;
bool gPlaySequence = false;
struct MyStates
{
struct First : State
{
virtual Transition GetTransition()
{
if (gPlaySequence)
return SiblingTransition<Second>();
return NoTransition();
}
virtual void Update()
{
printf("First::Update\n");
}
};
struct Second : State
{
virtual Transition GetTransition()
{
if (gPlaySequence)
return SiblingTransition<Third>();
return NoTransition();
}
virtual void Update()
{
printf("Second::Update\n");
}
};
struct Third : State
{
virtual Transition GetTransition()
{
return NoTransition();
}
virtual void Update()
{
printf("Third::Update\n");
}
};
};
int main()
{
StateMachine stateMachine;
stateMachine.Initialize<MyStates::First>();
stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
stateMachine.ProcessStateTransitions();
stateMachine.UpdateStates();
stateMachine.ProcessStateTransitions();
stateMachine.UpdateStates();
gPlaySequence = true;
stateMachine.ProcessStateTransitions();
stateMachine.UpdateStates();
stateMachine.ProcessStateTransitions();
stateMachine.UpdateStates();
}
我们已经为状态First、Second和Third添加了Update函数。我们使用全局变量gPlaySequence将First设为Second,然后再设为Third。在主函数中,我们现在将对ProcessStateTransition的调用与UpdateStates配对。通常,我们希望每帧连续调用这两个函数一次(或者每当需要更新状态机时)。在这个人为的例子中,我们在修改全局变量之前调用两次这对,以显示当您在多个帧的状态下保持时会发生什么。
以下是运行程序的输出:
HSM_1_TestHsm: Init : struct MyStates::First
First::Update
First::Update
HSM_1_TestHsm: Sibling : struct MyStates::Second
HSM_1_TestHsm: Sibling : struct MyStates::Third
Third::Update
Third::Update
当我们处于状态First时,每次调用stateMachine.UpdateStates时都会调用First::Update。修改全局变量后,对stateMachine.ProcessStateTransitions的下一次调用会导致First到同级到Second,Second到Third。由于我们处于状态Third,Third::Update每次调用stateMachine.UpdateStates都会被调用两次。这里需要注意的是,Second::Update从未被调用,因为我们在ProcessStateTransitions结束时从未处于该状态。如果我们真的想让Second在通过它时做点什么,我们可以使用OnEnter。
关于UpdateStates,还有一些需要注意的事项:
事实上,这个功能实际上并不是必需的。然而,在游戏和实时模拟中,事实证明,我们经常需要对当前状态进行某种类型的更新功能,因此将其添加到HSM中是为了方便。
将某些参数传递给Update函数通常很有用,例如帧增量时间。HSM提供了可以修改的宏,以定义StateMachine::UpdateStates和State::Update:的参数。
所有者
基本用途
到目前为止,在我们的示例中,我们已经直接在main中创建了一个StateMachine实例,并使用全局变量与状态进行了通信。在实践中,StateMachine将是一个类的数据成员——它的所有者——我们希望该StateMachine的状态访问该所有者上的成员(它的函数和数据成员)。
让我们来看看一个在功能上与上一节中的示例等效的示例,只是这次我们添加了一个所有者:
// ownership_basic_usage.cpp
#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;
class MyOwner
{
public:
MyOwner();
void UpdateStateMachine();
void PlaySequence();
bool GetPlaySequence() const;
private:
StateMachine mStateMachine;
bool mPlaySequence;
};
struct MyStates
{
struct First : State
{
virtual Transition GetTransition()
{
MyOwner* owner = reinterpret_cast<MyOwner*>(GetStateMachine().GetOwner());
if (owner->GetPlaySequence())
return SiblingTransition<Second>();
return NoTransition();
}
virtual void Update()
{
printf("First::Update\n");
}
};
struct Second : State
{
virtual Transition GetTransition()
{
MyOwner* owner = reinterpret_cast<MyOwner*>(GetStateMachine().GetOwner());
if (owner->GetPlaySequence())
return SiblingTransition<Third>();
return NoTransition();
}
virtual void Update()
{
printf("Second::Update\n");
}
};
struct Third : State
{
virtual Transition GetTransition()
{
return NoTransition();
}
virtual void Update()
{
printf("Third::Update\n");
}
};
};
MyOwner::MyOwner()
{
mPlaySequence = false;
mStateMachine.Initialize<MyStates::First>(this);
mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}
void MyOwner::UpdateStateMachine()
{
mStateMachine.ProcessStateTransitions();
mStateMachine.UpdateStates();
}
void MyOwner::PlaySequence()
{
mPlaySequence = true;
}
bool MyOwner::GetPlaySequence() const
{
return mPlaySequence;
}
int main()
{
MyOwner myOwner;
myOwner.UpdateStateMachine();
myOwner.UpdateStateMachine();
myOwner.PlaySequence();
myOwner.UpdateStateMachine();
myOwner.UpdateStateMachine();
}
The output is exactly the same as before:
HSM_1_TestHsm: Init : struct MyStates::First
First::Update
First::Update
HSM_1_TestHsm: Sibling : struct MyStates::Second
HSM_1_TestHsm: Sibling : struct MyStates::Third
Third::Update
Third::Update
好吧,让我们分解这个例子,以便更好地理解这些变化。首先,我们引入了一个新的类MyOwner:
class MyOwner
{
public:
MyOwner();
void UpdateStateMachine();
void PlaySequence();
bool GetPlaySequence() const;
private:
StateMachine mStateMachine;
bool mPlaySequence;
};
此类包含StateMachine实例作为名为mStateMachine的成员。我们还将gPlaySequence全局移动到此类,作为数据成员mPlaySequence,它由成员函数PlaySequence和GetPlaySequence设置和读取:
void MyOwner::PlaySequence()
{
mPlaySequence = true;
}
bool MyOwner::GetPlaySequence() const
{
return mPlaySequence;
}
构造函数是初始化mPlaySequence和mStateMachine的地方。这里的重要区别在于,我们现在将一个参数传递给mStateMachine。Initialize:“this”:
MyOwner::MyOwner()
{
mPlaySequence = false;
mStateMachine.Initialize<MyStates::First>(this); //*** Note that we pass 'this' as our owner
mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}
StateMachine::Initialize函数接受一个指向所有者实例的可选指针作为它的第一个参数。指针类型为void*,因此任何类型都可以在此处传递。在我们了解这个所有者指针是如何使用的之前,让我们看看UpdateStateMachine,每当状态机需要更新时(例如,游戏中每帧一次),我们都会调用它:
void MyOwner::UpdateStateMachine()
{
mStateMachine.ProcessStateTransitions();
mStateMachine.UpdateStates();
}
在main中主要,我们创建MyOwner实例,并模拟四个帧更新,确保在其中两个帧更新之后设置PlaySequence:
int main()
{
MyOwner myOwner;
myOwner.UpdateStateMachine();
myOwner.UpdateStateMachine();
myOwner.PlaySequence();
myOwner.UpdateStateMachine();
myOwner.UpdateStateMachine();
}
现在让我们来看看我们的州。以前,状态First和Second会在其GetTransition函数中读取全局变量gPlaySequence的值,以确定是否与下一个状态同级。现在,这些状态通过GetStateMachine()访问其所有者。GetOwner():
struct First : State
{
virtual Transition GetTransition()
{
MyOwner* owner = reinterpret_cast<MyOwner*>(GetStateMachine().GetOwner());
if (owner->GetPlaySequence())
return SiblingTransition<Second>();
return NoTransition();
}
<snip>
}
自GetStateMachine()以来。GetOwner()返回我们之前通过StateMachine::Initialize设置的void指针,我们需要将其强制转换为MyOwner,以便调用owner->GetPlaySequence()。在下一节中,我们将看到如何摆脱这种铸造。
理解分层状态机为什么有用的最好方法是通过一个例子。我们的例子将是我们为视频游戏中的角色控制器构建的典型状态机。我们首先从一个简单的平面状态机开始,就像我们在第2章中看到的那样:
我们有三个状态,它们之间有一定的转换。站立和移动都可以根据速度在彼此之间转换,如果角色的生命值降至0,在任何一种状态下,我们都会确保进入死亡状态。现在,让我们再添加几个状态和转换:
我们增加了3个新状态:跳跃、卧倒和投篮。虽然它看起来像一大堆箭头,但如果你仔细观察,你会发现过渡是有意义的:你可以从站立和移动中跳跃、蹲下或射击,从所有状态来看,你必须能够进入死亡状态。
现在,这看起来像是一张图片上的大混乱,这不仅仅是因为它在图片上——这种混乱也可以转化为代码。如果你曾经不得不用不止几个状态和许多可能的转换的代码编写一个平面状态机,那么你很快就知道了管理它有多困难。事实上,bug在第11个小时悄悄出现是很常见的,因为你忘记了从一个你从未想过会需要的状态转换到一个状态。
事实是,平面状态机的复杂性通常与它所包含的状态数成指数比例。添加新状态时,需要添加的转换数量会随着状态机本身的增长而增加。例如,如果我们要将伤害状态添加到上述状态机,我们需要从除死亡之外的所有状态添加到伤害的转换。
平面状态机的问题是,所有状态都被视为独立的单元:一次只能处于一个状态。然而,通常情况下,同时处于多个状态是有意义的。例如,假设我们引入状态Alive来表示Dead的对立面。只要健康度<=0,活着的人就会转变为死去的人。现在,从逻辑上讲,只要你处于除了上面例子中的“死亡”之外的任何状态,你就应该处于“活着”。事实上,当你站着、动着、蹲着和射击时,你仍然活着,对吧?
以下是这样一个状态机的外观示例:
正如你所看到的,大多数状态都被分组到新的“活着”状态中,这是现在唯一一个转换到“死”状态的状态。这正是分层状态机:它提供了一种在其他状态中嵌套状态的方式。
以下是此HSM的另一个版本,其中包含更多的状态嵌套:
在这个版本中,我们将“站立”、“移动”和“蜷缩”组合为一种运动状态,这本身就是“活着”的内部状态。不仅转换次数大大减少,而且更容易理解状态机。例如,我们可以看到,无论你是站着、蹲着还是移动,你都可以开始射击,但你不能在跳跃时射击。因此,通过使我们的平面状态机成为一个分层的状态机,我们可以更容易地推断出我们的角色控制器的规则。
希望现在你能看到分层状态机有多有用。本章的其余部分将深入研究如何使用HSM来实现这些类型的状态机。
上一节中的图像显示嵌套为同心圆的状态。尽管这对理解分层状态机的概念很有用,但对于具有许多嵌套级别的大型状态机来说,用这种方式绘制它们是不现实的。本书的其余部分使用plotHsm的输出,它采用了不同的方法来表示状态层次结构。例如,此状态机:
…使用plotHsm时如下所示:
以下是理解此输出的一些提示:
括号中的数字是状态的深度级别
嵌套越深的状态的状态颜色越亮
实线表示内部过渡(从外部状态到内部状态)
虚线表示同级转换
最后,plotHsm将根据状态名称将状态分组为集群:如果一组状态共享相同的前缀,名称中后跟下划线,则它们将分组在一起。例如:
因为我们在Crouch、Move和Stand的状态名称前面加了“Lomotion_”,plotHsm将状态聚类到一个标记为“Lomotion”的框中。这在理解大型状态机时尤其有用,在大型状态机中,通常会有许多处于相同深度级别的状态集群。当然,以这种方式为所有州名加前缀可能会很麻烦,因此我们建议您对最深或最内部的州进行分组。
Inner and Outer States
当谈论(或写作)分层状态机时,在描述状态及其嵌套状态之间的关系时,通常会使用“父”和“子”这两个术语。在HSM中,我们使用术语“外部”和“内部”。我们这样做是为了避免与状态实际上是C++多态类这一事实混淆,因此在类层次结构级别上已经存在父/子关系(例如,每个状态都是hsm::state的子类)。
我们还用其他一些术语来描述状态之间的关系,以及外部和内部的关系:兄弟、直接和根。定义这些的最佳方法是通过一个示例:
我们可以如下描述上面状态机中的状态关系:
射击、运动、跳跃、蹲下、移动和站立是活着的内在状态
射击、运动和跳跃是Alive的内在状态
射击、运动和跳跃也是Alive的直接内在状态
卧姿、移动和站立是运动的直接内在状态
活着是投篮、运动、跳跃、蹲下、移动和站立的外在状态
活力是射击、运动和跳跃的直接外部状态
死亡不是一种外在状态
Alive和Dead是状态机的根状态
活着和死了是兄弟国家
射击、运动、跳跃是兄弟状态
Crouch、Move和Stand是同级状态
如果你处于“移动”状态,你也处于“运动”状态和“活动”状态
如果你处于射击状态,你也处于活动状态
这是贯穿本书以及HSM代码中使用的术语。
状态堆栈
在我们研究如何编写分层状态机之前,我们必须首先讨论HSM如何管理状态的一个关键特性:状态堆栈。
每个StateMachine实例管理一个States实例堆栈。推送到堆栈上的第一个状态是最外层的状态,下一个推送到堆栈的内部,最后一个推到堆栈的状态是最内层的状态。同级转换将首先从堆栈中弹出当前状态,然后将目标状态推回到堆栈上,从而使其保持相同的深度。内部和内部入口转换,我们将在接下来的几节中介绍,用于将内部状态推送到堆栈上。每次将一个状态推送到堆栈上时,都会调用它的OnEnter。
当源状态向目标状态进行同级转换时,在推送目标状态之前,源状态及其内部都会从堆栈中弹出。这种情况从最内部一直发生到源状态,每个状态都调用OnExit,使每个状态都有机会自行清理。
让我们以上一节的例子为例。运行ProcessStateTransitions后,状态堆栈可能如下所示:
如果玩家按下移动输入,则对ProcessStateTransitions的下一次调用将产生此状态堆栈:
在这种情况下,Stand将同级转换为Move,结果是Stand::OnExit(弹出),然后是Move::OnEnter(推送)。现在,如果角色被杀死,那么对ProcessStateTransitions的下一次调用将产生以下状态堆栈:这一次,Alive将同级转换为Dead,导致Move::OnExit(pop)、Lomotion::OnExport(pop。
在接下来的几节中,我们将了解用于推动内部状态的两种类型的转换。
内部入口转换
到目前为止,我们看到的唯一类型的转换是兄弟转换,它用于退出一个状态并进入另一个状态。在本节中,我们将介绍内部进入转换,它用于进入一个新的内部状态。让我们从一些代码开始`// inner_entry_transition.cpp
#include “hsm/statemachine.h”
using namespace hsm;
class MyOwner
{
public:
MyOwner();
void UpdateStateMachine();
void Die() { mDead = true; }
private:
bool IsDead() const { return mDead; }
bool PressedMove() const { return false; } // Stub
bool mDead;
friend struct MyStates;
StateMachine mStateMachine;
};
struct MyStates
{
struct BaseState : StateWithOwner
{
};
struct Alive : BaseState
{
virtual Transition GetTransition()
{
if (Owner().IsDead())
return SiblingTransition<Dead>();
return InnerEntryTransition<Locomotion>();
}
};
struct Dead : BaseState
{
virtual Transition GetTransition()
{
return NoTransition();
}
};
struct Locomotion : BaseState
{
virtual Transition GetTransition()
{
return InnerEntryTransition<Stand>();
}
};
struct Stand : BaseState
{
virtual Transition GetTransition()
{
if (Owner().PressedMove())
return SiblingTransition<Move>();
return NoTransition();
}
};
struct Move : BaseState
{
virtual Transition GetTransition()
{
if (!Owner().PressedMove())
return SiblingTransition<Stand>();
return NoTransition();
}
};
};
-
MyOwner::MyOwner()
-
mDead(false)
{
mStateMachine.Initialize MyStates::Alive(this);
mStateMachine.SetDebugInfo(“TestHsm”, TraceLevel::Basic);
}
void MyOwner::UpdateStateMachine()
{
mStateMachine.ProcessStateTransitions();
mStateMachine.UpdateStates();
}
int main()
{
MyOwner myOwner;
myOwner.UpdateStateMachine();
myOwner.Die();
myOwner.UpdateStateMachine();
}`