在Unity實現遊戲命令模式
你是否想知道《超級食肉男孩》(Super Meat Boy)等遊戲是如何實現回放功能的?其中一種方法是完全按照玩家發出的命令執行輸入,這意味著輸入需要以某種方式儲存。
命令模式可用於執行此操作和其他操作。如果你希望在策略遊戲裡實現撤銷和重做功能,命令模式也非常實用。
在本教程中,我們將使用C#實現命令模式,然後使用命令模式來遍歷3D迷宮中的機器人角色。
我們會學習到以下內容:
- 命令模式的基礎知識。
- 實現命令模式的方法。
- 對輸入命令進行排隊,並推遲執行。
- 在執行前,撤銷和重做已發出的命令。
本教程使用Unity 2019.1和C# 7,學習本文你需要熟悉Unity的使用,並且對C#有一定的瞭解。
學習準備
本教程將為你提供專案檔案和素材,請傳送[命令模式]到微信後臺,獲取下載地址。
下載完成專案素材後,請解壓檔案,並在Unity中開啟Starter專案。然後開啟RW/Scenes資料夾,開啟主場景。
如下圖所示,場景中有一個迷宮和機器人,左側有一個顯示指令的終端UI。地面的是一個網格,當玩家在迷宮中移動機器人時,這些網格將有助於玩家進行觀察。
場景中最有趣的部分是Bot物件,它代表遊戲中的機器人,我們在層級視窗單擊選中該物件。
在檢視視窗檢視該物件,可以看見它帶有Bot元件,我們將在發出輸入命令時使用該元件。
理解Bot的邏輯
我們開啟RW/Scripts資料夾,在程式碼編輯器開啟Bot指令碼。我們不必瞭解Bot指令碼的作用,但要了解其中的Move方法和Shoot方法的使用。
我們發現,Move方法會接收一個型別為CardinalDirection的輸入引數。CardinalDirection是一個列舉,型別為CardinalDirection的列舉物件可以為Up,Down,Right或Left。
根據所選的CardinalDirection不同,機器人會在網格上朝著對應方向移動一個網格。
Shoot方法可以讓機器人發射炮彈,摧毀黃色的牆體,但對其它牆體毫無作用。
現在檢視ResetToLastCheckpoint方法,我們對迷宮進行觀察。在迷宮中,有一些點被稱為檢查點。為了通過迷宮,機器人應該到達綠色檢查點。
在機器人穿過新檢查點時,該點會成為機器人的最後檢查點。ResetToLastCheckpoint方法會重置機器人的位置到最後檢查點。
什麼是命令設計模式
命令模式是《設計模式:可複用物件導向軟體的基礎》(Design Patterns: Elements of Reusable Object-Oriented Software)一書中介紹的23種設計模式之一。
書中寫道:命令模式把請求封裝為物件,從而允許我們使用不同的請求,佇列或日誌請求,來引數化處理其它物件,並支援可撤銷的操作。
這麼表達或許難以理解,下面我們詳細講解一下。
封裝:方法呼叫封裝為物件的過程。
引數化其它物件:封裝的方法可以根據輸入引數來處理多個物件。
請求的佇列:得到的“命令”可以在執行前和其它命令一起儲存。
命令佇列
“Undoable”(可撤銷)在此不是指無法實現的東西,而是指可以通過撤銷功能恢復的操作。那麼這些內容怎麼用程式碼表示呢?
簡單來說,Command類會有Execute方法,該方法可以接收一個名為Receiver的物件作為輸入引數。因此,Execute方法會由Command類進行封裝。
Command類的多個例項可以作為常規物件來傳遞,這表示它們可以儲存在資料結構中,例如:佇列,棧等。
為了執行命令,Execute方法需要進行呼叫。觸發執行過程的類叫作Invoker。
我們的專案中已包含一個名叫BotCommand的空類。下面我們將完成要求,讓Bot物件可以使用命令模式執行動作。
移動機器人Bot物件
實現命令模式
首先,開啟RW/Scripts資料夾,在編輯器開啟BotCommand指令碼,並加入下面的程式碼。
- //1
- private readonly string commandName;
- //2
- public BotCommand(ExecuteCallback executeMethod, string name)
- {
- Execute = executeMethod;
- commandName = name;
程式碼解讀:
- commandName變數用於儲存使用者可以理解的命令名稱。
- BotCommand建構函式會接收一個函式和一個字串,它幫助我們設定Command物件的Execute方法和名稱。
- ExecuteCallback委託會定義封裝方法的型別。封裝方法會返回void型別,接收型別為Bot物件作為輸入引數。
- Execute屬性會引用封裝方法,我們要使用它來呼叫封裝方法。
- ToString方法會被重寫,返回commandName字串,該方法主要在UI中使用。
儲存改動,現在我們已經實現了命令模式。
建立命令
我們從RW/Scripts資料夾中開啟BotInputHandler指令碼。
我們將建立BotCommand的5個例項,這些例項會分別封裝方法,從而讓Bot物件向上、下、左、右移動,以及讓機器人發射炮彈。
新增下列程式碼到BotCommand類中。
- //1
- private static readonly BotCommand MoveUp =
- new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Up); }, "moveUp");
- //2
- private static readonly BotCommand MoveDown =
- new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Down); }, "moveDown");
- //3
- private static readonly BotCommand MoveLeft =
- new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Left); }, "moveLeft");
- //4
- private static readonly BotCommand MoveRight =
- new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Right); }, "moveRight");
- //5
- private static readonly BotCommand Shoot =
- 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中。
- public static BotCommand HandleInput()
- {
- if (Input.GetKeyDown(KeyCode.W))
- {
- return MoveUp;
- }
- else if (Input.GetKeyDown(KeyCode.S))
- {
- return MoveDown;
- }
- else if (Input.GetKeyDown(KeyCode.D))
- {
- return MoveRight;
- }
- else if (Input.GetKeyDown(KeyCode.A))
- {
- return MoveLeft;
- }
- else if (Input.GetKeyDown(KeyCode.F))
- {
- return Shoot;
- }
- return null;
- }
HandleInput方法會根據使用者的按鍵,返回單個命令例項。繼續下一步前,儲存改動內容。
使用命令
現在我們要使用建立好的命令。開啟RW/Scripts資料夾,在程式碼編輯器開啟SceneManager指令碼。在該類中,我們會發現有UIManager型別的uiManager變數的引用。
UIManager類為場景中的終端UI提供了實用的功能性方法。此外,Bot變數引用了附加到Bot物件的Bot元件。
我們將下面的程式碼新增給SceneManager類,替換程式碼註釋//1的已有程式碼。
- //1
- private List<BotCommand> botCommands = new List<BotCommand>();
- private Coroutine executeRoutine;
- //2
- private void Update()
- {
- if (Input.GetKeyDown(KeyCode.Return))
- {
- ExecuteCommands();
- }
- else
- {
- CheckForBotCommands();
- }
- }
- //3
- private void CheckForBotCommands()
- {
- var botCommand = BotInputHandler.HandleInput();
- if (botCommand != null && executeRoutine == null)
- {
- AddToCommands(botCommand);
- }
- }
- //4
- private void AddToCommands(BotCommand botCommand)
- {
- botCommands.Add(botCommand);
- //5
- uiManager.InsertNewText(botCommand.ToString());
- }
- //6
- private void ExecuteCommands()
- {
- if (executeRoutine != null)
- {
- return;
- }
- executeRoutine = StartCoroutine(ExecuteCommandsRoutine());
- }
- private IEnumerator ExecuteCommandsRoutine()
- {
- Debug.Log("Executing...");
- //7
- uiManager.ResetScrollToTop();
- //8
- for (int i = 0, count = botCommands.Count; i < count; i++)
- {
- var command = botCommands[i];
- command.Execute(bot);
- //9
- uiManager.RemoveFirstTextLine();
- yield return new WaitForSeconds(CommandPauseTime);
- }
- //10
- botCommands.Clear();
- bot.ResetToLastCheckpoint();
- executeRoutine = null;
- }
儲存程式碼,通過使用這些程式碼,我們可以在遊戲檢視正常執行專案。
執行遊戲並測試命令模式
現在要構建所有內容,在Unity編輯器按下Play按鈕。
我們可以使用W,A,S,D按鍵輸入方向命令。輸入射擊模式時,使用F鍵。最後按下Enter鍵執行命令。
現在觀察程式碼新增到終端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宣告後新增以下變數宣告。
- private Stack <BotCommand> undoStack = new Stack <BotCommand>();
undoStack變數屬於來自Collections名稱空間的Stack類,它會儲存撤銷的命令引用。
現在,我們要分別為撤銷和重做新增UndoCommandEntry和RedoCommandEntry兩個方法。在SceneManager類中,新增下面程式碼到ExecuteCommandsRoutine後。
- private void UndoCommandEntry()
- {
- //1
- if (executeRoutine != null || botCommands.Count == 0)
- {
- return;
- }
- undoStack.Push(botCommands[botCommands.Count - 1]);
- botCommands.RemoveAt(botCommands.Count - 1);
- //2
- uiManager.RemoveLastTextLine();
- }
- private void RedoCommandEntry()
- {
- //3`
- if (undoStack.Count == 0)
- {
- return;
- }
- var botCommand = undoStack.Pop();
- AddToCommands(botCommand);
- }
解讀這部分程式碼:
- 如果命令正在執行,或botCommands列表是空的,UndoCommandEntry方法不執行任何操作。否則,它會把最後輸入的命令引用推送到undoStack上。這部分程式碼也會從botCommands列表移除命令引用。
- UIManager類的RemoveLastTextLine方法會移除終端UI的最後一行文字,這樣在發生撤銷時,終端UI內容符合botCommands的內容。
- 如果undoStack為空,RedoCommandEntry不執行任何操作。否則,它會把最後的命令從undoStack移出,然後通過AddToCommands把命令新增到botCommands列表。
現在我們新增鍵盤輸入來使用這些方法。在SceneManager類中,把Update方法的主體替換為下列程式碼。
- if (Input.GetKeyDown(KeyCode.Return))
- {
- ExecuteCommands();
- }
- else if (Input.GetKeyDown(KeyCode.U)) //1
- {
- UndoCommandEntry();
- }
- else if (Input.GetKeyDown(KeyCode.R)) //2
- {
- RedoCommandEntry();
- }
- else
- {
- CheckForBotCommands();
- }
現在按下U鍵會呼叫UndoCommandEntry方法,按下R鍵會呼叫RedoCommandEntry方法。
處理邊緣情況
現在我們快要完成該教程了,在完成前,我們要確定二件事:
- 輸入新命令時,undoStack應該被清空。
- 執行命令前,undoStack應該被清空。
首先,我們給SceneManager新增一個新方法。新增下面的方法到CheckForBotCommands之後。
- private void AddNewCommand(BotCommand botCommand)
- {
- undoStack.Clear();
- AddToCommands(botCommand);
- }
該方法會清空undoStack,然後呼叫AddToCommands方法。
現在把CheckForBotCommands內的AddToCommands呼叫替換為下列程式碼:
- AddNewCommand(botCommand);
最後,複製貼上下列程式碼到ExecuteCommands方法內的if語句中,從而在執行前清空undoStack。
- undoStack.Clear();
現在專案終於完成了,我們儲存並構建專案。在Unity編輯器單擊Play按鈕。輸入命令,按下U鍵撤銷命令,按下R鍵恢復被撤銷的命令。
下圖展示了讓機器人到達綠色檢查點。
學習資源
希望瞭解更多遊戲程式設計中的設計模式,請訪問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
相關文章
- 在 Unity 多人遊戲中實現語音對話Unity遊戲
- 在Unity中實現手部跟蹤Unity
- 在Unity中為即時戰略遊戲實現戰爭迷霧(上)Unity遊戲
- 在Unity中為即時戰略遊戲實現戰爭迷霧(下)Unity遊戲
- 命令模式(c++實現)模式C++
- 設計模式——命令模式實現撤銷設計模式
- Unity射擊遊戲例項—物理碰撞的實現Unity遊戲
- 在Unity中實現2D光照系統Unity
- Go語言實現設計模式之命令模式Go設計模式
- 設計模式學習筆記(十五)命令模式及在Spring JdbcTemplate 中的實現設計模式筆記SpringJDBC
- Unity遊戲開發技巧集錦2.1.3實現效果Unity遊戲開發
- Unity3D遊戲實戰Unity3D遊戲
- 用Flash、HTML5和Unity開發網頁遊戲的現實HTMLUnity網頁遊戲
- 現在的中國遊戲圈,能有多現實?遊戲
- 在Python中實現單例模式Python單例模式
- 設計模式實戰 - 命令模式設計模式
- [unity3d]如何實現遊戲物件跟隨滑鼠方向移動Unity3D遊戲物件
- Unity 華為快遊戲JS橋接 實現寫日誌等功能Unity遊戲JS橋接
- 用Unity實現彈反效果Unity
- Unity——計時器功能實現Unity
- unity實現場景跳轉Unity
- Unity——觀察者模式Unity模式
- 在Unity中實現一個簡單的訊息管理器Unity
- 在linux上用dd命令實現ghost功能Linux
- 【Unity 3D遊戲開發】在Unity使用NoSQL資料庫方法介紹Unity3D遊戲開發SQL資料庫
- Unity實現簡單的物件池Unity物件
- 【unity】 Loom實現多執行緒UnityOOM執行緒
- Unity中實現人形角色的攀爬Unity
- 設計模式在Python中的完美實現設計模式Python
- Java實現在訪問者模式中使用反射Java模式反射
- MVVM模式解析和在WPF中的實現(三)命令繫結MVVM模式
- unity遊戲開發雜項系列:unity在商店裡下載的package儲存位置Unity遊戲開發Package
- Unity 利用Cache實現邊下邊玩Unity
- Unity使用TextMeshPro實現聊天圖文混排Unity
- Unity TA總監王靖:Unity如何實現美術畫質升級?Unity
- MVC模式在Java Web應用程式中的實現MVC模式JavaWeb
- Unity和騰訊遊戲成立聯合創新實驗室,從技術創新探索遊戲產品新模式和概念Unity遊戲模式
- 使用C# (.NET Core) 實現命令設計模式 (Command Pattern)C#設計模式