美文网首页
在Unity实现游戏命令模式

在Unity实现游戏命令模式

作者: Uonfan | 来源:发表于2020-01-17 14:51 被阅读0次

    本文介绍如何在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)等游戏是如何实现回放功能的?其中一种方法是执行和玩家完全相同的输入操作,这样意味着输入需要保存起来。命令模式可以实现该功能,以及更多其它功能。

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

    image

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

    • 命令模式的基础知识。

    • 实现命令模式的方法。

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

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

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

    准备过程

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

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

    image

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

    image

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


    004.png

    了解Bot对象的逻辑

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

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

    image

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

    image

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

    007.png

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

    image

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

    命令设计模式介绍

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

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

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

    image

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

    010.png

    “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键。最后,按下回车键执行命令。

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

    image

    现在观察代码添加到终端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

    image

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

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

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

    相关文章

      网友评论

          本文标题:在Unity实现游戏命令模式

          本文链接:https://www.haomeiwen.com/subject/vapczctx.html