1. 程式人生 > >譯文|GAMEMAKER STUDIO 系列:簡單狀態機

譯文|GAMEMAKER STUDIO 系列:簡單狀態機

按照設計,狀態機一次只能處於一種狀態。 由我們來定義對我們的情況有意義的狀態,以及它們之間的關係。 在本文中,我們將使用狀態機來控制在任何給定時間可用的玩家操作,允許我們設定角色並定義角色可以執行的操作。

大家好, 今天我想告訴你如何設定一個簡單的狀態機。 狀態機是一種資料結構,顧名思義,它跟蹤不同的狀態。 例如,我們的遊戲可能有三種狀態:“遊戲執行”,“遊戲暫停”和“遊戲結束”。我們可能會使用狀態機來記住哪一個處於活動狀態,並定義如何從一個狀態轉換到另一個狀態(請參閱 上面的圖片)。

基礎設定

此條目需要比上一個篇文章(繪製精靈)多一些動畫,因此在開始之前,你需要將這些動畫新增到工程中。 從此

連結下載精靈並將其新增到你的專案中。 我已經恰當地命名了檔案,因此只要確保精靈的名稱與檔名相匹配,就可以將其新增到 GMS 中。 繼續新增所有精靈 - 甚至是敵人的精靈 - 因為我們將在以後的文章中需要這些精靈。 確保每個精靈的原點是 (16,32)

列舉,控制器和永續性

為了設定我們的狀態機,我們首先確定哪些狀態是可能的,以及我們如何在程式碼中識別它們。 由於這個例子都是關於角色動作的,所以讓我們定義那些動作是什麼,並給每個動作一個整數 id。 最簡單的方法是使用 enum(列舉,Enumeration 的縮寫),它是在自定義變數型別下儲存的常量的集合。 如果你熟悉 Gamemaker 中的 Macros(巨集),那麼列舉就是這樣的。 我更喜歡使用列舉,因為它們比巨集更容易管理和跟蹤。

建立一個指令碼並將其命名為enum_init 新增以下行。
//states enum states  { normal, crouch, attack, hit }

請注意,我們不必設定每個條目的值,列舉自動將值0指定為“正常”,然後在每個條目後遞增。 我們可以隨時獲得值

var example = states.attack; show_message(string(example)); //output: 3

實際上,你可以通過為每個條目指定值來覆蓋列舉的自動編號,但重要的是要重申列舉是常量。 它們定義後無法更改!

列舉也是全域性的,這意味著任何物件都可以訪問它們。 這對我們的狀態機來說非常完美。

現在我們有了列舉,我們在哪裡例項化它? 從非持久物件呼叫這些變數,比如我們的 oPlayer 物件,不是最好的主意。 我們想要做的是建立一個持久的控制器物件(它始終存在),以管理許多物件可以訪問的列舉和其他資料型別之類的東西。 繼續建立一個新物件並將其命名為“con”(譯註:建議還是 oController 這樣與其他物件保持相同的命名字首,在看程式碼時會比較易讀。)。 我喜歡保持我的控制器名稱簡短,因為它更容易返回。選中新物件上的“持久(Persistent)”框。 最後,將 Create 事件新增到物件,並新增以下面的程式碼:

///init 初始化 
enum_init();

將 con 物件放在你的房間裡。 由於此物件是持久的,因此除非您明確銷燬,否則它將繼續存在! 無需在每個房間放置此物體。

Switch cases

既然我們已經在列舉中定義了狀態,我們就可以從我們的玩家物件中訪問它們了。 開啟我們在上一個條目中建立的 oPlayer 物件,並將以下行新增到 create 事件中。

attack = false; //states  currentState = 0; lastState = 0; //movement  xSpeed = 0; ySpeed = 0; lastSprite = sprite;

將 End Step 事件新增到 oPlayer,然後新增一些程式碼。

xPos = x; 
yPos = y; x += xSpeed; y += ySpeed; //animation  frame_reset();

現在讓我們跳到 step 事件。 我們可以刪除我們在之前那篇文章中新增的幾乎所有程式碼,因為大多數程式碼只是為了展示 draw_sprite_ext 的不同部分。 檢視下面的程式碼,並確保你的 step 事件看起來完全相同。

//buttons 
player_buttons(); 
 
//animation  frame_counter(); //state switch  switch currentState { case states.normal: normal_state(); break; case states.crouch: crouch_state(); break; case states.attack: attack_state(); break; }

如果你之前從未見過 switch 語句,你可能會想知道到底發生了什麼。 我稍後會解釋,但首先我們需要建立三個新指令碼:normal_state,crouch_state 和 attack_state。 我喜歡使用不同狀態的指令碼,因為它使程式碼更容易閱讀。 你可以彈出所需的任何指令碼(譯註:在 GMS2 中,在程式碼中的指令碼上按下滑鼠中鍵即可彈出對應的指令碼,並連結在當前物件視窗),並在該特定狀態下工作。

好吧,所有這一切究竟意味著什麼呢? 什麼是 switch 語句以及它是如何工作的? 將 switch 語句視為 if 語句的更具體版本。if 語句用於布林值檢查,條件滿足則執行,switch 語句用於根據變數的值執行程式碼。 看看下面的程式碼塊。

//if statement 
if(currentState == states.normal) { normal_state(); } else if(currentState == states.crouch) { crouch_state(); } //switch statement  switch currentState { case states.normal: normal_state(); break; case states.crouch: crouch_state(); break; }

這兩段程式碼在功能上都是相同的。 它們都將根據 currentState 變數的當前值執行我們想要的指令碼,但 switch 語句要清晰得多。 當我們新增狀態時,使用 if 語句變得難以管理。 switch 語句更容易管理。

最後,我們需要在 player_buttons 指令碼中新增一個新的按鈕變數。 開啟該指令碼並新增此行:

attack = keyboard_check_pressed(ord("Z"));

狀態機

我們已經定義了一個可能狀態的列舉,以及變數 currentState 來跟蹤哪個狀態是活動的。 現在我們知道了 switch 語句的工作原理,我們可以建立在每個狀態下執行的程式碼,以及在它們之間進行轉換的規則。 switch 語句可以很容易地顯示我們的狀態機是什麼以及它正在做什麼。 如果我們的 currentState 變數等於語句中的一個 case,則執行與該 case 相關的程式碼。 由於我們為每個狀態建立了指令碼,因此請繼續開啟 normal_state 指令碼並新增以下程式碼

//移動 
if(left) { xSpeed = -2; } else if(right) { xSpeed = 2; } else { xSpeed = 0; } //切換到下蹲狀態  if(down) { currentState = states.crouch; } //切換到攻擊狀態  if(attack) { currentState = states.attack; }

這段程式碼非常簡單。 對我來說,正常狀態意味著角色的預設狀態。 他們沒有執行任何特殊操作,例如攻擊或使用道具,玩家可以完全控制角色。 在這裡,我們有左右移動,並轉換到蹲和攻擊狀態。 如果你現在運行遊戲,你將無法看到我們的狀態機的全部效果。 如果你按  或 Z,你將改變狀態,不再能夠移動。 接下來讓我們定義蹲狀態。 開啟 crouch_state 指令碼並新增以下程式碼:

xSpeed = 0; if(!down) { currentState = states.normal; }

蹲下時(按住向下箭頭鍵)我們停止玩家的水平移動(xSpeed = 0)。 如果他們釋放向下鍵,我們將返回正常狀態。 這將是一個在蹲下時新增不同動作的好地方,比如爬行或者可能是蹲下的攻擊。
開啟我們建立的最後一個狀態指令碼,attack_state,並新增以下程式碼:

xSpeed = 0; if(frame > sprite_get_number(sprite) - 1) { currentState = states.normal; }

我們再次將水平速度歸零,並且當動畫結束時我們將玩家狀態設定回正常。 但是......我們還沒有設定我們的動畫,是嗎? 動畫控制是狀態機和 switch 的另一個重要用途! 建立一個新指令碼並將其命名為 animation_control。 新增以下程式碼:

xScale = approach(xScale,1,0.03); yScale = approach(yScale,1,0.03); //動畫控制  switch currentState { case states.normal: if(left) { facing = -1; } else if(right) { facing = 1; } if(left || right) { sprite = sprPlayer_Run; } else { sprite = sprPlayer_Idle; } break; case states.crouch: sprite = sprPlayer_Crouch; break; case states.attack: sprite = sprPlayer_Attack; break; } //如果精靈更改,則將幀重置為0  if (lastSprite != sprite) { lastSprite = sprite; frame = 0; }

通過使用另一個 switch 語句,我們可以輕鬆控制播放器動畫。 請注意,我們可以在 switch case 中使用 if 語句! 我們沒有將動畫控制與我們建立的初始 switch 語句組合在一起的原因有幾個。 首先,我們希望我們的動畫在所有程式碼的最後發生。 動畫是之前發生的一切的結果! 其次,它讓程式碼更好讀。 上面程式碼底部的最後一個表示式會在精靈更改時將幀重置為 0。 這可以防止在更改精靈時動畫在錯誤的幀上啟動。

請注意,我們將 xScale 和 yScale 程式碼移動到 animation_control 的頂部。 這對以後很重要。

在 oPlayer 物件中開啟 end step 事件,並將以下行新增到程式碼的底部。 這將確保它在其他一切之後發生。

animation_control();

來吧,運行遊戲。 你應該有一個能夠左右奔跑,空閒,蹲和攻擊的角色了! 我們能夠根據當前狀態區分角色的行為。 除了管理優勢之外,設定狀態機還可以更輕鬆地跟蹤錯誤,新增新行為以及跟蹤物件的整體結構。 我經常使用狀態機和 switch 語句來控制比如要顯示的選單螢幕,當前的遊戲模式以及定義給敵人的 AI 型別等內容。

謝謝閱讀! 在 Twitter 上關注我,並在我的網站上關注更多與遊戲開發相關的內容。