在Unity实现游戏命令模式

本文由开发者Najmm Shora介绍在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. //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键。最后按下回车键执行命令。
 

 


现在观察代码添加到终端UI的方式。命令会通过它们在UI中的名称表示,该效果通过commandName变量实现。

在执行前,UI会滚动到顶部,执行后的代码行会被移除。

详解命令代码

现在我们详解在使用命令部分添加的代码。

botCommands列表存储了BotCommand实例的引用。考虑到内存,我们只可以创建5个命令实例,但有多个引用指向相同的命令。此外,executeCoroutine变量引用了ExecuteCommandsRoutine,后者会处理命令的执行过程。

如果用户按下回车键,更新检查结果,此时它会调用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.         {

  1.             return;
  2.         }
  3.         var botCommand = undoStack.Pop();
  4.         AddToCommands(botCommand);
  5.     }
复制代码


解读这部分代码:
 

  • 如果命令正在执行,或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键恢复被撤销的命令。

下图展示了让机器人到达绿色检查点。
 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值