媒体号
首页
原创
论坛
产品库
开测表
招聘
首页
原创
论坛
产品库
开测表
招聘
More
GameRes游资网在以下平台有媒体号,欢迎关注收藏:
微信公众号
微博
头条号
百家号
网易号
知乎号
开
放
分
享
游戏梦的起点!
投稿邮箱:news@gameres.com
产品邮箱:cp@gameres.com
厂商·专访
产品
观察·投资
专访
电子竞技
区块链
直播
政策
元宇宙
人工智能
手游
虚拟·VR·AR
页游·H5
端游·PC
主机
云游戏
拆解分析
策划
程序·引擎
美术
音乐
测试
数据·报告
运营·渠道
职场·创业
Steam
App Store
海外
八卦
活动
综合
硬件·周边
扫一扫,手机打开
在Unity实现游戏命令模式
作者:Najmm Shora
Unity官方平台
2019-09-19
本文由开发者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
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键。最后按下回车键执行命令。
现在观察代码添加到终端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声明后添加以下变量声明。
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
最新评论
暂无评论
参与评论
推广
开测游戏
查看更多
荒野国度
魔法
上线
研发 : 儒意景秀
发行 : 儒意景秀
辉煌岁月
合成
删档计费测试
研发 : 广州金科文化科技有限公司
发行 : 广州金科文化科技有限公司
诛仙2
角色扮演
限量删档测试
研发 : 完美世界
三千幻世
像素
删档计费测试
研发 : 中手游
发行 : 中手游
唱舞星计划
舞蹈
计费删档测试
研发 : 上海龙游网络科技有限公司
发行 : 上海晋展网络科技有限公司
解限机-PC
近期热门预约
风暴测试
研发 : 西山居
商务合作
查看更多
斗道世界
点点传说
维斯塔的餐桌
星烬 烛耀山海
编辑推荐
查看更多
Kingdom Come: Deliverance II
问剑长生
燕云十六声
纪念碑谷3