譯文|GAMEMAKER STUDIO 系列:簡單狀態機
按照設計,狀態機一次只能處於一種狀態。 由我們來定義對我們的情況有意義的狀態,以及它們之間的關係。 在本文中,我們將使用狀態機來控制在任何給定時間可用的玩家操作,允許我們設定角色並定義角色可以執行的操作。
大家好, 今天我想告訴你如何設定一個簡單的狀態機。 狀態機是一種資料結構,顧名思義,它跟蹤不同的狀態。 例如,我們的遊戲可能有三種狀態:“遊戲執行”,“遊戲暫停”和“遊戲結束”。我們可能會使用狀態機來記住哪一個處於活動狀態,並定義如何從一個狀態轉換到另一個狀態(請參閱 上面的圖片)。
基礎設定
此條目需要比上一個篇文章(繪製精靈)多一些動畫,因此在開始之前,你需要將這些動畫新增到工程中。 從此(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 上關注我,並在我的網站上關注更多與遊戲開發相關的內容。