回覆列表
  • 1 # 勝己半子

    有限狀態機:這是個啥?

    如果你有關注YoYo Games的官方技術部落格,也許你曾經見過這篇文章,這篇文章很好的解釋了什麼事狀態機,強烈對此有興趣的朋友仔細閱讀,不過我會先簡單定義一下。

    一個有限狀態機(後文以狀態機縮寫指代)是一種特殊的組織程式碼的方式,用這種方式你能確保你的物件隨時都知道自己所處的狀態以及所能做的操作。其中每一個狀態都是獨立的程式碼塊,與其他不同的狀態分開獨立執行,這麼做可以使得遊戲的除錯變得更加方便,同時也更易於增加新的功能(比如一些特殊的能力和動畫之類)。玩家角色在跳躍的時候看起來有點奇怪?那就直接去“跳躍”的狀態裡找問題吧!

    同樣這個機制也可以用於給敵人實現基本的AI邏輯,讓敵人可以根據狀態做出不同的決策。

    恰好狀態機機制是本月挑戰裡的“專家級”難度,但我十分希望當我剛開始學習程式設計時就能瞭解這個知識點,正確運用狀態機可以為你免去很多不必要的麻煩。接下來我們就來看一下如何使用吧。

    有限狀態機適合我的專案嗎?

    這個問題儼然是“世上沒有愚蠢的問題”這句話的最佳反證。狀態機系統永遠適合你的專案,這個問題可以修改成這樣“狀態機是否適合我這個物件?”確實,並不是所有的物件都需要用上狀態機機制,但你可能會驚訝地發現有那麼多物件都適合使用狀態機去進行管理。

    顯然,可控的角色和敵人都需要使用狀態機,但實際上我的遊戲控制器物件也採用了這一機制,用來區分在主選單、設定選單和關卡選擇等不同的場景的用途,甚至我的鏡頭控制器也用了狀態機,比方說“跟隨玩家狀態”,“過場動畫狀態”和“顯示特定物件狀態”等等。

    那麼如何才能確認某個物件需要使用狀態機呢?其實非常簡單,對於每個物件都要問一問自己:“這個物件可以做些什麼?”

    如果這個物件需要處理超過2件事情以上的功能,那你就應該考慮去做一個狀態機。讓我們來試著問一下這個問題,比如馬里奧,尤其是在超級馬里奧世界裡,馬里奧可以做什麼呢?

    他可以:

    站立

    行走/跑

    躲閃

    爬牆

    顯然上面這個列表還有更多沒列出來的,但這是個好的開始。因此,顯然馬里奧有很多事情可做,而且幾乎所有這些事情都是獨立的狀態,那你現在就已經有了一個物件應該要做的功能列表了,現在是時候畫一個流程圖了。

    認真臉:流程圖。

    不開玩笑,港真,流程圖是你的好夥伴。下面是幾個示例(從最上面那個連結裡借用的)

    這是上一張圖的漢化版?

    設計好流程圖並梳理好你所有需要處理的狀態是非常重要的第一步。在你正式開始編碼之前,你需要制定出基本的狀態和各自的規則。你不需要徹底搞清楚你的角色能做到每一個操作,這是狀態機最棒的特性,它總是易於擴充套件,但是基本的設計是非常重要的。

    OK,設計好了。怎麼實現呢?

    最好的辦法當然是一頭扎進去然後直接動手了,是吧?為了讓這個過程變得更輕鬆簡單,我做了一個小指令碼可供使用

    讓我們來看一下這個指令碼並瞭解一下它是怎麼工作的:

    state_machine_init()

    當你在建立需要使用狀態機的物件時,可以在Create事件中呼叫這個指令碼。它會建立一對資料結構和一系列十分有用的變數。現在我們先來看看這些變數

    state - 這個變數是當前狀態的標誌位。這個指令碼中將會包含在物件step事件中執行的程式碼。

    state_next - 當我們切換狀態時,我們希望在切換之前當前狀態能執行完畢,因此我們呼叫這個指令碼來切換狀態,同時更新這個值,然後上述"state"變數將在"End Step"事件中發生變化。

    state_name - 這個變數用於獲取儲存當前狀態的名稱(建立時設定的名稱)

    state_timer - 這個變數用於記錄當前狀態持續的“step”數(即運行了多少幀),實用程度絕對超乎想象

    state_map - 一個ds_map資料結構,把你設定的狀態名稱作為鍵名儲存進來,

    state_stack - 一個用來記錄你歷史狀態的資料結構。可以用來實現一些狀態機的進階功能,比如變回到之前的狀態。

    state_new - 這是個非常有用的變數,當你切換到某個狀態時,有可能你希望該狀態處於初始化狀態,比如速度設為0,或者更新精靈等等,這些情況十分常見。這時候你只需要在狀態的最開始將這個值設為“true”即可完成這一切操作。

    state_var[0] - state_var 這個變數比較特別。這是一個用來儲存某個狀態的持續時間的陣列。因為我發現我自己經常會有這樣的需求——“我想要清楚地跟蹤並記錄這個狀態下的一些資訊……但是別的狀態沒這種需求”那我該怎麼辦?每次有這種需求的時候都新建一個變數?這不是很荒謬嘛?所以,我用“state_var”來作為替代品,把這個陣列作為針對該狀態的一個便箋本,或是剪貼簿,取決於你的用法。這個陣列可以記錄我所需要的值,因此我不必新建變數來進行記錄,可能有點說不太清楚,但這個非常有用。

    如果你下載並仔細看了我提供的指令碼,你可能會注意懂我在裡面放了一些建議性的變數(也許你的遊戲用得著)。比如,通常情況下"state_can_interrupt"或"state_is_invincible" 這些變數都會派得上用場。

    State_create("state name",state_script)

    當狀態機引擎例項化以後,我們需要建立我們自己的狀態。比如說,我們要建立幾個馬里奧的狀態,那我們就可以像下面這樣操作

    state_create("Stand",state_mario_stand); //呼叫"state_mario_stand"指令碼處理“站立”狀態

    state_create("Walk",state_mario_walk); //呼叫"state_mario_walk"指令碼處理“行走”狀態

    state_create("Air",state_mario_air); //呼叫"state_mario_air"指令碼處理“空中”狀態

    state_create("Crouch",state_mario_crouch); //呼叫"state_mario_crouch"指令碼處理“蹲下”狀態

    一個物件可以建立任意多的狀態,儘可能根據你的需求去隨意建立即可

    state_init("State Name")

    一旦建立好所有的狀態,現在就可以來設定物件初始的預設狀態了,對於馬里奧而言,這應該是站立狀態

    state_init("Stand"); //把“站立”狀態設定為預設初始狀態

    非常簡單

    state_execute()

    這是狀態機的核心,在“step”事件中呼叫這個方法就可以呼叫你當前狀態的指令碼

    state_update()

    這個指令碼應當放在“end step”事件中呼叫,用於處理不同狀態之間的切換

    state_cleanup()

    這個指令碼呼叫最好放在“destroy”事件裡。因為之前我們在處理狀態機的過程中會建立一些資料結構,因此當物件例項被銷燬時應當徹底銷燬那些資料結構來釋放記憶體。

    重要提醒 也許你還不知道,當你切換“room”時,如果你的非持久化物件(沒有勾選“persistant”)具備“destory”事件,那在切換場景的時候這個物件會直接消失但是並不觸發“destory”事件裡的程式碼,因此,如果你的遊戲中會出現這種情況,請務必謹慎處理。

    此處強調,GMS2中有一個新的事件“clean up”,該事件可以確保在跳轉room之類的操作後仍然可以執行清除動態資源的操作,因此在GMS2中可以吧這個指令碼的呼叫放在“clean up”中,就不會出現這個提醒中所說的問題了,感謝群友提醒:)

    state_switch("State Name" or state_script)

    這個方法是用來在不同狀態間進行切換的。你可以把建立狀態時起的名字或狀態的指令碼名作為引數(推薦使用名字更直觀)。比如當馬里奧在站立狀態下,我可能會按下方向鍵來控制他下蹲:

    if(keyboard_check_pressed(vk_down))

    switch_state("Crouch");

    你也可以用相同的方法去利用左右方向鍵來控制走動狀態,或用跳躍鍵來使他執行跳躍狀態的指令碼。

    state_switch_previous()

    之前提過,這是狀態機的進階功能。在某些情況下,你可能需要物件恢復到上一階段的狀態。比如我有一個角色擁有施放咒語的狀態,並且有另一個狀態是擊中時被擊退,他有可能在任何狀態下被擊中:站立、行走、下蹲甚至丟道具等等,但當他被“擊退”後不能總是讓他恢復成站立狀態,我希望他能恢復到被擊退之前的狀態。那在這種情況下,這個指令碼就能派上用場了,這個利用了"state_stack"這一資料結構

    好吧,也許用說的還不夠清楚,下面這個圖可以簡要的說明具體的使用情況

    你可看到"create"、"step"、"end step"、"destory"等不同的事件中的程式碼,這是狀態機系統的基礎設定,這其實非常容易,其複雜程度取決於你的狀態到底有多複雜。

    動手編寫一個狀態

    讓我們從最簡單的狀態開始:“站力狀態”. 開啟文章開頭的那個工程連結並下載下來開啟,然後找到“ Scripts>Platform Boy States>pb_state_stand”。

    然後你會發現我寫了一行"if(state_new)". 讓我們來看一下這個狀態裡都做了些什麼. 我把所有的速度變數都設為0,並確保物件處於預設精靈下(馬里奧步行精靈的第一幀就是站立狀態),為了確保他確實顯示的是第一幀海拔image_index設為了0。只要我處於站立狀態,所有這些值都應當保持如此,這樣就沒必要反覆去設定了。

    在第12行,你會看到我正在檢測輸入操作,看馬里奧是不是馬上就要撞牆,我傾向於在狀態指令碼執行之前先檢測使用者的輸入操作。這不是100%必要的,但是做一次檢測然後去呼叫狀態指令碼中的內容是個很好的習慣。

    為什麼我要在12-13行檢測碰撞呢?因為如果我不檢測碰撞只單純執行控制事件,那當我們向左或向右朝著牆走過去就會走到牆裡去,我不希望馬里奧鑽進牆裡去,所以必須時刻檢測,當你試圖操作馬里奧鑽進牆壁時立刻進行切換,因此這個檢測在你切換到“行走”狀態前是必須完成的。

    你可能會注意到,在這裡我用了兩個“if”來處理“state_switch”(嗯,你注意到這點了嗎?)沒錯,設想一下如果現在我們衝著沒有牆的方向走去,同時按下了跳躍鍵……會發生什麼呢?所以,當這種情況發生時最後被下達的指令優先順序應當更高。因此在這種情況下我會執行跳躍的操作而不是向左走。你可以簡單的按照你程式碼中的順序來排列不同狀態的優先順序,25行就是一個示例。

    在這裡,我正在檢測馬里奧的腳底的地面。比如當我站在地面上時,也許因為一些原因下面的地面突然消失了,那現在他就要掉下來,而此時應當切換到“空中”狀態,迫使他掉下去,並且優先順序要高於跳躍或落下。

    當然,在同一時間任意組合所有的操作可能性也不大,但是正確梳理不同狀態之間的優先順序和關係是十分必要的,可以避免很多不必要的意外狀況。

    這就是我的站立狀態中所需要的全部功能,但想一想我們還可以在裡面加些什麼東西?比如我們可以檢測馬里奧當前是不是站在一個移動的平臺或者傳送帶上。如果是,那我會獲取這個物件的速度,並根據實際情況將這個速度設到馬里奧的x軸或y軸的速度上,以便他可以跟著平臺運動。而當我處於“空中”狀態時則完全不用操心這些問題,甚至我都用不著做相關的判斷和檢測。但是在“行走”狀態下還是需要考慮這種情況的,所以也許你也要考慮在行走狀態的指令碼中新增相應的檢測程式碼。

  • 中秋節和大豐收的關聯?
  • 從王菲演唱會天價門票事件,大家如何看待如今的王菲?