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

编程入门 行业动态 更新时间:2024-10-24 10:27:35

<a href=https://www.elefans.com/category/jswz/34/1771107.html style=机器人控制——C++ HSM状态机基础知识"/>

机器人控制——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()。在下一节中,我们将看到如何摆脱这种铸造。

更多推荐

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

本文发布于:2023-11-16 10:20:35,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1618835.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:机器人   基础知识   状态机   HSM

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!