您是否曾经想过在
Super Meat Boy之类的游戏中如何实现重播功能? 实现它的方法之一是以与播放器相同的方式执行输入,这又意味着需要以某种方式存储输入。 您可以为此使用
Command模式等等。
命令模板对于在策略游戏中创建撤消和重做功能也很有用。
在本教程中,我们在C#中实现Command模板,并使用它来指导机器人角色穿越三维迷宫。 通过本教程,您将学到:
- 命令模式的基础。
- 如何实现命令模式
- 如何创建输入命令队列并延迟其执行。
注意 :假设您已经熟悉Unity并具有C#的一般知识。 在本教程中,我们将使用Unity 2019.1和C#7 。
开始工作
首先,下载
项目资料 。 解压缩文件并在Unity中打开
Starter项目。
转到
RW /场景并打开
主场景。 该场景由一个机器人和一个迷宫以及一个显示指令的终端UI组成。 关卡设计以网格形式进行,当我们在视觉上将机器人穿过迷宫时,这很有用。
如果您点击
Play ,我们将看到说明不起作用。 这是正常现象,因为我们会将此功能添加到教程中。
场景中最有趣的部分是GameObject
Bot 。 通过单击它在“层次结构”窗口中将其选中。
在检查器中,您可以看到它具有
Bot组件。 我们将通过发出输入命令来使用此组件。
我们了解机器人的逻辑
转到
RW /脚本,然后在代码编辑器中打开
Bot脚本。 您无需知道
Bot脚本中正在发生的事情。 但是,请看一下两种方法:
Move
和
Shoot
。 同样,您不需要了解这些方法中发生的事情,但是您需要了解如何使用它们。
注意,
Move
方法接收输入参数
CardinalDirection
。
CardinalDirection
是一个枚举。
CardinalDirection
类型的枚举元素可以是
Up
,
Down
,
Right
或
Left
。 根据所选的
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
类仍然为空,但不会持续很长时间。
将以下代码插入到类中:
这是怎么回事
commandName
变量commandName
用于存储人类可读的命令名称。 不需要在模板中使用它,但是在本教程的后面部分将需要它。BotCommand
的构造函数接收一个函数和一个字符串。 这将帮助我们设置Command对象的Execute
方法及其name
。ExecuteCallback
委托定义封装方法的类型。 封装的方法将返回void并接受Bot
类型的对象(组件Bot )作为输入参数。Execute
属性将引用封装的方法。 我们将使用它来调用封装的方法。- 重写
ToString
方法以返回字符串commandName
。 例如,这很方便在UI中使用。
保存更改,仅此而已! 我们已经成功实现了Command模式。
仍然可以使用它。
团队建设
在
RW /脚本文件夹中打开
BotInputHandler 。
在这里,我们将创建
BotCommand
五个实例。 这些实例将封装用于上下移动GameObject Bot以及进行射击的方法。
要实现此目的,请将以下内容插入此类:
在每种情况下,
匿名方法都传递给构造函数。 此匿名方法将封装在相应的命令对象中。 如您所见,每个匿名方法的签名都符合
ExecuteCallback
委托指定的要求。
此外,构造函数的第二个参数是指示命令名称的字符串。 该名称将由命令实例的
ToString
方法返回。 稍后,我们将其应用于UI。
在前四个实例中,匿名方法调用
bot
对象上的
Move
方法。 但是,它们的输入参数不同。
MoveUp
,
MoveDown
,
MoveLeft
和
MoveRight
传递
Move
参数
CardinalDirection.Up
,
CardinalDirection.Down
,
CardinalDirection.Left
和
CardinalDirection.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
:
哇,多少代码! 但是不用担心; 我们终于可以在游戏窗口中首次真正启动该项目了。
我将在稍后解释代码。 记住要保存更改。
运行游戏以测试命令模板
因此,现在是构建的时候了; 在Unity编辑器中单击
播放 。
您应该能够使用
WASD键输入移动命令。 要输入拍摄命令,请按
F键。 要执行命令,请按
Enter 。
注意 :在执行过程完成之前,无法输入新命令。
请注意,这些行已添加到终端UI。 UI中的团队由其名称指示。 这要归功于
commandName
变量。
另外,请注意,UI在执行之前如何向上滚动以及执行期间如何删除行。
我们更仔细地研究团队
是时候学习我们在“应用命令”部分中添加的代码了:
botCommands
列表存储指向BotCommand
实例的BotCommand
。 请记住,为了节省内存,我们只能创建五个命令实例,但是可能对一个命令有多个引用。 另外, executeCoroutine
变量引用ExecuteCommandsRoutine
,它控制命令的执行。Update
检查用户是否已按Enter键; 如果是这样,它将调用ExecuteCommands
,否则将调用CheckForBotCommands
。CheckForBotCommands
使用HandleInput
的静态HandleInput
方法检查用户是否已完成输入,如果已完成,则返回命令。 返回的命令将传递到AddToCommands
。 但是,如果执行了命令,即 如果executeRoutine
不为null,则它将返回而不将任何内容传递给AddToCommands
。 即,用户需要等待直到完成。AddToCommands
新链接添加到命令的返回实例。InsertNewText
类的InsertNewText
方法向终端UI添加一行新文本。 文本字符串是作为输入参数传递的字符串。 在这种情况下,我们将commandName传递给commandName
。ExecuteCommands
方法启动ExecuteCommandsRoutine
。UIManager
ResetScrollToTop
向上滚动终端UI。 这是在执行开始之前完成的。ExecuteCommandsRoutine
包含一个for
循环,该循环遍历botCommands
列表内的命令并botCommands
执行,并将bot
对象传递给Execute
属性返回的方法。 每次执行后,将在CommandPauseTime
秒内添加一个暂停。UIManager
的RemoveFirstTextLine
方法删除终端UI中的第一行文本(如果存在)。 也就是说,执行命令时,其名称将从UI中删除。- 完成所有命令
botCommands
将清除botCommands
并使用ResetToLastCheckpoint
将机器人重置为最后一个断点。 最后, executeRoutine
null
,用户可以继续输入命令。
实施撤消和重做功能
再次运行场景,然后尝试到达绿色控制点。
您会注意到,尽管我们无法取消输入的命令。 这意味着,如果您犯了一个错误,则必须先完成所有输入的命令,然后才能返回。 您可以通过添加
撤消和
重做功能来解决此问题。
返回
SceneManager.cs并在
botCommands
的
List声明之后立即添加以下变量声明:
private Stack<BotCommand> undoStack = new Stack<BotCommand>();
undoStack
变量是一个
堆栈 (来自Collections系列),它将存储对可以撤消的命令的所有引用。
现在,我们添加两个方法
UndoCommandEntry
和
RedoCommandEntry
,它们将执行Undo和Redo。 在
SceneManager
类中,在
ExecuteCommandsRoutine
之后
SceneManager
以下代码:
private void UndoCommandEntry() {
让我们分析一下代码:
- 如果执行了命令或
botCommands
列表botCommands
空,则UndoCommandEntry
方法UndoCommandEntry
任何操作。 否则,它将写入到在undoStack
堆栈上输入的最后一个命令的链接。 这还将从botCommands
列表中删除该命令的链接。 UIManager
的RemoveLastTextLine
方法从终端UI删除文本的最后一行,以使UI与botCommands
的内容匹配。- 如果
undoStack
堆栈undoStack
空,则RedoCommandEntry
不执行任何操作。 否则,它将从undoStack
的顶部提取最后一个命令,并使用AddToCommands
将其添加回botCommands
列表中。
现在,我们将添加键盘输入以使用这些功能。 在
SceneManager
类中
SceneManager
用以下代码替换
Update
方法的主体:
if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else if (Input.GetKeyDown(KeyCode.U))
- 当您按U键时,将
UndoCommandEntry
方法。 - 当您按R键时,将
RedoCommandEntry
方法。
边缘案例处理
太好了,我们快完成了! 但是首先,我们需要执行以下操作:
- 输入新命令时,应清除
undoStack
堆栈。 - 在执行命令之前,必须清除
undoStack
堆栈。
为了实现这一点,我们首先需要向
SceneManager
添加一个新方法。 在
CheckForBotCommands
之后插入以下方法:
private void AddNewCommand(BotCommand botCommand) { undoStack.Clear(); AddToCommands(botCommand); }
该方法清除
undoStack
,然后调用
AddToCommands
方法。
现在,使用以下代码替换对
CheckForBotCommands
对
AddToCommands
的调用:
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