在Unity中实现命令设计模式

图片

您是否曾经想过在Super Meat Boy之类的游戏中如何实现重播功能? 实现它的方法之一是以与播放器相同的方式执行输入,这又意味着需要以某种方式存储输入。 您可以为此使用Command模式等等。

命令模板对于在策略游戏中创建撤消和重做功能也很有用。

在本教程中,我们在C#中实现Command模板,并使用它来指导机器人角色穿越三维迷宫。 通过本教程,您将学到:

  • 命令模式的基础。
  • 如何实现命令模式
  • 如何创建输入命令队列并延迟其执行。

注意 :假设您已经熟悉Unity并具有C#的一般知识。 在本教程中,我们将使用Unity 2019.1C#7

开始工作


首先,下载项目资料 。 解压缩文件并在Unity中打开Starter项目。

转到RW /场景并打开场景。 该场景由一个机器人和一个迷宫以及一个显示指令的终端UI组成。 关卡设计以网格形式进行,当我们在视觉上将机器人穿过迷宫时,这很有用。


如果您点击Play ,我们将看到说明不起作用。 这是正常现象,因为我们会将此功能添加到教程中。

场景中最有趣的部分是GameObject Bot 。 通过单击它在“层次结构”窗口中将其选中。


在检查器中,您可以看到它具有Bot组件。 我们将通过发出输入命令来使用此组件。


我们了解机器人的逻辑


转到RW /脚本,然后在代码编辑器中打开Bot脚本。 您无需知道Bot脚本中正在发生的事情。 但是,请看一下两种方法: MoveShoot 。 同样,您不需要了解这些方法中发生的事情,但是您需要了解如何使用它们。

注意, Move方法接收输入参数CardinalDirectionCardinalDirection是一个枚举。 CardinalDirection类型的枚举元素可以是UpDownRightLeft 。 根据所选的CardinalDirection机器人会沿着网格在相应方向上精确移动一个正方形。


Shoot方法迫使机器人发射弹壳,摧毁黄色的墙壁 ,但对其他墙壁无用。


最后,看一下ResetToLastCheckpoint方法; 要了解他在做什么,请看迷宫。 迷宫中有一些点称为检查点 。 为了通过迷宫,机器人需要到达绿色控制点。


当机器人踩到新的控制点时,它就成了他的最后选择。 ResetToLastCheckpoint重置机器人的位置,将其移动到最后一个控制点。


尽管我们无法使用这些方法,但我们会尽快对其进行修复。 首先,您需要了解Command设计模式。

什么是命令设计模式?


命令模式是Erich Gamma,Richard Helm,Ralph Johnson和John Vlissides( GoF ,四人帮)的四人帮编写的《 设计模式:可重用的面向对象软件的元素》一书中描述的23种设计模式之一。

作者报告说:“命令模式将请求封装为一个对象,从而使我们能够对具有不同请求,队列或日志请求的其他对象进行参数化,并支持可逆操作。”

哇! 怎么了

我知道这个定义不是很简单,所以让我们对其进行分析。

封装意味着可以将方法调用封装为对象。


封装的方法可能会影响许多对象,具体取决于输入参数。 这称为其他对象的参数化

可以将生成的“命令”与其他团队一起保存,直到执行完毕。 这是请求队列


团队队列

最后, 可逆性意味着可以使用撤消功能来还原操作。

好的,但这在代码中如何体现?

Command类将具有Execute方法,该方法接收称为Receiver的对象(通过其执行命令)作为输入参数。 也就是说,实际上,Execute方法是由Command类封装的。

Command类的许多实例可以作为普通对象传递,也就是说,它们可以存储在诸如队列,堆栈等数据结构中。

要执行命令,必须调用其Execute方法。 开始执行的类称为Invoker

该项目当前包含一个名为BotCommand的空类。 在下一节中,我们将实现上述实现,以允许bot使用Command模板执行操作。

移动机器人


命令模式实现


在本节中,我们实现命令模式。 有很多方法可以实现它。 在本教程中,我们将介绍其中之一。

首先,转到RW /脚本 ,然后在编辑器中打开BotCommand脚本。 BotCommand类仍然为空,但不会持续很长时间。

将以下代码插入到类中:

  //1 private readonly string commandName; //2 public BotCommand(ExecuteCallback executeMethod, string name) { Execute = executeMethod; commandName = name; } //3 public delegate void ExecuteCallback(Bot bot); //4 public ExecuteCallback Execute { get; private set; } //5 public override string ToString() { return commandName; } 

这是怎么回事

  1. commandName变量commandName用于存储人类可读的命令名称。 不需要在模板中使用它,但是在本教程的后面部分将需要它。
  2. BotCommand的构造函数接收一个函数和一个字符串。 这将帮助我们设置Command对象的Execute方法及其name
  3. ExecuteCallback委托定义封装方法的类型。 封装的方法将返回void并接受Bot类型的对象(组件Bot )作为输入参数。
  4. Execute属性将引用封装的方法。 我们将使用它来调用封装的方法。
  5. 重写ToString方法以返回字符串commandName 。 例如,这很方便在UI中使用。

保存更改,仅此而已! 我们已经成功实现了Command模式。

仍然可以使用它。

团队建设


RW /脚本文件夹中打开BotInputHandler

在这里,我们将创建BotCommand五个实例。 这些实例将封装用于上下移动GameObject Bot以及进行射击的方法。

要实现此目的,请将以下内容插入此类:

  //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。

在前四个实例中,匿名方法调用bot对象上的Move方法。 但是,它们的输入参数不同。

MoveUpMoveDownMoveLeftMoveRight传递Move参数CardinalDirection.UpCardinalDirection.DownCardinalDirection.LeftCardinalDirection.Right 。 如“ 什么是命令设计模式”部分所述,它们指示GameObject Bot移动的不同方向。

在第五个实例中,匿名方法调用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 /脚本 ,然后在编辑器中打开SceneManager脚本。 在此类中,您会注意到一个指向UIManager类型的uiManager变量的链接。

UIManager类为我们在此场景中使用的终端UI提供了有用的帮助程序方法。 如果使用UIManager的方法,则本教程将说明其功能,但通常出于我们的目的,无需了解其内部结构。

另外, bot变量是指附加到GameObject 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编辑器中单击播放

您应该能够使用WASD键输入移动命令。 要输入拍摄命令,请按F键。 要执行命令,请按Enter

注意 :在执行过程完成之前,无法输入新命令。



请注意,这些行已添加到终端UI。 UI中的团队由其名称指示。 这要归功于commandName变量。

另外,请注意,UI在执行之前如何向上滚动以及执行期间如何删除行。

我们更仔细地研究团队


是时候学习我们在“应用命令”部分中添加的代码了:

  1. botCommands列表存储指向BotCommand实例的BotCommand 。 请记住,为了节省内存,我们只能创建五个命令实例,但是可能对一个命令有多个引用。 另外, executeCoroutine变量引用ExecuteCommandsRoutine ,它控制命令的执行。
  2. Update检查用户是否已按Enter键; 如果是这样,它将调用ExecuteCommands ,否则将调用CheckForBotCommands
  3. CheckForBotCommands使用HandleInput的静态HandleInput方法检查用户是否已完成输入,如果已完成,则返回命令。 返回的命令将传递到AddToCommands 。 但是,如果执行了命令,即 如果executeRoutine不为null,则它将返回而不将任何内容传递给AddToCommands 。 即,用户需要等待直到完成。
  4. AddToCommands新链接添加到命令的返回实例。
  5. InsertNewText类的InsertNewText方法向终端UI添加一行新文本。 文本字符串是作为输入参数传递的字符串。 在这种情况下,我们将commandName传递给commandName
  6. ExecuteCommands方法启动ExecuteCommandsRoutine
  7. UIManager ResetScrollToTop向上滚动终端UI。 这是在执行开始之前完成的。
  8. ExecuteCommandsRoutine包含一个for循环,该循环遍历botCommands列表内的命令并botCommands执行,并将bot对象传递给Execute属性返回的方法。 每次执行后,将在CommandPauseTime秒内添加一个暂停。
  9. UIManagerRemoveFirstTextLine方法删除终端UI中的第一行文本(如果存在)。 也就是说,执行命令时,其名称将从UI中删除。
  10. 完成所有命令botCommands将清除botCommands并使用ResetToLastCheckpoint将机器人重置为最后一个断点。 最后, executeRoutine null ,用户可以继续输入命令。

实施撤消和重做功能


再次运行场景,然后尝试到达绿色控制点。

您会注意到,尽管我们无法取消输入的命令。 这意味着,如果您犯了一个错误,则必须先完成所有输入的命令,然后才能返回。 您可以通过添加撤消重做功能来解决此问题。

返回SceneManager.cs并在botCommandsList声明之后立即添加以下变量声明:

  private Stack<BotCommand> undoStack = new Stack<BotCommand>(); 

undoStack变量是一个堆栈 (来自Collections系列),它将存储对可以撤消的命令的所有引用。

现在,我们添加两个方法UndoCommandEntryRedoCommandEntry ,它们将执行Undo和Redo。 在SceneManager类中,在ExecuteCommandsRoutine之后SceneManager以下代码:

  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); } 

让我们分析一下代码:

  1. 如果执行了命令或botCommands列表botCommands空,则UndoCommandEntry方法UndoCommandEntry任何操作。 否则,它将写入到在undoStack堆栈上输入的最后一个命令的链接。 这还将从botCommands列表中删除该命令的链接。
  2. UIManagerRemoveLastTextLine方法从终端UI删除文本的最后一行,以使UI与botCommands的内容匹配。
  3. 如果undoStack堆栈undoStack空,则RedoCommandEntry不执行任何操作。 否则,它将从undoStack的顶部提取最后一个命令,并使用AddToCommands将其添加回botCommands列表中。

现在,我们将添加键盘输入以使用这些功能。 在SceneManager类中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(); } 

  1. 当您按U键时,将UndoCommandEntry方法。
  2. 当您按R键时,将RedoCommandEntry方法。

边缘案例处理


太好了,我们快完成了! 但是首先,我们需要执行以下操作:

  1. 输入新命令时,应清除undoStack堆栈。
  2. 在执行命令之前,必须清除undoStack堆栈。

为了实现这一点,我们首先需要向SceneManager添加一个新方法。 在CheckForBotCommands之后插入以下方法:

  private void AddNewCommand(BotCommand botCommand) { undoStack.Clear(); AddToCommands(botCommand); } 

该方法清除undoStack ,然后调用AddToCommands方法。

现在,使用以下代码替换对CheckForBotCommandsAddToCommands的调用:

  AddNewCommand(botCommand); 

然后,在执行undoStack命令之前清除ExecuteCommands方法内的if后插入以下行:

  undoStack.Clear(); 

我们终于完成了!

保存您的工作。 生成项目,然后单击“ 播放”编辑器。 像以前一样输入命令。 按U取消命令。 按R重复取消的命令。


尝试到达绿色检查站。

接下来要去哪里?


要了解有关游戏编程中使用的设计模式的更多信息,建议您学习Robert Nystrom的“ 游戏编程模式”

要了解有关高级C#技术的更多信息,请参加C#Collections,Lambdas和LINQ课程。

工作任务


作为一项任务,尝试到达迷宫尽头的绿色控制点。 我将解决方案之一隐藏在扰流板下。

解决方案
  • move×2
  • moveRight×3
  • move×2
  • moveLeft
  • 射击
  • moveLeft×2
  • move×2
  • moveLeft×2
  • 下移×5
  • moveLeft
  • 射击
  • moveLeft
  • move×3
  • 拍×2
  • 上移×5
  • moveRight×3

Source: https://habr.com/ru/post/zh-CN463953/


All Articles