【翻譯】遊戲設計模式之狀態機

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自學不成才

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章