在Unity實現遊戲命令模式

遊資網發表於2019-09-19
本文由開發者Najmm Shora介紹在Unity中通過使用命令模式實現回放功能,撤銷功能和重做功能。我們可以使用該方法來強化自己的策略類遊戲。

你是否想知道《超級食肉男孩》(Super Meat Boy)等遊戲是如何實現回放功能的?其中一種方法是完全按照玩家發出的命令執行輸入,這意味著輸入需要以某種方式儲存。

命令模式可用於執行此操作和其他操作。如果你希望在策略遊戲裡實現撤銷和重做功能,命令模式也非常實用。

在Unity實現遊戲命令模式

在本教程中,我們將使用C#實現命令模式,然後使用命令模式來遍歷3D迷宮中的機器人角色。

我們會學習到以下內容:

  • 命令模式的基礎知識。
  • 實現命令模式的方法。
  • 對輸入命令進行排隊,並推遲執行。
  • 在執行前,撤銷和重做已發出的命令。


本教程使用Unity 2019.1和C# 7,學習本文你需要熟悉Unity的使用,並且對C#有一定的瞭解。

學習準備

本教程將為你提供專案檔案和素材,請傳送[命令模式]到微信後臺,獲取下載地址。

下載完成專案素材後,請解壓檔案,並在Unity中開啟Starter專案。然後開啟RW/Scenes資料夾,開啟主場景。

如下圖所示,場景中有一個迷宮和機器人,左側有一個顯示指令的終端UI。地面的是一個網格,當玩家在迷宮中移動機器人時,這些網格將有助於玩家進行觀察。

在Unity實現遊戲命令模式

場景中最有趣的部分是Bot物件,它代表遊戲中的機器人,我們在層級視窗單擊選中該物件。

在Unity實現遊戲命令模式

在檢視視窗檢視該物件,可以看見它帶有Bot元件,我們將在發出輸入命令時使用該元件。

在Unity實現遊戲命令模式

理解Bot的邏輯

我們開啟RW/Scripts資料夾,在程式碼編輯器開啟Bot指令碼。我們不必瞭解Bot指令碼的作用,但要了解其中的Move方法和Shoot方法的使用。

我們發現,Move方法會接收一個型別為CardinalDirection的輸入引數。CardinalDirection是一個列舉,型別為CardinalDirection的列舉物件可以為Up,Down,Right或Left。

根據所選的CardinalDirection不同,機器人會在網格上朝著對應方向移動一個網格。

在Unity實現遊戲命令模式

Shoot方法可以讓機器人發射炮彈,摧毀黃色的牆體,但對其它牆體毫無作用。

在Unity實現遊戲命令模式

現在檢視ResetToLastCheckpoint方法,我們對迷宮進行觀察。在迷宮中,有一些點被稱為檢查點。為了通過迷宮,機器人應該到達綠色檢查點。

在Unity實現遊戲命令模式

在機器人穿過新檢查點時,該點會成為機器人的最後檢查點。ResetToLastCheckpoint方法會重置機器人的位置到最後檢查點。

在Unity實現遊戲命令模式

什麼是命令設計模式

命令模式是《設計模式:可複用物件導向軟體的基礎》(Design Patterns: Elements of Reusable Object-Oriented Software)一書中介紹的23種設計模式之一。

書中寫道:命令模式把請求封裝為物件,從而允許我們使用不同的請求,佇列或日誌請求,來引數化處理其它物件,並支援可撤銷的操作。

這麼表達或許難以理解,下面我們詳細講解一下。

封裝:方法呼叫封裝為物件的過程。

在Unity實現遊戲命令模式

引數化其它物件:封裝的方法可以根據輸入引數來處理多個物件。

請求的佇列:得到的“命令”可以在執行前和其它命令一起儲存。

在Unity實現遊戲命令模式
命令佇列

“Undoable”(可撤銷)在此不是指無法實現的東西,而是指可以通過撤銷功能恢復的操作。那麼這些內容怎麼用程式碼表示呢?

簡單來說,Command類會有Execute方法,該方法可以接收一個名為Receiver的物件作為輸入引數。因此,Execute方法會由Command類進行封裝。

Command類的多個例項可以作為常規物件來傳遞,這表示它們可以儲存在資料結構中,例如:佇列,棧等。

為了執行命令,Execute方法需要進行呼叫。觸發執行過程的類叫作Invoker。

我們的專案中已包含一個名叫BotCommand的空類。下面我們將完成要求,讓Bot物件可以使用命令模式執行動作。


移動機器人Bot物件


實現命令模式

首先,開啟RW/Scripts資料夾,在編輯器開啟BotCommand指令碼,並加入下面的程式碼。
  1. //1
  2.     private readonly string commandName;

  3.     //2

  4.     public BotCommand(ExecuteCallback executeMethod, string name)

  5.     {

  6.         Execute = executeMethod;

  7.         commandName = name;
複製程式碼

程式碼解讀:

  • commandName變數用於儲存使用者可以理解的命令名稱。
  • BotCommand建構函式會接收一個函式和一個字串,它幫助我們設定Command物件的Execute方法和名稱。
  • ExecuteCallback委託會定義封裝方法的型別。封裝方法會返回void型別,接收型別為Bot物件作為輸入引數。
  • Execute屬性會引用封裝方法,我們要使用它來呼叫封裝方法。
  • ToString方法會被重寫,返回commandName字串,該方法主要在UI中使用。


儲存改動,現在我們已經實現了命令模式。

建立命令

我們從RW/Scripts資料夾中開啟BotInputHandler指令碼。

我們將建立BotCommand的5個例項,這些例項會分別封裝方法,從而讓Bot物件向上、下、左、右移動,以及讓機器人發射炮彈。

新增下列程式碼到BotCommand類中。
  1.    //1

  2.     private static readonly BotCommand MoveUp =

  3.         new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Up); }, "moveUp");


  4.     //2

  5.     private static readonly BotCommand MoveDown =

  6.         new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Down); }, "moveDown");

  7.     //3

  8.     private static readonly BotCommand MoveLeft =

  9.         new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Left); }, "moveLeft");

  10.     //4

  11.     private static readonly BotCommand MoveRight =

  12.         new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Right); }, "moveRight");

  13.     //5

  14.     private static readonly BotCommand Shoot =

  15.         new BotCommand(delegate (Bot bot) { bot.Shoot(); }, "shoot");
複製程式碼

在每個例項中,都有一個匿名方法傳到建構函式。該匿名方法會封裝在相應命令物件之中,每個匿名方法的簽名都符合ExecuteCallback委託設定的要求。

此外,建構函式的第二個引數是一個字串,表示用於指代命令的名稱。該名稱會通過命令例項的ToString方法返回,它會在後面為UI使用。

在前4個例項中,匿名方法會在Bot物件上呼叫Move方法。

對於MoveUp、MoveDown、MoveLeft和MoveRight命令,傳入Move方法的引數分別是CardinalDirection.Up,CardinalDirection.Down,CardinalDirection.Left和CardinalDirection.Right,這些引數對應著Bot物件的不同移動方向。

在第5個例項上,匿名方法在Bot物件呼叫Shoot方法。這將在執行該命令時,讓機器人發射炮彈。

現在我們建立了命令,這些命令需要在使用者發出輸入時進行訪問。請將下面的程式碼新增到BotInputHandler中。

  1. public static BotCommand HandleInput()

  2.     {

  3.         if (Input.GetKeyDown(KeyCode.W))

  4.         {

  5.             return MoveUp;

  6.         }

  7.         else if (Input.GetKeyDown(KeyCode.S))

  8.         {

  9.             return MoveDown;

  10.         }

  11.         else if (Input.GetKeyDown(KeyCode.D))

  12.         {

  13.             return MoveRight;

  14.         }

  15.         else if (Input.GetKeyDown(KeyCode.A))

  16.         {

  17.             return MoveLeft;

  18.         }

  19.         else if (Input.GetKeyDown(KeyCode.F))

  20.         {

  21.             return Shoot;

  22.         }



  23.         return null;

  24.     }

複製程式碼

HandleInput方法會根據使用者的按鍵,返回單個命令例項。繼續下一步前,儲存改動內容。

使用命令

現在我們要使用建立好的命令。開啟RW/Scripts資料夾,在程式碼編輯器開啟SceneManager指令碼。在該類中,我們會發現有UIManager型別的uiManager變數的引用。

UIManager類為場景中的終端UI提供了實用的功能性方法。此外,Bot變數引用了附加到Bot物件的Bot元件。

我們將下面的程式碼新增給SceneManager類,替換程式碼註釋//1的已有程式碼。

  1. //1

  2.     private List<BotCommand> botCommands = new List<BotCommand>();

  3.     private Coroutine executeRoutine;


  4.     //2

  5.     private void Update()

  6.     {

  7.         if (Input.GetKeyDown(KeyCode.Return))

  8.         {

  9.             ExecuteCommands();

  10.         }

  11.         else

  12.         {

  13.             CheckForBotCommands();

  14.         }         

  15.     }


  16.     //3

  17.     private void CheckForBotCommands()

  18.     {

  19.         var botCommand = BotInputHandler.HandleInput();

  20.         if (botCommand != null && executeRoutine == null)

  21.         {

  22.             AddToCommands(botCommand);

  23.         }

  24.     }


  25.     //4

  26.     private void AddToCommands(BotCommand botCommand)

  27.     {

  28.         botCommands.Add(botCommand);

  29.         //5

  30.         uiManager.InsertNewText(botCommand.ToString());

  31.     }


  32.     //6

  33.     private void ExecuteCommands()

  34.     {

  35.         if (executeRoutine != null)

  36.         {

  37.             return;

  38.         }


  39.         executeRoutine = StartCoroutine(ExecuteCommandsRoutine());

  40.     }


  41.     private IEnumerator ExecuteCommandsRoutine()

  42.     {

  43.         Debug.Log("Executing...");

  44.         //7

  45.         uiManager.ResetScrollToTop();


  46.         //8

  47.         for (int i = 0, count = botCommands.Count; i < count; i++)

  48.         {

  49.             var command = botCommands[i];

  50.             command.Execute(bot);

  51.             //9

  52.             uiManager.RemoveFirstTextLine();

  53.             yield return new WaitForSeconds(CommandPauseTime);

  54.         }


  55.         //10

  56.         botCommands.Clear();


  57.         bot.ResetToLastCheckpoint();


  58.         executeRoutine = null;

  59.     }

複製程式碼

儲存程式碼,通過使用這些程式碼,我們可以在遊戲檢視正常執行專案。

執行遊戲並測試命令模式
現在要構建所有內容,在Unity編輯器按下Play按鈕。

我們可以使用W,A,S,D按鍵輸入方向命令。輸入射擊模式時,使用F鍵。最後按下Enter鍵執行命令。

在Unity實現遊戲命令模式

現在觀察程式碼新增到終端UI的方式。命令會通過它們在UI中的名稱表示,該效果通過commandName變數實現。

在執行前,UI會滾動到頂部,執行後的程式碼行會被移除。

詳解命令程式碼

現在我們詳解在使用命令部分新增的程式碼。

botCommands列表儲存了BotCommand例項的引用。考慮到記憶體,我們只可以建立5個命令例項,但有多個引用指向相同的命令。此外,executeCoroutine變數引用了ExecuteCommandsRoutine,後者會處理命令的執行過程。

如果使用者按下Enter鍵,更新檢查結果,此時它會呼叫ExecuteCommands,否則會呼叫CheckForBotCommands。

CheckForBotCommands使用來自BotInputHandler的HandleInput靜態方法,檢查使用者是否發出輸入資訊,此時會返回命令。返回的命令會傳遞到AddToCommands。然而,如果命令被執行的話,即如果executeRoutine不是空的話,它會直接返回,不把任何內容傳遞給AddToCommands。因此,使用者必須等待執行過程完成。

AddToCommands給返回的命令例項新增了新引用,返回到botCommands。

UIManager類的InsertNewText方法會給終端UI新增新一行文字。該行文字是作為輸入引數傳給方法的字串。我們會在此給它傳入commandName。

ExecuteCommands方法會啟動ExecuteCommandsRoutine。

UIManager類的ResetScrollToTop會向上滾動終端UI,它會在執行過程開始前完成。

ExecuteCommandsRoutine有一個for迴圈,它會迭代botCommands列表中的命令,通過把Bot物件傳給Execute屬性返回的方法,逐個執行這些命令。在每次執行後,我們會新增CommandPauseTimeseconds時長的暫停。

UIManager類的RemoveFirstTextLine方法會移除終端UI裡的第一行文字,只要那裡仍有文字。因此,每個命令執行後,它的相應名稱會從終端UI移除。

執行所有命令後,botCommands會清空,機器人會使用ResetToLastCheckpoint,重置到最後檢查點。接著,executeRoutine會設為null,使用者可以繼續發出更多輸入資訊。


實現撤銷和重做功能

我們再執行一次場景,嘗試到達綠色檢查點。現在無法撤銷輸入的命令,這意味著如果犯了錯,我們無法後退,除非執行完所有命令。

我們可以通過新增撤銷功能和重做功能來解決該問題。返回SceneManager.cs指令碼,在botCommands的List宣告後新增以下變數宣告。

  1. private Stack <BotCommand> undoStack = new Stack <BotCommand>();
複製程式碼

undoStack變數屬於來自Collections名稱空間的Stack類,它會儲存撤銷的命令引用。

現在,我們要分別為撤銷和重做新增UndoCommandEntry和RedoCommandEntry兩個方法。在SceneManager類中,新增下面程式碼到ExecuteCommandsRoutine後。

  1. private void UndoCommandEntry()

  2.     {

  3.         //1

  4.         if (executeRoutine != null || botCommands.Count == 0)

  5.         {

  6.             return;

  7.         }


  8.         undoStack.Push(botCommands[botCommands.Count - 1]);

  9.         botCommands.RemoveAt(botCommands.Count - 1);


  10.         //2

  11.         uiManager.RemoveLastTextLine();

  12.      }


  13.     private void RedoCommandEntry()

  14.     {

  15.         //3`

  16.         if (undoStack.Count == 0)

  17.         {

  18.             return;

  19.         }


  20.         var botCommand = undoStack.Pop();

  21.         AddToCommands(botCommand);

  22.     }

複製程式碼

解讀這部分程式碼:

  • 如果命令正在執行,或botCommands列表是空的,UndoCommandEntry方法不執行任何操作。否則,它會把最後輸入的命令引用推送到undoStack上。這部分程式碼也會從botCommands列表移除命令引用。
  • UIManager類的RemoveLastTextLine方法會移除終端UI的最後一行文字,這樣在發生撤銷時,終端UI內容符合botCommands的內容。
  • 如果undoStack為空,RedoCommandEntry不執行任何操作。否則,它會把最後的命令從undoStack移出,然後通過AddToCommands把命令新增到botCommands列表。


現在我們新增鍵盤輸入來使用這些方法。在SceneManager類中,把Update方法的主體替換為下列程式碼。

  1. if (Input.GetKeyDown(KeyCode.Return))

  2.     {

  3.         ExecuteCommands();

  4.     }

  5.     else if (Input.GetKeyDown(KeyCode.U)) //1

  6.     {

  7.         UndoCommandEntry();

  8.     }

  9.     else if (Input.GetKeyDown(KeyCode.R)) //2

  10.     {

  11.         RedoCommandEntry();

  12.     }

  13.     else

  14.     {

  15.         CheckForBotCommands();

  16.     }
複製程式碼

現在按下U鍵會呼叫UndoCommandEntry方法,按下R鍵會呼叫RedoCommandEntry方法。

處理邊緣情況

現在我們快要完成該教程了,在完成前,我們要確定二件事:

  • 輸入新命令時,undoStack應該被清空。
  • 執行命令前,undoStack應該被清空。


首先,我們給SceneManager新增一個新方法。新增下面的方法到CheckForBotCommands之後。

  1. private void AddNewCommand(BotCommand botCommand)

  2.     {

  3.         undoStack.Clear();

  4.         AddToCommands(botCommand);

  5.     }
複製程式碼

該方法會清空undoStack,然後呼叫AddToCommands方法。

現在把CheckForBotCommands內的AddToCommands呼叫替換為下列程式碼:
  1. AddNewCommand(botCommand);
複製程式碼

最後,複製貼上下列程式碼到ExecuteCommands方法內的if語句中,從而在執行前清空undoStack。

  1. undoStack.Clear();
複製程式碼

現在專案終於完成了,我們儲存並構建專案。在Unity編輯器單擊Play按鈕。輸入命令,按下U鍵撤銷命令,按下R鍵恢復被撤銷的命令。

下圖展示了讓機器人到達綠色檢查點。

在Unity實現遊戲命令模式

學習資源

希望瞭解更多遊戲程式設計中的設計模式,請訪問Robert Nystrom的遊戲程式設計模式網站:
http://gameprogrammingpatterns.com/

瞭解更多高階C#方法,請訪問《C# Collections, Lambdas, and LINQ》課程:
https://www.raywenderlich.com/604358-c-collections-lambdas-and-linq

小結


在Unity中通過使用命令模式實現回放功能,撤銷功能和重做功能為大家介紹到這裡。

作者:Najmm Shora  
來源:Unity官方平臺
原地址:https://mp.weixin.qq.com/s/3dbta9vSvY-nERUUH5IDyg

相關文章