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

news2024/11/26 15:26:45

本章将向您介绍使用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();
}`

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

签名加密发送请求

本篇文章介绍下通过HMAC-SHA1加密验证&#xff0c;将加密之后得到的签名作为参数发送请求&#xff0c;假如咱们需要按照下列要求将参数加密之后传递&#xff1a; 咱们来逐步分析得到签名的步骤和最终发送请求需要携带的参数内容&#xff0c;本次实现是在node.js的环境下。 首先…

计算机系统

由计算机硬件系统和计算机软件系统组成的综合体 冯诺伊曼计算机 定义 以“存储程序”的概念为基础的各类计算机通称为冯诺伊曼机 特点 计算机由运算器、存储器、控制器、输入设备和输出设备五大部件组成指令和数据以同等地位存放于存储器内&#xff0c;并可按地址寻访指令和…

Python Faker批量生成测试数据

一、前言 在做自动化测试或压力测试时会需要大批量生成测试数据&#xff0c;简单的方式你可以写一个存储过程使用随机函数来生成记录&#xff0c;但这种生成数据看起来不够真实&#xff0c;其实有蛮多现成的工具可以完成这一任务。 二、Faker基本使用介绍 faker是一个生成伪…

Python零基础入门教程(非常详细)

目前python可以说是一门非常火爆的编程语言&#xff0c;应用范围也非常的广泛&#xff0c;工资也挺高&#xff0c;未来发展也极好。 Python究竟应该怎么学呢&#xff0c;我自己最初也是从零基础开始学习Python的&#xff0c;给大家分享Python的学习思路和方法。一味的买书看书…

从零开始搭建微服务(一)

构建项目父工程 添加公共核心模块 安装nacos 安装nacos nacos 文档地址&#xff1a; https://nacos.io/zh-cn/docs/what-is-nacos.html 本文使用版本2.2.2 下载地址&#xff1a;https://github.com/alibaba/nacos/archive/refs/tags/2.2.2.zip 使用nacos 我们下载是源代码 解…

轻松编写第一个油猴脚本

第一步&#xff1a;安装油猴 首先&#xff0c;我们需要安装油猴。油猴是一个免费的浏览器扩展&#xff0c;可让您在浏览器中运行脚本。 在 Chrome 浏览器中&#xff0c;可以通过以下步骤安装油猴&#xff1a; 在 Chrome 浏览器中打开 Chrome 网上应用店。搜索 "油猴&qu…

揭秘ChatGPT的智慧密码:向量数据库的神奇作用解析

2023年8月&#xff0c;亚马逊云科技推出了自己的向量引擎Amazon OpenSearch Serverless向量引擎&#xff0c;这被认为是其旗下大语言模型Amazon Titan超越ChatGPT的一个重要契机。 那么&#xff0c;这个Amazon OpenSearch Serverless向量引擎有何厉害之处&#xff1f;为什么能…

Elasticsearch:ES|QL 中的数据丰富

在之前的文章 “Elasticsearch&#xff1a;ES|QL 查询语言简介”&#xff0c;我有介绍 ES|QL 的 ENRICH 处理命令。ES|QL ENRICH 处理命令在查询时将来自一个或多个源索引的数据与 Elasticsearch 丰富索引中找到的字段值组合相结合。这个有点类似于关系数据库查询中所使用的 jo…

数据治理概念篇:(一)相关术语与名词

数据治理内容众多&#xff0c;其中与数据治理相关名词和术语也是多不胜数。下面记录一下常见的数据治理相关名词。 1.数据仓库 1.1.名词解释 Bill Inmon 将数据仓库定义为&#xff1a;“面向主题的、整合的、随时间变化的、相对稳定的支持管理决策的数据集合”。 Ralph Kimbal…

【python】爬取斗鱼直播照片保存到本地目录

一、导入必要的模块&#xff1a; 这篇博客将介绍如何使用Python编写一个爬虫程序&#xff0c;从斗鱼直播网站上获取图片信息并保存到本地。我们将使用requests模块发送HTTP请求和接收响应&#xff0c;以及os模块处理文件和目录操作。 如果出现模块报错 进入控制台输入&#xff…

APP开发:用途与未来前景|软件定制开发|网站小程序建设

APP开发&#xff1a;用途与未来前景|软件定制开发|网站小程序建设 APP开发已成为现代科技趋势的一部分&#xff0c;无论是日常生活还是商业领域&#xff0c;都有它的身影。通过开发APP&#xff0c;我们可以将想法、功能和内容转化为直观、易用的移动设备应用程序&#xff0c;满…

【算法| 差分 No.1】AcWing 797. 差分 AcWing 798. 差分矩阵

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望对大家有所帮…

Spring中@Async的使用技巧

引言 在Java开发中&#xff0c;我们常常会遇到需要执行耗时操作的场景&#xff0c;例如文件上传、网络请求等。为了提高系统的响应速度和并发能力&#xff0c;我们可以使用异步方法来处理这些任务。本文将介绍如何在Java中使用异步方法&#xff0c;并探讨其中的一些注意事项。…

棋牌室电脑计时灯控,棋牌室计时灯控安装,佳易王计时计费管理系统软件

棋牌室电脑计时灯控&#xff0c;棋牌室计时灯控安装&#xff0c;佳易王计时计费管理系统软件 棋牌室的灯可以用佳易王计时计费软件来控制开关&#xff0c;当开始计时的时候&#xff0c;软件发送开灯的指令&#xff0c;灯打开&#xff0c;在结账后&#xff0c;软件发送关灯指令…

酷开科技 | 酷开系统里萌萌哒小维在等你!

在一片金黄淡绿的颜色中&#xff0c;深秋的脚步更近了&#xff0c;在这个气候微凉的季节里&#xff0c;你是不是更想拥有一种温暖的陪伴呢&#xff1f;酷开科技智慧AI语音功能更懂你&#xff0c;贴心的小维用心陪伴你的每一天。 01.全天候陪伴 在酷开系统中&#xff0c;只要你…

华为OD机试 - 高效的任务规划 - 逻辑分析(Java 2023 B卷 200分)

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#…

操作系统(一):进程状态与进程调度

前言 操作系统作为计算机基础的四大件&#xff0c;系统学习无疑是十分重要的。在这个系列的文章中&#xff0c;荔枝会结合操作系统的知识进行归纳梳理&#xff0c;总结输出博文&#xff01;下面这篇文章主要介绍的是进程状态和调度&#xff0c;重点是几种调度算法的理解和掌握&…

基于yolov2网络的人脸识别系统matlab仿真,包括识别正脸,侧脸等

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1、训练阶段 4.2、预处理阶段 4.3、识别阶段 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ........................................…

数据中心加密:保障数据安全的重要一环

随着信息化的快速发展&#xff0c;数据已经成为企业的重要资产&#xff0c;数据安全也成为了企业面临的重大挑战。数据中心作为企业数据存储和管理的重要场所&#xff0c;其安全性对于整个企业的数据安全具有至关重要的作用。而数据中心加密则是保障数据安全的重要一环。本文将…

15 款 PDF 编辑器帮助轻松编辑、合并PDF文档

PDF 编辑器在当今的数字环境中至关重要&#xff0c;因为 PDF 已成为共享和存储信息的首选格式。只需几分钟&#xff0c;可靠的 PDF 编辑器即可让用户能够根据其特定需求修改、定制和定制文档。在本文中&#xff0c;我们全面汇编了 15 款最佳免费 PDF 编辑器&#xff0c;让您可以…