文章基于【GameAIPro3,Chapter12 —— A Reusable, Light-Weight Finite-State Machine】
总的来说,我们通过状态机、状态、过渡对象三个类的实现,基于策略模式,实现一个可替换状态和过渡逻辑的状态机
//状态类
class State
{
GameObject* m_pOwner;
public:
State(GameObject* pOwner)
: m_pOwner(pOwner)
{ }
virtual ~State() { }
virtual void OnEnter() { }
virtual void OnExit() { }
virtual void OnUpdate(float deltaTime) { }
protected:
GameObject* GetOwner() const { return m_pOwner; }
};
//状态机类
class StateMachine
{
typedef pair<Transition*, State*> TransitionStatePair;
typedef vector<TransitionStatePair> Transitions;
typedef map<State*, Transitions> TransitionMap;
TransitionMap m_transitions;
State* m_pCurrState;
public:
void Update(float deltaTime);
};
void StateMachine::Update(float deltaTime)
{
// find the set of transitions for the current state
auto it = m_transitions.find(m_pCurrState);
if (it != m_transitions.end())
{
// loop through every transition for this state
for (TransitionStatePair& transPair : it->second)
{
// check for transition
if (transPair.first->ToTransition())
{
SetState(transPair.second);
break;
}
}
}
// update the current state
if (m_pCurrState)
m_pCurrState->Update(deltaTime);
}
//过渡对象类
class Transition
{
GameObject* m_pOwner;
public:
Transition(GameObject* pOwner)
:m_pOwner(pOwner)
{ }
virtual bool ToTransition() const = 0;
};
数据驱动帮助我们构建一个轻量、可重复使用的状态机,因此我们需要一个好的数据驱动方案 Unity这种主流游戏引擎,通过反射系统和图形化界面使得我们通常可以在编辑器模式下设定数据,不过具体怎么实现还不好说
直接将每个状态的属性暴露在外部,让编辑者在Inspector窗口中直接进行编辑,不过因为State类一般设定为不可挂载于游戏物体之上,所以具体的实现还不好说
将状态需要的所有数据打包为一个新的类StateDef,根据需要创建不同的子类来保存多种数据组合,并且将他们保存为Json文件。不同状态在需要时加载各自的Json进行初始化。这种方法一定程度解决了复用性的问题,也不需要状态将属性暴露在外部;但仍旧可能需要创建多种结构来保存多种数据组合,并且Json文件的保存和读取也不是很直观。
Unity嵌入Lua、XML,通过 Lua 表格或XML,开发人员和设计师可以方便地调整和配置游戏中的状态机逻辑,而无需重新编译代码,待实现。
无论我们选择哪种解决方案,都需要考虑如何组织这些数据。最重要的是允许根据数据对状态和转换进行参数化。 理想情况下,我们应该只需要少数几种通用状态和转换,每种状态和转换都可以设定不同的数据来提供不同的行为。
由于在实际应用中,可能有许多挂载着状态机的游戏物体,每个状态机有有着很多种状态,这个时候要在每帧都去遍历它们来执行,就很耗费性能了,因此限制状态机的更新就很重要了
这次我们完全取消状态更新,而使整个过程都由事件驱动。虽然实现起来比较麻烦,但如果对系统的性能有很大的要求,还是值得的。 在每个状态进入时,将执行函数注册到事件中,由事件驱动状态更新
在内存方面,一个很大的问题是,相同的状态机如果存在于多个游戏对象中,就会非常浪费。因为他们的行为都是一样的,导致大量的数据实际上是重复的。
这里的一个解决方案是将状态机(StateMachine)分成两个不同的类,一个用于处理在运行时永远不会改变的静态数据,另一个用于处理会改变的易变数据。存储易变数据的类会持有对静态数据类的引用,实际上就是 Flyweight 设计模式。
沿着这个思路,我们可以将每个状态机类中,基本上不会改变的数据抽取出来,存储在每个状态机实例共用的一个黑板中。比如状态之间允许的跳转规则、状态的通用参数等。为每个敌人仅创建存储动态数据的实例,保留对静态数据类的引用,并且在需要时去引用它
进一步优化的思路是把所有动态数据(比如角色的位置、血量等)集中在一个“黑板”上,任何状态都可以从黑板上读写数据。
状态本身不需要保存数据(通用化):比如“跳跃状态”不需要知道具体角色是谁,只需从黑板读取当前角色的数据,因此同一个状态实例可被所有角色共享。每个角色只要维护自己的状态黑板即可。
评论区
共 条评论热门最新