V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
UNITY: Game Development Tool
License Comparisons
unn
V2EX  ›  UNITY

在 Unity 实现游戏命令模式

  •  
  •   unn · 2020-01-17 14:20:45 +08:00 · 1144 次点击
    这是一个创建于 1570 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文介绍如何在 Unity 通过使用命令模式实现回放功能,撤销功能和重做功能,你可以使用该方法来强化自己的策略类游戏。

    原博地址:https://www.raywenderlich.com/3067863-implementing-the-command-pattern-in-unity

    原文链接:https://connect.unity.com/p/zai-unityshi-xian-you-xi-ming-ling-mo-shi?app=true

    作者:Najmm Shora 预计阅读时间:20 分钟

    Unity 版本:Unity 2019.1

    你是否想知道《超级食肉男孩》(Super Meat Boy)等游戏是如何实现回放功能的?其中一种方法是执行和玩家完全相同的输入操作,这样意味着输入需要保存起来。命令模式可以实现该功能,以及更多其它功能。

    如果希望在策略游戏里实现撤销和重做功能,命令模式也非常实用。

    在本教程中,我们会使用 C#实现命令模式,然后使用命令模式来遍历 3D 迷宫中的机器人( bot, 文中 bot,机器人交替出现,请整理一下)角色。在这个过程中,我们会学习到以下内容:

    • 命令模式的基础知识。

    • 实现命令模式的方法。

    • 对输入命令进行排队,推迟这些命令的执行。

    • 在执行前,撤销和重做发出的命令。

    备注:阅读本文需要熟悉 Unity 的使用,并且拥有对 C#有一定的了解。本教程使用 Unity 2019.1 和 C# 7。

    准备过程

    跟随本教程进行学习时,请下载文末链接的项目素材文件。解压文件,在 Unity 中打开 Starter 项目。

    打开 RW/Scenes 文件夹,打开 Main 场景。我们会注意到,场景中有一个迷宫和机器人,旁边有终端 UI 显示指令。地面上有网格,当玩家在迷宫中移动机器人时,这些网格有助于玩家进行观察。

    单击 Play 按钮后,我们发现指令不会进行工作,这是因为我们还没添加该功能,我们将在教程中添加功能。 场景中最有趣的部分是 Bot 游戏对象,在层级窗口单击选中该对象。

    在检视窗口查看该对象,我们看到它带有 Bot 组件。我们会在发出输入命令时使用该组件。

    了解 Bot 对象的逻辑

    打开 RW/Scripts 文件夹,在代码编辑器打开 Bot 脚本。我们不必了解 Bot 脚本会做什么,但要注意其中的 Move 方法和 Shoot 方法。我们也不用知道二个方法中的代码作用,但需要了解如何使用二个方法。

    我们发现,Move 方法会接收一个类型为 CardinalDirection 的输入参数。CardinalDirection 是一个枚举,类型为 CardinalDirection 的枚举对象可以为 Up,Down,Right 或 Left。根据所选的 CardinalDirection 不同,机器人会在网格上朝着对应方向移动一个网格。

    Shoot 方法可以让机器人发射炮弹,摧毁黄色的墙体,但对其它墙体毫无作用。

    现在查看 ResetToLastCheckpoint 方法,为了了解它的功能,我们要观察迷宫。在迷宫中,有一些点被称为检查点。为了通过迷宫,机器人应该到达绿色检查点。

    在机器人穿过新检查点时,该点会成为机器人的最后检查点。ResetToLastCheckpoint 方法会重置机器人的位置到最后检查点。

    我们目前无法使用这些方法,但我们很快就会用到了。首先,我们要介绍命令设计模式。

    命令设计模式介绍

    命令模式是 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 编写的《设计模式:可复用面向对象软件的基础》( Design Patterns: Elements of Reusable Object-Oriented Software )

    书中介绍的 23 种设计模式之一。 书中写道:命令模式把请求封装为对象,从而允许我们使用不同的请求,队列或日志请求,来参数化处理其它对象,并支持可撤销的操作。

    这个定义确实对读者来说并不友好,我们下面详细讲解一下。 封装是指方法调用封装为对象的过程。

    参数化其它对象指的是:封装的方法可以根据输入参数来处理多个对象。 请求的队列指的是:得到的“命令”可以在执行前和其它命令一起存储。

    “undoable”(可撤销)在此不是指无法实现的东西,而是指可以通过撤销功能恢复的操作。

    那么这些内容怎么用代码表示呢?

    简单来说,Command 类会有 Execute 方法,该方法可以接收命令处理的对象,该对象叫 Receiver,用作输入参数。因此,Execute 方法会由 Command 类进行封装。

    最后,为了执行命令,Execute 方法需要进行调用。触发执行过程的类叫作 Invoker。

    现在,该项目包含名叫 BotCommand 的空类。在下个部分,我们会完成要求,实现之前的功能,让 Bot 对象可以使用命令模式执行动作。

    移动 Bot 对象

    实现命令模式

    在这部分,我们会实现命令模式。实现该模式有多种方法。本教程会介绍其中一种方法。

    首先打开 RW/Scripts 文件夹,在编辑器打开 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 变量用于存储用户可以理解的命令名称。它对于该模式并不重要,但是我们会在后面需要到它。

    2. BotCommand 构造函数会接收一个函数和一个字符串。它会帮助我们设置 Command 对象的 Execute 方法和名称。

    3. ExecuteCallback 委托会定义封装方法的类型。封装方法会返回 void 类型,接收类型为 Bot (即带有 Bot 组件)的对象作为输入参数。

    4. Execute 属性会引用封装方法。我们要使用它来调用封装方法。

    5. ToString 方法会被重写,返回 commandName 字符串,该方法主要在 UI 中使用。

    保存改动,现在我们已经实现了命令模式。

    接下来要使用命令模式。

    创建命令

    从 RW/Scripts 文件夹打开 BotInputHandler 脚本。

    我们会在此创建 BotCommand 的五个实例。这些实例会分别封装方法,从而让 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 使用。

    在前四个实例中,匿名方法会在 Bot 对象上调用 Move 方法。该方法有多种参数。

    对于 MoveUp、MoveDown、MoveLeft 和 MoveRight 命令,传入 Move 方法的参数分别是 CardinalDirection.Up ,CardinalDirection.Down,CardinalDirection.Left 和 CardinalDirection.Right。

    这些参数对应着 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/Scripts 文件夹,在代码编辑器打开 SceneManager 脚本。在该类中,我们会发现有 UIManager 类型的 uiManager 变量的引用。

    UIManager 类为场景中的终端 UI 提供了实用的功能性方法。在 UIManager 类的方法使用时,我们会介绍方法的用途,但在本文中,我们不必知道它内部的工作方式。

    此外,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 按钮。

    我们可以使用 WASD 按键输入方向命令。输入射击模式时,使用 F 键。最后,按下回车键执行命令。

    备注:在执行过程结束前,我们无法输入更多命令。

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

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

    详细讲解命令

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

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

    2. 如果用户按下回车键,更新检查结果,此时它会调用 ExecuteCommands,否则会调用 CheckForBotCommands。

    3. CheckForBotCommands 使用来自 BotInputHandler 的 HandleInput 静态方法,检查用户是否发出输入信息,此时会返回命令。返回的命令会传递到 AddToCommands。然而,如果命令被执行的话,即如果 executeRoutine 不是空的话,它会直接返回,不把任何内容传递给 AddToCommands。因此,用户必须等待执行过程完成。

    4. AddToCommands 给返回的命令实例添加了新引用,返回到 botCommands。

    5. UIManager 类的 InsertNewText 方法会给终端 UI 添加新一行文字。该行文字是作为输入参数传给方法的字符串。我们会在此给它传入 commandName。

    6. ExecuteCommands 方法会启动 ExecuteCommandsRoutine。

    7. UIManager 类的 ResetScrollToTop 会向上滚动终端 UI。它会在执行过程开始前完成。

    8. ExecuteCommandsRoutine 拥有 for 循环,它会迭代 botCommands 列表内的命令,通过把 Bot 对象传给 Execute 属性返回的方法,逐个执行这些命令。在每次执行后,我们会添加 CommandPauseTimeseconds 时长的暂停。

    9. UIManager 类的 RemoveFirstTextLine 方法会移除终端 UI 里的第一行文字,只要那里仍有文字。因此,每个命令执行后,它的相应名称会从终端 UI 移除。

    10. 执行所有命令后,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);
    }
    

    现在讲解这部分代码:

    1. 如果命令正在执行,或 botCommands 列表是空的,UndoCommandEntry 方法不执行任何操作。否则,它会把最后输入的命令引用推送到 undoStack 上。这部分代码也会从 botCommands 列表移除命令引用。

    2. UIManager 类的 RemoveLastTextLine 方法会移除终端 UI 的最后一行文字,这样在发生撤销时,终端 UI 内容符合 botCommands 的内容。

    3. 如果 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();
    }
    
    1. 按下 U 键会调用 UndoCommandEntry 方法。

    2. 按下 R 键会调用 RedoCommandEntry 方法。

    处理边缘情况

    现在我们快要完成该教程了,在完成前,我们要确定二件事:

    1. 输入新命令时,undoStack 应该被清空。

    2. 执行命令前,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 的游戏编程模式网站。

    如果想了解更多高级 C#方法,可以查看C# Collections,Lambdas,and LINQ课程。

    挑战

    小挑战:尝试达到迷宫终点的绿色检查点。如果遇到困难,我在下面提供了解决方法,这是多个解决方法之一。

    解决方法:

    • moveUp × 2

    • moveRight × 3

    • moveUp × 2

    • moveLeft

    • shoot

    • moveLeft × 2

    • moveUp × 2

    • moveLeft × 2

    • moveDown × 5

    • moveLeft

    • shoot

    • moveLeft

    • moveUp × 3

    • shoot × 2

    • moveUp × 5

    • moveRight × 3

    本文到此结束,感谢阅读。希望你喜欢这篇教程,如果有问题或评论,请在评论区讨论。 特别感谢艺术家 Lee Barkovich、Jesús Lastra 和 sunburn 提供本项目的资源。

    原文链接: https://connect.unity.com/p/zai-unityshi-xian-you-xi-ming-ling-mo-shi?app=true

    欢迎大家戳上方链接,下载 Unity 官方 app,技术社区互动答疑,干货资源学不停!

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2764 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 14:47 · PVG 22:47 · LAX 07:47 · JFK 10:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.