机器人控制——C++ HSM状态机基础知识

本章将向您介绍使用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()elsedone = 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 ownermStateMachine.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; } // Stubbool mDead;friend struct MyStates;StateMachine mStateMachine;
};struct MyStates
{struct BaseState : StateWithOwner<MyOwner>{};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();
}

在我们讨论代码之前,让我们看看这个状态机的plotHsm输出:
在这里插入图片描述
看这张图,我们可以看到Alive和Dead是兄弟姐妹,Move和Stand也是兄弟姐妹。然而,运动是“活着”的内在状态,“动”和“立”都是运动的内在状态。
现在请注意,虽然Stand和Move都是机车的内部状态,但从机车到Stand只有一个箭头,而从机车到Move没有一个箭头。这是因为机车内部进入过渡到Stand。我们可以在查看状态机车代码时看到这一点:

	struct Locomotion : BaseState{virtual Transition GetTransition(){return InnerEntryTransition<Stand>();}};

当一个状态返回InnerEntryTransition<TargetState>时,如果尚未输入其他内部状态,则状态机将进入TargetState。
换句话说,如果状态堆栈上深度D处的状态返回InnerEntryTransition<TargetState>,则只要深度D+1处还没有状态,就会创建TargetState并将其推送到堆栈上。下一次同一状态返回InnerEntryTransition<TargetState>时,由于内部状态已被推送,此转换将被忽略。

回到我们的例子,Lomotion将始终从其GetTransition函数返回InnerEntryTransition<Stand>();但内部转换只会在第一次返回时发生,此时Stand将被创建为内部状态。现在Stand可以自由进行兄弟转换:

	struct Stand : BaseState{virtual Transition GetTransition(){if (Owner().PressedMove())return SiblingTransition<Move>();return NoTransition();}};

我们可以看到,如果用户按下移动输入,Stand将同级移动。当这种情况发生时,Move将是机车的当前内部状态。请注意,即使Lomotion::GetTransition将继续返回InnerEntryTransition<Stand>(),这也不会有任何效果;Move将保持其当前内部状态。

inner过渡

在上一节中,我们介绍了InnerEntryTransition,它用于仅在堆栈上还没有内部状态的情况下推送内部状态。另一方面,更广义的InnerTransition用于将内部状态强制到状态堆栈上,而不管它上有什么。
更具体地说:当从GetTransition返回InnerTransition<TargetState>时,状态机会确保TargetState变为(或保持)内部状态。如果还没有内部状态,则会推送TargetState。如果内部状态已经是TargetState,则不会发生任何事情。如果内部状态不是TargetState,则当前内部状态及其所有内部将从堆栈中弹出(从最里面向外),然后推送TargetState。
让我们看一个例子:

// inner_transition.cpp#include "hsm/statemachine.h"using namespace hsm;class MyOwner
{
public:MyOwner();void UpdateStateMachine();void Die() { mDead = true; }void SetMove(bool enable) { mMove = enable; }private:bool IsDead() const { return mDead; }bool PressedMove() const { return mMove; }bool mDead;bool mMove;friend struct MyStates;StateMachine mStateMachine;
};struct MyStates
{struct BaseState : StateWithOwner<MyOwner>{};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(){if (Owner().PressedMove())return InnerTransition<Move>();elsereturn InnerTransition<Stand>();}};struct Stand : BaseState{};struct Move : BaseState{};
};MyOwner::MyOwner(): mDead(false), mMove(false)
{mStateMachine.Initialize<MyStates::Alive>(this);mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}void MyOwner::UpdateStateMachine()
{mStateMachine.ProcessStateTransitions();mStateMachine.UpdateStates();
}int main()
{MyOwner myOwner;myOwner.UpdateStateMachine();printf("Set Move = true\n");myOwner.SetMove(true);myOwner.UpdateStateMachine();printf("Set Move = false\n");myOwner.SetMove(false);myOwner.UpdateStateMachine();
}

此示例与InnerEntryTransition部分中的示例相同,只是我们修改了Stand和Move状态转换为的方式。在上一节中,Lomotion将InnerEntreTransition转换为Stand,然后Stand和Moved将根据Owner()的结果彼此同级。PressedMove():

	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();}};

在当前示例中,机车使用InnerTransition来选择将在堆栈上推送的内部状态:

	struct Locomotion : BaseState{virtual Transition GetTransition(){if (Owner().PressedMove())return InnerTransition<Move>();elsereturn InnerTransition<Stand>();}};struct Stand : BaseState{};struct Move : BaseState{};

主要的区别在于,机车通过使用InnerTransition来决定站立还是移动应该是当前的内部状态。以前,机车只是通过将InnerEntryTransition返回到这些州中的哪个州来决定开始——在我们的情况下是Stand;之后,Stand and Move使用SiblingTransitions来决定哪个状态应该是运动的当前内部。
大体上,我们切换“move”变量以查看这些内部转换的作用:

大体上,我们切换“move”变量以查看这些内部转换的作用:

int main()
{MyOwner myOwner;myOwner.UpdateStateMachine();printf("Set Move = true\n");myOwner.SetMove(true);myOwner.UpdateStateMachine();printf("Set Move = false\n");myOwner.SetMove(false);myOwner.UpdateStateMachine();
}
HSM_1_TestHsm: Init    : struct MyStates::Alive
HSM_1_TestHsm:  Entry   : struct MyStates::Locomotion
HSM_1_TestHsm:   Inner   : struct MyStates::Stand
Set Move = true
HSM_1_TestHsm:   Inner   : struct MyStates::Move
Set Move = false
HSM_1_TestHsm:   Inner   : struct MyStates::Stand

我们可以从这个输出中看到,机车首先进行InnerTransition到Stand,然后在我们将移动字段设置为true后,机车“inners”到move,最后在重置移动字段后,机车inners回到Stand。
此示例的plotHsm输出与上一个稍有不同:
在这里插入图片描述
现在有两个箭头指示从运动到移动和站立的InnerTransitions。请注意,从活动到运动的InnerEntryTransition箭头比从运动到移动和站立的InnerTransition箭头更厚。

状态堆栈查询

在实现状态机时,有许多实例需要查询当前状态堆栈。在本节中,我们将了解HSM为此目的提供的功能。
最基本的查询是“我们是否处于给定的状态?”。为此,请使用StateMachine::IsInState<StateType>,如果StateType在状态堆栈中的任何位置,则返回true。例如,给定以下状态堆栈:
在这里插入图片描述
然后是mStateMachine。IsInState<Alive>();,mStateMachine。IsInState<机车>()和mStateMachine。IsInState<Stand>()将全部返回true,但mStateMachine。IsInState<Move>()将返回false。
另一个有用的查询函数是StateMachine::GetState<StateType>,如果在堆栈上找到StateType,则返回指向StateType的指针,否则返回NULL。这使您既可以检查自己是否处于给定状态,也可以访问该状态下的成员,这意味着您可以将特定于状态的函数和数据的范围保持在状态本身。
这涵盖了StateMachine类上可用的查询函数,但State类中还有更多可用函数可以在状态内使用(通常来自GetTransition):
State::GetState<StateType>只是转发到StateMachine类上的相同函数。当一个状态感兴趣知道某个特定状态是否在堆栈上的任何位置时,此函数都很有用。然而,下面描述的函数缩小了搜索方向,并且通常更有用和更优化。
State::GetOuterState<StateType>类似于GetState,只是它从当前状态的直接外部到最外部搜索状态堆栈。当内部状态需要从公共外部状态访问成员时,通常会使用此函数。
State::GetInnerState<StateType>类似于GetState,只是它从当前状态的直接内部到最内部搜索状态堆栈。当一个状态需要根据堆栈上存在的内部状态进行同级转换时,通常会使用此函数。
State::GetImmediateInnerState<StateType>类似于GetInnerState,只是它只检查堆栈上当前状态深度+1处是否存在StateType。当您知道目标状态是直接内部时,这很有用,并且主要是对GetInnerState的优化,因为它不搜索所有内部。
最后,State提供了与上述函数类似的功能,用于检查状态的存在:IsInState、IsInOuterState、IsInInnerState和IsInImmediaInnerState。
我们将在未来的示例中看到这些状态堆栈查询函数的作用。

重新审视流程状态转换

在第2章中,我们介绍了StateMachine::ProcessStateTransitions函数的基本知识,但仅限于它在非层次(平面)状态机中的行为。在本节中,我们将介绍考虑状态堆栈以及InnerEntry和Inner转换的完整算法。
请注意,这可能是本书中最重要的部分,因为它详细介绍了HSM的执行模型。要构建有效的状态机,了解状态是如何创建/销毁的,以及如何处理转换是很重要的。
算法
在第2章中,我们提供了以下伪代码来解释StateMachine::ProcessStateTransitions的工作原理:

done = false
while (!done)transition = currState.GetTransition()if (transition != NoTransition)currState.OnExit()currState = transition.GetTargetState()currState.OnEnter()elsedone = true

这个伪代码足以理解HSM如何处理非层次(平面)状态机。我们现在扩展此代码,看看它是如何处理分层状态机的:

function ProcessStateTransitions
{if stateStack.IsEmpty()CreateAndPushInitialState()bool stackModified = truewhile (stackModified){stackModified = ProcessStateTransitionsOnce()} 
}function ProcessStateTransitionsOnce
{for (depth = 0; depth < stateStack.size(); ++depth){State* currState = GetStateAtDepth(depth)Transition transition = currState->GetTransition()if transition.Type() == NoTransition{continue // Move to next inner}else if transition.Type() == Inner{if transition.TargetState() == GetStateAtDepth(depth + 1){continue // Inner is already target state, move to next inner}else{// Pop all states under us and push targetPopStatesToDepth(depth + 1) // Invokes OnExit on each state, then popsPushState(CreateState(transition.TargetState())) // Pushes to stack, then calls OnEnterreturn true // State stack was modified}}else if transition.Type() == InnerEntry{// If current state has no inner (is currently the innermost), then push the entry stateif GetStateAtDepth(depth + 1) == NULL{State* targetState = CreateState(transition.TargetState())PushState(targetState) // Pushes to stack, then calls OnEnterreturn true // State stack was modified}}else if transition.Type() == Sibling{// Pop all states under and including current, then push target state		PopStatesToDepth(depth)State* targetState = CreateState(transition)PushState(targetState) // Pushes to stack, then calls OnEnterreturn true // State stack was modified}}return false // State stack was not modified
}

这个版本的ProcessStateTransitions肯定比我们在第2章中介绍的版本更长。然而,它应该相当简单,而且它与StateMachine实现中的代码相同,只是为了帮助关注重要部分而进行了简化。
你可能会注意到的第一件事是,这个过程被分解为两个函数。我们将首先关注ProcessStateTransitionsOnce。该函数的作用是处理堆栈上从最外层(深度为0)到最内层的每个状态,对每个状态调用GetTransition,一旦状态返回将修改状态堆栈的转换,它就会执行转换并返回true以表示已进行了修改。如果没有状态返回将修改当前状态堆栈的转换,则函数返回false,表示没有进行任何修改。
对于每个转换类型,我们可以看到状态堆栈是如何操作的。这里不应该有任何意外;堆栈的操作方式与本章前面对每个转换的定义方式相对应。请注意,Sibling转换总是修改状态堆栈,而InnerEntry转换仅在还没有内部状态的情况下修改堆栈,而内部转换仅在当前内部状态还不是目标状态时修改堆栈。
现在请记住,无论状态堆栈是否被修改,ProcessStateTransition都会返回,ProcessStateTransfer只是在循环中调用ProcessStateTransitionsOn,直到后者返回false。实际上,ProcessStateTransition的作用是保持处理堆栈上从最外层到最内层的状态转换(通过ProcessStateTransitionsOn),直到不再有修改堆栈的转换为止。当这种情况发生时,我们说状态堆栈已经稳定。

根本原因
让我们来谈谈ProcessStateTransition算法中一些决策背后的基本原理。
首先,为什么ProcessStateTransition需要在状态堆栈上不断迭代,直到堆栈稳定为止?为什么不简单地通过一次调用ProcessStateTransitionOnce来运行一次堆栈呢?主要原因是幂等性:在所有条件相同的情况下,如果您两次调用StateMachine::ProcessStateTransitions,第二次调用实际上应该是“no-op”(即不应该修改状态堆栈)。
为了理解为什么这很重要,比如说在游戏中,敌人对玩家造成伤害,使其生命值降至0。根据状态机的设计方式,它可能需要多次处理从外部到内部的转换,以使自己进入死亡状态。如果我们没有确保在状态堆栈稳定之前继续处理转换,那么对于一个或多个帧,玩家的健康值将为0,但不会处于死亡状态。这种类型的不一致通常会导致难以理解的错误。实际上,我们想要的是外部数据和状态堆栈之间的1:1映射:如果health为0,那么在调用ProcessStateTransitions之后,我们应该处于Dead状态。
注意:ProcessStateTransitions以这种方式工作的事实也很有用,因为它允许转换到所谓的“瞬态”状态:一种一旦稳定下来就永远不会存在于堆栈中的状态。一个典型的例子是“完成”状态,它在第4章中介绍
关于ProcessStateTransition算法经常被问到的另一个问题是:为什么处理从外部到内部的转换,而不是从内部到外部的转换?原因与分层状态机的典型组织方式有关:外部状态表示更全局的状态,而内部状态表示更局部的状态。外部国家比内部国家做出“更大”或更重要的决定,所以它们首先得到处理——它们有优先权。例如,在本章的大多数示例中,Alive是最外层的状态,无论堆栈中当前的内部状态是什么,如果生命值降至0,Alive将始终与Dead同级。此转换是高优先级转换,因此在内部转换之前进行处理。

含义
让我们讨论一下ProcessStateTransitions算法如何保持处理转换直到状态堆栈稳定的一些重要含义。
首先,请记住,在对StateMachine::ProcessStateTransitions的一次调用中,可以对任何给定状态多次调用State::GetTransition。因此,GetTransition不是执行“更新”逻辑的好地方——为此,请改用State::update函数,它保证在每个状态上只被调用一次。GetTransition实际上应该只通过读取/轮询状态数据来返回要进行的转换。换句话说,GetTransition除了返回修改状态堆栈的转换之外,通常应该没有任何副作用。
其次,ProcessStateTransitions有可能最终进入一个无限的转换循环——在这个循环中,状态堆栈永远不会稳定下来。一个简单的例子是,如果布尔变量foo为true,则状态A与B同级,而B与A同级则为foo为false。幸运的是,像这样的无限转换循环是由HSM通过断言检测和报告的,并且在跟踪输出的帮助下,通常很容易理解和纠正。(请参阅“延迟转换”一节,了解打破这种无限转换循环的一种方法)。
重新访问的更新状态
在第2章中,我们了解到StateMachine::UpdateStates可以用于在平面状态机中对当前状态调用State::Update。更一般地说,StateMachine::UpdateStates实际所做的是从最外层到最内层对状态堆栈上的每个状态调用State::Update。
让我们看一个UpdateStates如何工作的示例:

// revisit_update_states.cpp#include "hsm/statemachine.h"using namespace hsm;class MyOwner
{
public:MyOwner();void UpdateStateMachine();private:friend struct MyStates;StateMachine mStateMachine;
};struct MyStates
{struct BaseState : StateWithOwner<MyOwner>{};struct A : BaseState{virtual Transition GetTransition(){return InnerEntryTransition<B>();}virtual void Update(){printf("A::Update\n");}};struct B : BaseState{virtual Transition GetTransition(){return InnerEntryTransition<C>();}virtual void Update(){printf("B::Update\n");}};struct C : BaseState{virtual Transition GetTransition(){return InnerEntryTransition<D>();}virtual void Update(){printf("C::Update\n");}};struct D : BaseState{virtual void Update(){printf("D::Update\n");}};
};MyOwner::MyOwner()
{mStateMachine.Initialize<MyStates::A>(this);mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}void MyOwner::UpdateStateMachine()
{mStateMachine.ProcessStateTransitions();mStateMachine.UpdateStates();
}int main()
{MyOwner myOwner;myOwner.UpdateStateMachine();
}
HSM_1_TestHsm: Init    : struct MyStates::A
HSM_1_TestHsm:  Entry   : struct MyStates::B
HSM_1_TestHsm:   Entry   : struct MyStates::C
HSM_1_TestHsm:    Entry   : struct MyStates::D
A::Update
B::Update
C::Update
D::Update

从这个输出中,我们可以清楚地看到Update是从最外层到最内层调用的。
State::Update函数是一个状态应该执行与该状态相关的任何操作的地方。在分层状态机的上下文中,这允许您将状态的行为封装在它所属的位置——在状态本身中。
还要记住,StateMachine::UpdateStates是在StateMachine::ProcessStateTransitions之后调用的,这意味着只有在堆栈稳定后,才会对状态调用State::Update。这意味着某些状态可能永远不会调用State::Update,即使已转换到。例如,如果A转换到状态B,而B立即返回到状态C的同级转换,则不会调用B::Update。
还值得注意的是,与State::Update不同,State::OnEnter和State::on Exit总是在转换到的状态上调用,因此,如果状态必须始终执行操作,无论它们是否保留在StateMachine::ProcessTransitions结束时的状态堆栈上,都可以使用这些函数。

在最后一章中,我们介绍了如何使用HSM编写分层状态机的基本知识。在本章中,我们将了解一些高级技术,这些技术将有助于使状态机更易于管理,更具表现力。

状态值
当使用分层状态机时,出现的一种常见模式是在OnEnter中设置共享值,并在OnExit中恢复相同的值。在本节中,我们将学习如何利用HSM的一个名为StateValue的功能来促进这种常见模式。
没有状态值
在我们研究如何使用StateValue功能之前,我们将从一个直接在状态中设置/取消设置值的示例开始。再次,此代码演示了字符控制器的可能状态机:

// state_value_without.cpp#include "hsm/statemachine.h"using namespace hsm;class PhysicsComponent
{
public:void SetSpeed(float speed) {} // Stubvoid Move() {} // Stub
};class Character
{
public:Character();void Update();// Public to simplify samplebool mInWater;bool mMove;bool mCrawl;private:friend struct CharacterStates;StateMachine mStateMachine;PhysicsComponent mPhysicsComponent;float mSpeedScale; // [0,1]
};struct CharacterStates
{struct BaseState : StateWithOwner<Character>{};struct Alive : BaseState{virtual Transition GetTransition(){return InnerEntryTransition<OnGround>();}};struct OnGround : BaseState{virtual Transition GetTransition(){if (Owner().mInWater)return SiblingTransition<Swim>();return InnerEntryTransition<Stand>();}};struct Stand : BaseState{virtual Transition GetTransition(){if (Owner().mMove)return SiblingTransition<Move>();return NoTransition();}};struct Move : BaseState{virtual Transition GetTransition(){if (!Owner().mMove)return SiblingTransition<Stand>();return InnerEntryTransition<Move_Walk>();}};struct Move_Walk : BaseState{float mLastSpeedScale;virtual void OnEnter(){mLastSpeedScale = Owner().mSpeedScale;Owner().mSpeedScale = 1.0f; // Full speed when moving normally}virtual void OnExit(){Owner().mSpeedScale = mLastSpeedScale;}virtual Transition GetTransition(){if (Owner().mCrawl)return SiblingTransition<Move_Crawl>();return NoTransition();}};struct Move_Crawl : BaseState{float mLastSpeedScale;virtual void OnEnter(){mLastSpeedScale = Owner().mSpeedScale;Owner().mSpeedScale = 0.5f; // Half speed when crawling}virtual void OnExit(){Owner().mSpeedScale = mLastSpeedScale;}virtual Transition GetTransition(){if (!Owner().mCrawl)return SiblingTransition<Move_Walk>();return NoTransition();}};struct Swim : BaseState{float mLastSpeedScale;virtual void OnEnter(){mLastSpeedScale = Owner().mSpeedScale;Owner().mSpeedScale = 0.3f; // ~1/3 speed when swimming}virtual void OnExit(){Owner().mSpeedScale = mLastSpeedScale;}virtual Transition GetTransition(){if (!Owner().mInWater)return SiblingTransition<OnGround>();return NoTransition();}};
};Character::Character(): mInWater(false), mMove(false), mCrawl(false), mSpeedScale(0.0f) // By default we don't move
{mStateMachine.Initialize<CharacterStates::Alive>(this);mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}void Character::Update()
{// Update state machinemStateMachine.ProcessStateTransitions();mStateMachine.UpdateStates();// Move characterconst float MAX_SPEED = 100.0f;float currSpeed = mSpeedScale * MAX_SPEED;mPhysicsComponent.SetSpeed(currSpeed);mPhysicsComponent.Move();printf("Current speed: %f\n", currSpeed);
}int main()
{Character character;character.Update();character.mMove = true;character.Update();character.mCrawl = true;character.Update();character.mInWater = true;character.Update();character.mInWater = false;character.mMove = false;character.mCrawl = false;character.Update();
}

在我们讨论代码之前,让我们看看这个状态机的plotHsm输出:在这里插入图片描述
从这个情节中,我们可以推断出关于这个角色控制器的一些事情。我们看到这个角色可以在地上,也可以在游泳。在地面上时,角色可以站立或移动,移动时可以行走或爬行。
在示例代码中,有一个名为PhysicsComponent的类,它将负责移动角色的物理表示、处理冲突等。为了简单起见,这个类被截断了。在Character::Update中,我们可以看到在对该PhysicsComponent调用Move()之前,如何在此PhysicsComponent上计算和设置速度:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/164773.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

茶楼计时茶室时钟计费系统,佳易王共享茶室收银计时收费管理系统软件下载

茶楼计时茶室时钟计费系统&#xff0c;佳易王共享茶室收银计时收费管理系统软件下载 软件功能&#xff1a; 1、计时计费功能&#xff1a;可以按单价计费&#xff0c;可以按时间段计费。时间显示直观&#xff0c;每个桌子用时一目了然。每个桌子价格可以设置相同也可以不相同。…

kubernetes资源监控

目录 一、资源限制 1、limitrange 2、ResourceQuota 二、metrics-server 三、图形化监控和代码行监控 1、dashboard 2、k9s 四、hpa 一、资源限制 Kubernetes采用request和limit两种限制类型来对资源进行分配。request(资源需求)&#xff1a;即运行Pod的节点必须满足运…

关于卷积神经网络的填充(padding)

认识填充 &#xff08;padding&#xff09; 随着卷积层数的加深&#xff0c;输出进一步缩小&#xff0c;那么最终会导致输出很快就只剩下1∗1的数组&#xff0c;这也就没办法继续计算了&#xff0c;所以提出了填充的方法来方便网络的进一步加深。 其实填充的原因有两点&#xf…

JavaWeb Day05 前后端请求响应与分层解耦

目录 一、请求与响应 &#xff08;一&#xff09;请求的参数接收 ①数组参数 ②集合参数 ③日期参数 ④json参数 ⑤路径参数 总结 &#xff08;二&#xff09;响应 ①简单文本text ②数组 ③列表 ④同一响应数据格式 ⑤总结 二、三层架构与分层解耦 &#xff0…

宋浩高等数学笔记(一)函数与极限

b站宋浩老师的高等数学网课&#xff0c;全套笔记已记完&#xff0c;不定期复习并发布更新。 章节顺序与同济大学第七版教材所一致。 目录 1.1映射与函数 1.2数列的极限 1.3函数的极限 1.4无穷小和无穷大 1.5极限运算准则 1.6极限存在准则and两个重要极限 1.7无穷小 1…

【数组】【快慢指针】Leetcode 27 移除元素

【数组】【快慢指针】Leetcode 27 移除元素 解法1 ---------------&#x1f388;&#x1f388;题目链接&#x1f388;&#x1f388;------------------- 解法1 时间复杂度O(N) 空间复杂度O(1) class Solution {public int removeElement(int[] nums, int val) {// 快慢指针…

【UE4】UE编辑器乱码问题

环境&#xff1a;UE4.27、vs2019 如何解决 问题原因&#xff0c;UE的编码默认是UTF-8&#xff0c;VS的默认编码是GBK 通过"高级保存选项" 直接修改VS的 .h头文件 的 编码 为 UTF-8 步骤1. 步骤2. 修改编码后&#xff0c;从新编译&#xff0c;然后就可以解决编辑器…

在MacBook上实现免费的PDF文件编辑

之前我想对PDF文件进行简单处理&#xff08;比如删页面、添空白页、调整页面顺序&#xff09;&#xff0c;要么是开wps会员【花钱贵】&#xff0c;下载&#xff08;盗版&#xff09;Adobe Acrobat【macOS不好下载】&#xff0c;要么用福昕阅览器登陆学生账号&#xff08;学校买…

Jmeter接口自动化测试操作流程

在企业使用jmeter开展实际的接口自动化测试工具&#xff0c;建议按如下操作流程&#xff0c; 可以使整个接口测试过程更规范&#xff0c;更有效。 接口自动化的流程&#xff1a; 1、获取到接口文档&#xff1a;swagger、word、excel ... 2、熟悉接口文档然后设计测试用例&am…

第二证券:消费电子概念活跃,博硕科技“20cm”涨停,天龙股份斩获10连板

消费电子概念7日盘中再度拉升&#xff0c;到发稿&#xff0c;博硕科技“20cm”涨停&#xff0c;光大同创、波长光电涨超10%&#xff0c;易德龙、向阳科技、得润电子、天龙股份、同兴达等涨停。 博硕科技强势涨停&#xff0c;公司昨日在接受安排调研时表明&#xff0c;公司从上…

QT实现的一个MVP设计模式demo

最近做qt 项目,发现网上基于MVP设计模式的QT例程很少&#xff0c;这里写一个demo示例可作为参考&#xff1a; 一、简要概述 MVP是由MVC发展而来&#xff0c;总体目的与作用相同。都是为了软件构架有层次之分&#xff0c;使得核心逻辑、界面控制、数据这三者分层清晰明了。减少…

IDEA版SSM入门到实战(Maven+MyBatis+Spring+SpringMVC) -Maven核心概念

一.Maven的POM POM全称&#xff1a;Project Object Model【项目对象模型】&#xff0c;将项目封装为对象模型&#xff0c;便于使用Maven管理【构建】项目 pom.xml常用标签 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://m…