【翻译】游戏设计模式之状态机

状态机(有限状态机、并发状态机、层次状态机) 2. 可以知道状态from-to,知道是从哪个状态向哪个状态进行切换,可以进行一些前置操作,例如在游戏暂停时重新开始游戏,可以在切换前重置游戏数据。使用状态机,可以避免使用if-else/switch-case进行逻辑的条件判断,导致代码臃肿,难以维护,扩展性差。游戏状态:初始化、开始、暂停、恢复、下一波敌人、3选1技能、失败、胜利、退出游戏等。但是角色的状态,行走、攻击、跳跃等是可以并行的,例如行走并攻击,跳跃并攻击。游戏流程的状态是单一的,从初始化,开始,结束等状态都只会同时存在一个。 阅读详情

Godot3游戏引擎入门之十四:RigidBody2D刚体节点的应用以及简单的FSM状态机介绍

一、前言

本文是一篇关于游戏设计模式之状态模式的文章内容翻译,我在上一篇文章 Godot3游戏引擎入门之十四:刚体RidigBody2D节点的使用以及简单的FSM状态机介绍中简单地介绍了 FSM 有限状态机的含义以及游戏中的简单实现,讲述的很浅显,如果你对游戏设计模式感兴趣,我相信本篇文章会适合你,如有翻译不当之处请谅解,哈哈。 ?

作者简介:
Robert Nystrom ,《 Game Programming Patterns》的作者
原文链接: http://www.gameprogrammingpatterns.com/state.html

二、正文

忏悔时间:我对这一章节的内容有点夸大其词。表面上是关于状态设计模式的探讨,但我不得不谈及游戏中关于有限状态机制(或称为 “FSM” )的基本概念。不过我一旦提及到这个,那么我想我也不妨介绍下分层状态机和下推自动机的概念以及相关原理。

这会涵盖多方面的知识点,为了尽可能地缩短文章篇幅,文中使用的代码示例省略了一些细节,这些是您必须自己填写的。不管怎样,我还是希望这些知识点仍然能够清晰以便能让你了解整个理念。

如果你从未听说过状态机,也请不要感到难过。状态机不像 AI 和编译器、黑客那样,它在编程圈子里没有那么耳熟能详。不过我认为它们更应该广为人知,所以我在这里会把它们抛到一个不同层次的问题上去看待。

我们曾经都见识过

假设我们正在研究一个往一边滚动的平台游戏。我们的工作是实现游戏中的女主角,即玩家在游戏世界中的化身。这意味着要让她响应用户的输入。比如按下 B 键,她应该跳跃。实现起来非常简单:

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    yVelocity_ = JUMP_VELOCITY;
    setGraphics(IMAGE_JUMP);
  }
}

有什么问题吗?

目前还不能阻止“在空气中跳跃”的发生——当她在空中时继续点击 B 键,她将永远漂浮下去。这里最简单的解决方法是给 Heroine 添加一个 isJumping_ 的布尔字段,用于跟踪判断她是否已经跳跃,然后再执行操作:

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    if (!isJumping_)
    {
      isJumping_ = true;
      // 起跳...
    }
  }
}

接下来,我们希望实现:如果女主角在地面上,玩家按下下方向键按钮她就能进行躲闪,而松开按钮的时候,她又会重新站起来:

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    // 如果没有跳跃就起跳...
  }
  else if (input == PRESS_DOWN)
  {
    if (!isJumping_)
    {
      setGraphics(IMAGE_DUCK);
    }
  }
  else if (input == RELEASE_DOWN)
  {
    setGraphics(IMAGE_STAND);
  }
}

这次有没有发现问题所在?

通过以上代码玩家可以实现:

  1. 按下按键躲闪。
  2. 按 B 键从闪避位置开始跳跃。
  3. 在空中松开按钮也能站立。

女主角跳跃在空中就能切换到她的站立姿势。是时候再添加另一个判断标记了……

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    if (!isJumping_ && !isDucking_)
    {
      // 跳跃...
    }
  }
  else if (input == PRESS_DOWN)
  {
    if (!isJumping_)
    {
      isDucking_ = true;
      setGraphics(IMAGE_DUCK);
    }
  }
  else if (input == RELEASE_DOWN)
  {
    if (isDucking_)
    {
      isDucking_ = false;
      setGraphics(IMAGE_STAND);
    }
  }
}

接下来,如果能够实现女主角在跳跃过程中,玩家只要按下下方向键按钮女主角就可以进行俯冲攻击的话,那确实很炫:

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    if (!isJumping_ && !isDucking_)
    {
      // 跳跃...
    }
  }
  else if (input == PRESS_DOWN)
  {
    if (!isJumping_)
    {
      isDucking_ = true;
      setGraphics(IMAGE_DUCK);
    }
    else
    {
      isJumping_ = false;
      setGraphics(IMAGE_DIVE);
    }
  }
  else if (input == RELEASE_DOWN)
  {
    if (isDucking_)
    {
      // 站立...
    }
  }
}

又是寻找 Bug 的时候了。找到问题了吗?

我们已经确定玩家在跳跃的过程中是不能继续在空中二次跳跃了,但这对于俯冲效果并不适用。看来我们又开辟了一个新的问题领域……

我们的方法显然存在一些问题。每当我们修改这些代码,我们都会破坏某些逻辑。我们还需要添加更多的动作——我们还没有添加行走行为呢——但是按照目前这个进度,它会在我们完成之前就已经崩溃成一堆的 Bug 了。

有限状态机救场

有点沮丧,不过至少你可以扫除桌面上除了纸和笔之外的所有其他东西,并开始来绘制一个流程图。你把女主角可以做的每个动作都画成一个长方形框:站立,跳跃,闪避和俯冲。当她处于其中的某一个状态并按下某个按钮时,您就可以从该状态框中画出来一个箭头,箭头上用这个按钮做标记,然后将其连接到她应该切换到的另一个状态上。

state-flowchart.png

恭喜,您刚刚创建了一个有限状态机。这来自计算机科学的一个分支,被称为自动机理论,其数据结构所在家族还包括著名的图灵机。 FSM 是该家族中最简单的一位成员。

几个要点是:

  • **你有一套固定的机械状态。**在我们的例子中,那就是站立,跳跃,闪避和俯冲。
  • **机器一次只能处于一种状态中。**我们的女主角不能同时既跳跃又站立。事实上,防止这种情况的发生正是我们将要采用 FSM 机制的原因之一。
  • **一系列输入或者事件会被发送到机器。**在我们的示例中,也就是原始的按键按下与释放动作。
  • **每个状态都有一系列转换机制,每个转换与某个输入相关联并指向另一个状态。**当有输入进入时,如果输入与当前状态的转换相匹配,则机器的状态将切换为转换所指向的新状态。

例如,在站立状态时按下下方向键就可以过渡到闪避状态。在跳跃状态下按下下方向键可以过渡到俯冲状态。如果没有给当前状态的输入定义转换,那么这个输入会被忽略。

说的纯粹点,它的整个组成就是:状态,输入和转换。您可以把它绘制成一个小流程图。不幸的是,编译器没法识别我们的涂鸦,那么我们如何才能实现一个呢?四人帮 Gang of Four 的状态模式就是其中的一种方案——我们可以做到———不过先让我们从简单点开始吧。

枚举和 Switch 语句

在我们的 Heroine 类中一个问题就是一些布尔字段的某些组合是无效的:比如, isJumping_ 和 isDucking_ 不能全部为 true 。如果你的一些标记中,符合一次只有一个是 true ,那就意味着你所需要的是一个 enum 枚举类。

在这种情况下,枚举 enum 的内容恰好是我们 FSM 的状态集,所以我们给出如下定义:

enum State
{
  STATE_STANDING,
  STATE_JUMPING,
  STATE_DUCKING,
  STATE_DIVING
};

取代了一堆布尔标志, Heroine 类中将只有一个 state_ 的字段。同时我们将选择分支的对象进行了反转。在之前的代码中,我们先判断输入,然后再根据状态进行判断。这会把同一个按钮的输入事件全写在了一起,导致某一个状态的代码的混乱。我们希望对同一个状态的处理保持在一块,因此我们以状态进行分支处理。代码如下:

void Heroine::handleInput(Input input)
{
  switch (state_)
  {
    case STATE_STANDING:
      if (input == PRESS_B)
      {
        state_ = STATE_JUMPING;
        yVelocity_ = JUMP_VELOCITY;
        setGraphics(IMAGE_JUMP);
      }
      else if (input == PRESS_DOWN)
      {
        state_ = STATE_DUCKING;
        setGraphics(IMAGE_DUCK);
      }
      break;

    case STATE_JUMPING:
      if (input == PRESS_DOWN)
      {
        state_ = STATE_DIVING;
        setGraphics(IMAGE_DIVE);
      }
      break;

    case STATE_DUCKING:
      if (input == RELEASE_DOWN)
      {
        state_ = STATE_STANDING;
        setGraphics(IMAGE_STAND);
      }
      break;
  }
}

这看起来有点繁琐,但它确实在之前的代码上有了真正的改进。我们还缺少一些条件分支,不过我们将可变状态简化成了单个的字段。现在所有处理单个状态的代码都很好地集中在一块了。这是实现状态机的最简单方式,适用于某些用途。

但是,你的问题很可能会超出这个方案。假设我们想要添加一个新动作,我们的女主角可以闪避一段时间以补充能量,然后发动某个特殊攻击。当她进行闪避动作时,我们需要跟踪其能量补充的时间。

我们向 Heroine 类添加 chargeTime_ 字段以存储攻击前所要花费的时间。假设我们已经有一个每帧都会调用的 update() 函数,我们在这里添加代码:

void Heroine::update()
{
  if (state_ == STATE_DUCKING)
  {
    chargeTime_++;
    if (chargeTime_ > MAX_CHARGE)
    {
      superBomb();
    }
  }
}

我们需要在她开始闪避的那一刻重置计时器,所以我们还需要修改 handleInput() 的代码:

void Heroine::handleInput(Input input)
{
  switch (state_)
  {
    case STATE_STANDING:
      if (input == PRESS_DOWN)
      {
        state_ = STATE_DUCKING;
        chargeTime_ = 0;
        setGraphics(IMAGE_DUCK);
      }
      // 处理其他输入...
      break;

      // 其他状态...
  }
}

总而言之,为了增加这种特殊的大招攻击状态,我们必须修改两个方法并在 Heroine 类上添加一个 chargeTime_ 字段,即使这个字段只有在女主角处于闪避的状态下才有意义。我们倾向于将所有的代码和数据完美地整合在一起。这方面四人帮的设计模式已经涵盖了。

设计模式之状态模式

对于对面向对象思想有深入了解的人来说,每个条件分支都是一个使用动态分派的机会(换句话说,也就是在 C++ 中使用虚拟方法)。我估计你可能会太深入而掉进了那个兔子打的洞里。有时候你需要的仅仅是一个 if 语句而已。

不过在我们的例子中,我们已经达到了一个转折点,即使用面向对象的思想更合适。这让我们顺理成章地使用状态模式。引用四人帮的话来说:

允许对象在其内部状态发生变化时更改其行为。这个对象貌似会更改它所在的类。

其实这并没有告诉我们多少东西。不过,我们的 switch 已经搞定了。他们所描述的具体模式,在我们的女主角类中实现起来像下面这样:

一个状态接口

首先,我们为状态定义一个接口。每个行为都依赖于状态——就是我们之前每个 switch 分支的地方——都转变成该接口中的虚拟方法。对我们来说,这里的方法就是 handleInput() 和 update() :

class HeroineState
{
public:
  virtual ~HeroineState() {}
  virtual void handleInput(Heroine& heroine, Input input) {}
  virtual void update(Heroine& heroine) {}
};
每个状态封装成类

对于每个状态,我们定义一个实现接口的类。它的方法定义了女主角在该状态下的一些行为。换句话说,从之前的 switch 语句中获取每个 case 情形并将它们移动到其对应的 state 类中。例如:

class DuckingState : public HeroineState
{
public:
  DuckingState() : chargeTime_(0) {}

  virtual void handleInput(Heroine& heroine, Input input) {
    if (input == RELEASE_DOWN)
    {
      // 转换为站立状态...
      heroine.setGraphics(IMAGE_STAND);
    }
  }

  virtual void update(Heroine& heroine) {
    chargeTime_++;
    if (chargeTime_ > MAX_CHARGE)
    {
      heroine.superBomb();
    }
  }

private:
  int chargeTime_;
};

注意我们还会将 chargeTime_ 字段从 Heroine 类中移出并移入到 DuckingState 类中。这真是太好了——这个数据段只有在该状态下才会有意义,现在的对象模型很明显地反映出了这一点。

状态委托

接下来,我们给 Heroine 类一个指向她当前状态的指针,抛弃每段长长的 switch 语句,然后将其委托给状态:

class Heroine
{
public:
  virtual void handleInput(Input input)
  {
    state_->handleInput(*this, input);
  }

  virtual void update()
  {
    state_->update(*this);
  }

  // 其他方法...
private:
  HeroineState* state_;
};

为了“改变状态”,我们只需要赋值 state_ 变量以指向不同的 HeroineState 对象即可。整个就是状态模式的全部了。

状态对象实例在哪里?

在这里我掩饰了一些东西。为了改变状态,我们需要给 state_ 字段赋值所要指向的新状态,但是这个新状态对象从哪里来呢?如果是通过我们的枚举类实现,这是一个欠缺思考的方式—— enum 枚举类型的值都是一些原始的基本数据类型,比如数字。但现在我们的状态的类型确是类,这意味着我们需要一个真实的实例来指向它。通常这有两种常见的方案:

静态类的状态

如果状态对象没有任何其他字段,则它存储的唯一数据是一个指向内部虚拟方法表的指针,这样就可以实现其他方法的调用。在这种情况下,我们并没有什么理由让其拥有多个实例。无论如何,实例化的每个对象都是完全一样的。

所以基于这种情形,你可以创建一个静态的类型实例。即使你有一堆的 FSM 状态机都是同时运行在同一个状态下,它们都是可以指向同一个实例的,因为它没有任何特定于某个机器的实例。

把静态实例放在哪里这取决于你。最好是找一个有意义的地方吧。没有什么特别的原因的话,让我们把静态对象放在状态的基类中吧:

class HeroineState
{
public:
  static StandingState standing;
  static DuckingState ducking;
  static JumpingState jumping;
  static DivingState diving;

  // 其他代码...
};

每个静态字段都是游戏中使用的对应状态的一个实例。为了能让女主角正常跳跃,站立状态下应该是这样编写的:

if (input == PRESS_B)
{
  heroine.state_ = &HeroineState::jumping;
  heroine.setGraphics(IMAGE_JUMP);
}
实例化的状态

但是有时候这并不管用。静态状态类不适用于闪避状态。它有一个 chargeTime_ 字段,这个字段是特定于女主角的闪避状态的。如果碰巧只有一个女主角,这在我们的游戏中是没问题的,但如果我们假设添加多人玩家进行合作,同时在屏幕上出现两个女主角,那我们就会遇到问题了。

在这种情况下,我们必须在进行状态转换的时候创建一个新的状态对象实例。这样每个 FSM 都有自己的状态实例。当然,如果我们分配了一个新的状态对象,那意味着我们需要释放当前的旧状态对象内存。我们得小心翼翼,因为触发状态改变的代码是位于当前的旧状态的方法内。我们不想从自己本身当中删除 this 引用。

相反,我们将允许 HeroineState 中的 handleInput() 方法可选地返回一个新的状态。如果这样做, Heroine 将可以删除旧的状态然后转换为新的,代码如下所示:

void Heroine::handleInput(Input input)
{
  HeroineState* state = state_->handleInput(*this, input);
  if (state != NULL)
  {
    delete state_;
    state_ = state;
  }
}

这样的话,在方法的返回值之前,我们不会删除先前的旧状态。现在,站立状态对象就可以通过创建新实例来转换为闪避状态了:

HeroineState* StandingState::handleInput(Heroine& heroine, Input input)
{
  if (input == PRESS_DOWN)
  {
    // 其他代码...
    return new DuckingState();
  }

  // 停留在当前状态。
  return NULL;
}

如果给我选择的话,我更倾向于使用静态状态模式,因为它们不会在每次状态更改的时候因为分配对象空间而消耗内存和 CPU 调用周期。当然,对于状态机,呃,这是一种思路。

动作的进入和退出

状态模式的目的是将一个状态的所有行为和数据都封装在同一个类中。一般我们已经差不多实现,但仍然还有一些东西要完成。

当女主角状态改变时,我们同时会切换她的精灵( sprite )图片显示。目前这个代码由她所要发生转换的旧状态持有。当她从闪避状态转为站立状态时,闪避状态就会设定其显示图形:

HeroineState* DuckingState::handleInput(Heroine& heroine, Input input)
{
  if (input == RELEASE_DOWN)
  {
    heroine.setGraphics(IMAGE_STAND);
    return new StandingState();
  }

  // 其他代码...
}

我们真正想要的是每个状态能控制其自己的图形显示。我们可以通过向状态类提供一个*进入( enter )*的行为来解决这个问题:

class StandingState : public HeroineState
{
public:
  virtual void enter(Heroine& heroine)
  {
    heroine.setGraphics(IMAGE_STAND);
  }

  // 其他代码...
};

回到 Heroine 类,我们修改一下处理状态更改的代码,以便在新状态下调用这个方法:

void Heroine::handleInput(Input input)
{
  HeroineState* state = state_->handleInput(*this, input);
  if (state != NULL)
  {
    delete state_;
    state_ = state;

    // 在新的状态上调用 enter 行为。
    state_->enter(*this);
  }
}

这使我们可以简化闪避状态类的代码如下:

HeroineState* DuckingState::handleInput(Heroine& heroine, Input input)
{
  if (input == RELEASE_DOWN)
  {
    return new StandingState();
  }

  // 其他代码...
}

现在这段代码仅仅只用来处理切换到站立的状态而已,而图形显示由站立状态自行处理了。嗯,现在我们的状态类是真的被封装起来了。关于进入动作的一个特别好的效果就是它们一定是在进入该状态时才调用,而且不管你是从哪个状态转换而来。

大多数真实的游戏中,状态图是会存在从多个状态转换到同一个状态的情况。例如,我们的女主角在她跳跃或俯冲后最终都呈现站立状态。这意味着我们最后还是会在状态转换所发生的每一个地方编写一些重复的代码。进入( Entry )状态的方法为我们提供了处理这一点的地方。

当然,同样我们也可以扩展它以支持退出( exit )行为。这只是我们在切换到新状态之前所调用要离开的旧状态中的一个方法。

有何收获?

我已经花了这么多时间安利你 FSM 有限状态机,不过现在我要把你从飘飘然状态拉回原地了。到目前为止我所说的一切都是没有什么问题, FSM 非常适合解决某些问题。但是他们最大的优点也即他们最大的缺陷。

状态机通过强制使用固定死的结构来帮助您解开那些乱成一团的代码。你所拥有的全部仅为一组固定的状态,一个单一的当前状态和一些用于进行状态转换的硬编码。

如果您尝试使用状态机来处理游戏中更复杂的事情,例如游戏 AI ,那么你首先得弄清楚这个模型的局限性。值得庆幸的是,我们的先人已经为我们找到了避开这些疑难杂症的方法。我将通过向你介绍其中几个解决方案来结束本篇文章的主要内容。

并发状态机

我们决定让我们的女主角拥有携带枪支的能力。当她正在射击的时候,她仍然可以做之前所能做的一切动作:跑步,跳跃,闪避等等。而且她也能够在做这些动作的同时发射她的武器。

如果我们坚持使用 FSM 的范畴,那么我们必须将拥有的状态数量扩大一倍。对于每个现有的状态,我们同时需要另外一个她背着武器做同样事情的状态:站立,背着枪站立,跳跃,背着枪跳跃,嗯,你应该明白了。

再来添加几个武器,然后把状态进行组合,数量一下子爆增。不仅是大量的状态,而且还增加了大量的冗余:对于非武装和武装状态下的状态,除了处理射击的一点点代码外,其他几乎完全相同。

这个问题在于我们将两个状态——她正在做什么以及她所携带的东西——塞进了一个单一的状态机中。为了模拟所有可能的组合,我们需要编写成对的状态。这个问题的解决方案也很明显:分别设立两个独立的状态机。

我们先不管之前状态机做了些什么,我们只管保留原来的状态机。然后我们再分开单独定义一个她携带东西时的状态机。 Heroine 类将拥有两个“状态”引用,对应我们定义的两个状态机,如下代码:

class Heroine
{
  // 其他代码...

private:
  HeroineState* state_;
  HeroineState* equipment_;
};

当女主角向各状态委托处理输入时,她将输入交给两个相应的函数分别进行处理:

void Heroine::handleInput(Input input)
{
  state_->handleInput(*this, input);
  equipment_->handleInput(*this, input);
}

然后,每个状态机可以响应输入,生成相应的行为,并独立于其他状态机而各自更改其状态。这里两组状态机大多不会相关联,这样处理很有效。

在项目实践中,你确实会发现某些情形下状态机之间会发生一些交互。例如,或者她并不能边跳跃边开火,或者如果她有武装,那么她就不能进行俯冲攻击。为了解决此类问题,在一个状态的代码中,你可能会简单地使用 if 语句测试其他机器的状态来协调它们之间的交互。这当然不是最优雅的解决方案,但它至少可以搞定这个目标。

分层状态机

在完善了我们的女主角的一些行为后,她可能会有一堆相似的状态。例如,她可能有站立,行走,跑步和滑动状态。在其中任何一个状态下,按下 B 键会跳跃再按下方向键则俯冲。

如果通过一个简单的状态机实现,那么我们必须在每个状态中复制这段代码。如果我们能够只实现一次,然后在所有的状态中重用它那就更好了。

如果把这当做面向对象中的代码而不是状态机,那么这些状态共享代码的一种方式就是使用继承。我们可以定义一个“在地面上”的类来处理跳跃和闪避。然后,站立,行走,跑步和滑动将继承于它并添加他们各自应有的附加行为。

事实证明,这是一种被称为分层状态机的常见结构。一个状态可以有一个状态超类(使自己成为一个子状态)。当一个事件发生时,如果子状态没有处理它,它会顺着继承链到达状态的超类然后进行处理。换句话说,它就像继承中方法的重写一样。

实际上,如果我们使用 State 状态模式来实现我们的 FSM 有限状态机,我们可以使用类继承来实现层次结构。为状态超类定义一个基类:

class OnGroundState : public HeroineState
{
public:
  virtual void handleInput(Heroine& heroine, Input input)
  {
    if (input == PRESS_B)
    {
      // 跳跃...
    }
    else if (input == PRESS_DOWN)
    {
      // 俯冲...
    }
  }
};

然后每个子状态都继承于它:

class DuckingState : public OnGroundState
{
public:
  virtual void handleInput(Heroine& heroine, Input input)
  {
    if (input == RELEASE_DOWN)
    {
      // 站立...
    }
    else
    {
      // 不处理输入,顺着继承链往上走。
      OnGroundState::handleInput(heroine, input);
    }
  }
};

当然,这并不是实现层次结构的唯一方式。如果你没有使用 Gang of Four 四人帮的状态模式,这将不会起作用。相反,你可以使用一堆状态而不是主类中的单个状态来进行显式地模拟当前状态的超类继承链。

当前状态处于堆栈的顶部,在它之下则是它的直接超类,然后是该超类的超类。当你提出一些特定于状态的行为时,你便可以从堆栈的顶部开始往下走,直到其中某一个状态能够处理它。 (如果没有,你就忽略它吧。)

下推自动机

有限状态机的另一个比较常见的扩充就是使用状态堆栈。令人困惑的是,堆栈实质上代表着一种完全不同的东西,它也是用于解决完全不同的问题。

这里存在的问题是有限状态机没有什么过往历史概念。你仅知道自己当前处于什么状态,但对你过去所处的状态没有记忆保留。并没有什么方法可以回到以前的状态去。

这里有一个例子:早些时候,我们让无畏的女主角先行全付武装起来。当她开枪时,我们需要一个新的状态来播放射击的动画并不断生成子弹和对应的视觉效果。因此,我们弄了一个 FiringState 拼到一起,同时还要弄出来所有那些当射击按钮按下时可以过渡到这个新状态的其他状态。

而棘手的部分就是她在射击后所要过渡到的状态。她可以在站立,跑步,跳跃和闪避时弹出一些特效。当射击相关的一系列动作完成后,她应该回到她之前正在做的动作状态。

如果我们坚持使用这香喷喷的 FSM ,那么我们早已经忘记了她之前所处的是什么状态了。为了跟踪之前的状态,我们必须又定义一系列几乎完全一样的状态——站立时射击,边跑边射击,射击时跳跃等等——这样每个人都有一套可以正确地回到之前状态的硬编码转换代码了。

其实我们真正喜欢的方式是先存储她在射击之前所处的状态,之后再返回去调用它。同理,这就是自动机理论发挥作用的地方。相关数据结构被称为下推自动机。

在有限状态机只有一个指向状态的指针的情况下,下推自动机则拥有一个状态堆栈。在 FSM 中,当转换到新状态后将覆盖前一个状态。下推自动机也可以让你这样处理,但它同时还为你提供了两个额外的操作:

  1. 您可以将新的状态推入堆栈中。 “当前”的状态始终处于堆栈的顶部,这样实现转换为新的状态。同时它将先前的状态压在了新状态的下面,而不是直接丢弃它。
  2. 您可以将最顶层的状态弹出堆栈。该状态被丢弃,而它下面的状态则成为新的当前状态。

state-pushdown.png

这正是我们解决射击状态所需要的。我们创建了一个单一的射击状态。当处于任何其他状态情况下,按下射击按钮时,我们将射击状态推到堆栈顶层。当射击动画完成后,我们将该状态弹出,同时下推自动机自动将我们的状态转回之前的状态。

那么,这些东西有用吗?

即使对状态机的发展有这些常见的扩充,但是它们仍然非常有限。如今,人工智能游戏领域中的趋势更倾向于使用行为树和规划系统等令人兴奋的事物。如果您感兴趣的是那些复杂的 AI ,那么本章内容一定能够刺激到您的胃口。你会想要阅读其他更多相关的书籍以满足自己的兴趣。

这并不意味着有限状态机,下推自动机以及其他简单的系统都是毫无用处的。对于某类问题,它们确实是一个非常不错的模型工具。有限状态机在以下情况非常有用:

  • 你有一个实体,其行为根据某个内部状态变化而变化。
  • 该状态可以严格地划分为相对比较小的不同选项。
  • 随着时间推移,实体会对一系列的输入或者事件进行响应。

在游戏中,它们最常用于 AI ,但它们在用户输入处理,菜单导航切换,文本解析,网络协议以及其他异步行为的实现中也很常见。

三、其他

以上就是主要内容,有任何建议请给我留言吧,谢谢! ?

我的博客地址: http://liuqingwen.me ,我的博客即将同步至腾讯云+社区,邀请大家一同入驻: https://cloud.tencent.com/developer/support-plan?invite_code=3sg12o13bvwgc ,欢迎关注我的微信公众号:
IT自学不成才

LabVIEW状态机与数组操作实战:从零构建2048游戏 状态机是控制复杂程序流程的核心设计模式,通过定义明确的状态(如初始化、等待、处理、更新)和状态间的转移条件,它能清晰管理程序的执行逻辑,尤其适用于交互式应用。在数据层面,数组是存储和处理结构化信息的基石,对数组的遍历、筛选、合并等操作是算法实现的关键。结合事件驱动架构,程序能高效响应用户输入,实现实时交互。这些基础技术广泛应用于自动化测试、仪器控制和工业监控等场景。本文以开发2048游戏为例,具体展示了如何运用LabVIEW的状态机架构和数组操作来处理游戏逻辑,并深入探讨了事件驱动编程在实现用户交互中的应用 阅读详情

相关推荐

OTFS从零开始11 10

未来的无线网络有望提供全球覆盖,因此需要支持在恶劣环境下的各种新兴应用,例如飞机上的移动通信(MCA)、低地球轨道(LEO)卫星通信、车对车(V2V)通信、高速铁路(HSR)通信、无人驾驶飞机(UAV)通信和水下声学通信(UAC)。在这些恶劣环境中的无线信道通常被视为对通信不友好,其特点是严重的延迟和多普勒扩散,以及有限的路径寿命等。这些不利于通信的特性使目前移动网络中广泛采用的正交频分复用(OFDM)调制的性能恶化。

qq_46009045的博客 9857

状态模式:游戏、工作流引擎中常用的状态机是如何实现的?

有限状态机,英文翻译是Finite State Machine,缩写为FSM,简称为状态机。状态机有3个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。

abc123mma的博客 3395

Wafer的T7 Code是什么,怎么来的

在半导体Cim系统中,waferstart 业务需要支持 Sorter 读取 Wafer 的 T7 Code,与 MES 的 Wafer ID 进行绑定,以便向 前追溯 Wafer 历史,那么问题来了,T7code 是什么Wafer的T7 Code是一种,用于标识双面抛光的晶圆。这种标记采用二维矩阵代码符号,适用于各种尺寸的晶圆,不仅限于450mm无缺口晶圆7。T7 Code是衬底来料背面通常会有的供应商刻的追溯ID,这是一串数字和字母组成的流水码。

阿拉伯梳子的专栏 7927

设计模式之美64-状态模式:游戏、工作流引擎中常用的状态机是如何实现的?

有限状态机,英文翻译是Finite State Machine,缩写为FSM,简称为状态机。状态机有3个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。对于刚刚给出的状态机的定义,我结合一个具体的例子,来进一步解释一下。“超级马里奥”游戏不知道你玩过没有?

fegus的博客 1538

VINS-Fusion实战:从零搭建无人机视觉惯性导航系统(附ROS配置指南)

本文提供了一份详尽的VINS-Fusion实战指南,涵盖从硬件选型、传感器标定到ROS节点配置与参数调优的全过程。重点解析了如何将VINS-Fusion视觉惯性里程计系统与PX4飞控深度集成,并实现室外GPS融合,旨在帮助开发者从零搭建稳定、高精度的无人机视觉惯性导航系统。

lll78的博客 865

【时序列】时序列数据如何一步步分解成趋势(trend)季节性(seasonality)和误差(residual)- 详细理解python sm.tsa.seasonal_decompose

【时序列】时序列数据如何一步步分解成趋势(trend)季节性(seasonality)和误差(residual)- 详细理解python sm.tsa.seasonal_decompose 在做时序列分析的时候,好多教程都告诉你要把时序列分解成趋势,季节性,残差,然后画图看一下有没有趋势变化,有没有季节性。像这样: import statsmodels.api as sm decomposition = sm.tsa.seasonal_decompose(train['Count'] ,model='add

weixin_47442572的博客 2万+

状态设计模式

状态模式是状态机的一种实现方式即可。状态机又叫有限状态机,它有3个部分组成:状态、事件、动作。其中,事件也称为转移条件。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。状态机的三种实现方式第一种实现方式叫分支逻辑法。利用if-else或者分支逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。对于简单的状态机来说,这种实现方式最简单、最直接,是首选。第二种实现方式叫查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。

weixin_44399827的博客 5009

基于STM32L4XX、HAL库的AD7124-8BCPZ模数转换器ADC驱动程序设计

/ 公式: Voltage = (adc_value / (2^23 - 1)) * Vref。// 模式控制位[7:5]=000 (连续转换模式)// 3.2 配置通道0,使其使用AIN0(+)和AIN1(-),并关联到配置0和滤波器0。= 0x14) { // AD7124-8的ID是0x14。// 双极性操作,缓冲AINP/AINN。

zhusl6688的博客 376

【游戏设计模式】之三 状态模式、有限状态机 & Unity版本实现

游戏开发过程中,各种游戏状态的切换无处不在。但很多时候,简单粗暴的if else加标志位的方式并不能很地道地解决问题,这时,就可以运用到状态模式以及状态机来高效地完成任务。状态模式与状态机,因为他们关联紧密,常常放在一起讨论和运用。而本文将对他们在游戏开发中的使用,进行一些探讨。 PS:这篇文章起源于《Game Programming Patterns》第二章第六节。这是一篇略长的文章,约5200

【浅墨的游戏编程Blog】毛星云(浅墨)的专栏 6万+

4 屏 + 8 摄像头一开,谁在悄悄拖慢你的座舱域控?

如果你觉得座舱卡顿:❌ 别第一反应怪 SoC❌ 别盲目堆算力。

weixin_43199439的博客 148

电磁阀颤振工作原理

颤振 颤振,弹性结构在均匀气(或液)流中受到空气(或液体)动力、弹性力和惯性力(见达朗伯原理)的耦合作用而发生的大幅度振动,它是气动弹性力学中最重要的问题之一。 随着机电液一体化技术的发展,电液比例控制技术的应用越来越广泛。电液比例控制系统范围较广,在液压传动与控制过程中,系统能够接受模拟信号或数字信号,使输出的流量或压力连续成比例地变化,都属于此类。该控制系统的优点是充分利用了液压控制与电气控制的长处,即功率传递靠液压,信号处理靠电气的优化措施。 电液比例控制原理与要求 液压控制系统中两个最重要的被控参数

小幽余生不加糖 1万+

游戏设计模式--状态机模式(翻译整理)(上篇)

状态机设计模式本身有比较多的基础理论在里面,但在这里,我们游戏所用的有限状态机FSMs,我们用一种简单的方式,来学习和分析。当我们知道了,为什么要用,有什么优点后,对状态机的理解就容易多了。当然在下面的例子中,我们的代码并不是完美的,有很多细节是需要自己去完善的。原贴: http://gameprogrammingpatterns.com/state.html 现在就让我们开始吧:)

wylionheart的专栏 1140

置顶必读(1) |《YOLOv11实战:从入门到深度优化》专栏导读,带你一文了解本专栏!

🏆 本文收录于 《YOLOv8实战:从入门到深度优化》 专栏,该专栏专注于分享我在YOLOv8目标检测模型中的实战经验,涵盖从入门基础到深度优化的全方位解决方案。无论你是计算机视觉的新手,还是具有多年深度学习经验的专家,本专栏都将为你提供高效、可复现的技术方案,助你掌握YOLOv8的核心技术,提升目标检测性能,突破瓶颈,优化模型,并加速项目开发🚀!

**My Coding Family** 12万+

游戏设计模式--状态机模式(翻译整理)(下篇)

状态模式 随着面向对象对大家的深入人心。每一个条件分支,都会觉得是一个使用动态调度的地方(换句话说,就是我们c++中的虚拟函数)。我想你可能会进一步的去思考这个问题。但是有的时候,if是够使用的。 但是在我们这个例子里面,面线对象的思路对我们才是最合适的。在四人帮的设计模式中,有这么一句话: Allow an object to alter its behavior when i

wylionheart的专栏 1951

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

游戏中的设计模式一

介绍游戏编程中常用的一些设计模式

antsmall的专栏 723

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

Unity C# 《有限状态机》的用法教程详解

Unity C# 《有限状态机》的用法教程详解 有限状态机用法教程 本文提供全流程,中文翻译。 助力快速理解 FSM 有限状态机,完成游戏状态的切换 为新手节省宝贵的时间,避免采坑! 有限状态机简称: FSM —— 简称状态机 是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型 FSM 一共2个类,不需要挂载到游戏对象上 FSMState 状态父类,所有子类状态都继承与这个类 例如以下工程:我们要需要2个状态: 菜单状态 与 游戏状态 那么这两个类MenuState 和 Ga

Chinar优快云的博客 1万+

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

设计模式:状态模式,游戏、工作流引擎中常用的状态机是如何实现的

状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。 什么是有限状态机 有限状态机,英文翻译是 Finite State Machine,缩写为 FSM,简称为状态机。状态机有3个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也被称为转移条件(Transition Condition)。事件触发状态的转移以及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。 举个例子。“超级马里奥”游戏中,马里奥可以变身为多种形态,比如小马里奥(.

OceanStar的博客 1766

趣谈设计模式 | 状态模式(State):如何实现游戏中的状态切换?

文章目录案例:马里奥积分竞赛有限状态机分支逻辑法查表法状态模式孪生兄弟:状态模式与策略模式总结完整代码与文档 案例:马里奥积分竞赛 喜欢马里奥的小伙伴们都应该知道,前不久马里奥为了庆祝35周年,推出了一款以多人对抗大逃杀为核心的超级马里奥兄弟35 此处仅为举例,并无此开发计划 由于新颖的游戏模式带来了巨大的热度,于是任天堂决定趁热打铁,推出一款马里奥竞技游戏,在一定时间内获得积分最多的玩家将获得胜利。考虑到游戏并非正传,于是任天堂将游戏的开发工作外包给了小明所在的游戏公司来进行制作。 游戏的核心玩法就是

凌桓丶的博客 3989

go设计模式-状态模式

状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。该模式将与状态相关的行为抽取到独立的状态类中,让原对象将工作委派给这些类的实例,而不是自行进行处理。状态迁移有四个元素组成,起始状态、触发迁移的事件,终止状态以及要执行的动作,每个具体的状态包含触发状态迁移的执行方法,迁移方法的实现是执行持有状态对象的动作方法,同时设置状态为下一个流转状态;持有状态的业务对象包含有触发状态迁移方法,这些迁移方法将请求委托给当前具体状态对象的迁移方法。有限状态机,英文

湫然的博客 950

快马AI:Unity游戏敌人AI状态机的生成式工作流

游戏AI状态机是实现NPC行为逻辑的核心技术,其本质是基于有限状态机(FSM)原理,将巡逻、警戒、追击等行为抽象为可切换的状态节点,并通过条件触发完成流转。该技术显著提升行为可维护性与调试效率,广泛应用于Unity中敌人AI、Boss战机制及多角色协同系统。随着开发迭代加速,传统手写FSM或拖拽式插件面临协作低效、参数耦合、动画绑定繁琐等工程瓶颈。快马AI作为专为Unity设计的生成式工作流引擎,支持自然语言定义状态流转,自动输出含Inspector参数、Animator Controller配置及完整St

weixin_30410119的博客 414

《设计模式之美》状态模式:游戏、工作流引擎中常用的状态机是如何实现的?

王争《设计模式之美》学习笔记 在实际的软件开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。 状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。 什么是有限状态机 有限状态机,英文翻译是Finite State Machine,缩写为FSM,简称为状态机。 状态机有3个组成部分:状态(State)、事件(Event)、动作(Action)。 其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不

玲珑巫女的学习笔记 434

设计模式(状态模式)

在实际的软件开发中,状态模式并不是很常用,但是在能够用到的场景里,它可以发挥很大的作用。从这一点上来看,它有点像我们之前讲到的组合模式。状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。不过,状态机的实现方式有多种,除了状态模式,比较常用的还有分支逻辑法和查表法。今天,我们就详细讲讲这几种实现方式,并且对比一下它们的优劣和应用场景。有限状态机,英文翻译是Finite State Machine,缩写为FSM,简称为状态机。

pp1981002445的博客 2570

Unity状态机进阶:从Switch到面向对象的设计模式实践

状态机是游戏开发中管理复杂对象行为(如角色控制、UI流程)的核心设计模式。其原理是将对象的行为封装到不同的状态对象中,通过状态切换来改变行为,从而实现逻辑的解耦与模块化。这种面向对象的设计方法(常基于状态模式)在工程上具有极高的价值,它显著提升了代码的可维护性和扩展性,符合开闭原则,使新增状态如同添加新类一样简单。在Unity游戏开发中,代码驱动的状态机常与Animator动画系统协同工作,分别管理逻辑状态与表现状态,广泛应用于角色控制器、AI行为树、游戏流程管理等场景。本文深入探讨了如何构建一个轻量级、可

weixin_29009501的博客 246

如何为Crow Translate添加新的翻译引擎:完整开发教程

Crow Translate是一款功能强大的开源翻译工具,支持Google、Yandex、Bing、LibreTranslate和Lingva五大翻译引擎。如果你想要扩展其功能,添加新的翻译引擎,这篇完整开发教程将为你提供详细的步骤指导。无论是想要集成DeepL、百度翻译还是其他API服务,本文都将帮助你理解Crow Translate的翻译引擎架构,并掌握添加新引擎的核心技术要点。 ## 🚀

gitblog_00555的博客 583

探索《游戏编程模式》中文版:提升你的游戏开发技艺

探索《游戏编程模式》中文版:提升你的游戏开发技艺 项目简介 在软件工程领域中,设计模式是一种被广泛接受的最佳实践,用于解决常见的编程问题。而在游戏开发中,这些模式同样至关重要。是一个开源项目,它翻译了原作者 Robert Nystrom 的经典著作《Game Programming Patterns》,为中文开发者提供了一本深入理解游戏编程原理和技巧的指南。 这本书旨在帮助游戏开发者理解如何更有效...

gitblog_00061的博客 815
上一篇: Godot3游戏引擎入门之十四:RigidBody2D刚体节点的应用以及简单的FSM状态机介绍
下一篇: Godot3游戏引擎入门之十五:RigidBody2D刚体节点的几种应用场景及示例
IT自学不成才
博客等级 码龄19年 222粉丝 30原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值