(在命令模式的介绍和学习中,我们将会接触到C#语言中的事件委托知识。所以在进行此模式的学习的时候,需要学习C#中的事件委托。如果有C/C++基础的朋友,最好也要学习过指针、函数指针)
(而在我们的命令模式的学习中,我将使用C#和Unity作为学习的语言和工具,在将来的学习中我会抽时间推出C/C++和Win32作为工具的教程)
命令模式的定义:将一个请求(request)封装成一个对象,从而允许我们使用不同的请求、队列或日志将客户端参数化,同时支持请求操作的撤销与恢复。
我将上述的定义进行一个精炼的概括:命令就是一个对象化(实例化)的方法调用!命令就是面向对象化的回调。
接下来,我将举例说明命令模式的使用场景。
2.1 配置输入
基本上所有的游戏,都会有一处代码用来读取玩家的原始输入,比如:按钮点击、键盘事件、鼠标点击等等。不管我们点击何种按键,它都会记录并转换为游戏中的一个动作。
举例:我们按下“W”和“S”的时候便会改变物体的Y轴,"W"按钮使它增加1的高度,“S”的时候便会使它减少1的高度。
public class InputHandler { public void HandleInput() { if (Input.GetKeyDown(KeyCode.X)) { //玩家角色Y轴增加 } else if (Input.GetKeyDown(KeyCode.D)) { //玩家角色Y轴减少 } } }
在游戏的主循环中,我们则会去调用HandleInput()这个方法。而将InputHandler这个脚本绑在用户控制的角色下面就可以控制该角色了。相信大家一定能理解此段代码。不过,大多数的游戏允许玩家进行按钮与游戏之间的映射关系,所以为了支持自定义配置,我们需要使用到“命令模式”。
此时,我们需要一个基类,用来表示可触发的游戏命令:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Command { public virtual void Execute() { Debug.Log("Command.Execute"); } } //W public class JumpCommond : Command { public override void Execute() { //玩家角色向上移动 } } //S public class FireCommond : Command { public override void Execute() { //玩家角色向下移动 } }
然后我们再定义一个处理输入的类:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class InputHandler { JumpCommond buttonW = new JumpCommond(); FireCommond buttonS = new FireCommond(); public void HandleInput() { Debug.Log("HandleInput"); if (Input.GetKeyDown(KeyCode.W)) { buttonW.Execute(); } else if (Input.GetKeyDown(KeyCode.S)) { buttonS.Execute(); } } }
最后只需要调用“HandleInput”的方法就可以了。
对比两种代码实现的方式,其实它们所要达到的目的都是一样的,只是对于第二种方法,我增加了一个间接调用层。这种方式或者模式,就叫做命令模式。也许我们还看不到它的优点,接下来我将会讲解第三种方式(会用到事件委托)。
2.2 角色的说明
刚刚我们所写的脚本是有效的,但是它们都有局限性。
不管是“让角色的位置向Y轴增加”还是“让角色的位置向Y轴减少”等等命令,这些函数能够隐式地获知玩家游戏实体并对其进行操控。
这种“耦合性”的假设限制了这些命令的使用范围。例如:上述的角色命令只能作用于玩家的对象,“让角色的位置向Y轴增加”、“让角色的位置向Y轴减少”。假如我们放宽限制,传进去一个我们想要控制的对象而不是让命令自身来确定控制的对象:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Command { public virtual void Execute(GameObject actor) { Debug.Log("Command.Execute"); } } //W public class JumpCommond : Command { public override void Execute(GameObject actor) { actor.transform.position = actor.transform.position + Vector3.up; } } //S public class FireCommond : Command { public override void Execute(GameObject actor) { actor.transform.position = actor.transform.position - Vector3.up; } }
现在我们可以使用这个类让角色中的任何角色来回跳动。但是,在输入处理和接受命令并针对指定对象进行调用的命令之间,我们还缺少了一些东西。我们还需要修改"InputHandler"脚本:
using System.Collections; using System.Collections.Generic; using UnityEngine; public delegate void commandDelegate(GameObject actor); public class InputHandler { JumpCommond buttonX = new JumpCommond(); FireCommond buttonD = new FireCommond(); public event commandDelegate commandEvent; public void HandleInput(GameObject actor) { Debug.Log("HandleInput"); if (Input.GetKeyDown(KeyCode.W)) { commandEvent += buttonX.Execute; commandEvent(actor); commandEvent -= buttonX.Execute; } else if (Input.GetKeyDown(KeyCode.S)) { commandEvent += buttonD.Execute; commandEvent(actor); commandEvent -= buttonD.Execute; } } }
假设actor是玩家控制的角色,那么上面的代码将会基于用户的输入来驱动角色。而在命令和角色之间加入的间接层使得我们可以让玩家控制游戏中的任何角色,只需通过改变命令执行时传入的角色对象即可。所以我们补全游戏主循环的脚本:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameRun : MonoBehaviour { public InputHandler inputHandler; public GameObject gameActor1; public GameObject gameActor2; public GameObject gameActor; void Start () { inputHandler = new InputHandler(); gameActor1 = GameObject.Find("Actor1"); gameActor2 = GameObject.Find("Actor2"); } // Update is called once per frame void Update () { Debug.Log("Update"); if (Input.GetKeyDown(KeyCode.Alpha1)) { gameActor = gameActor1; } else if (Input.GetKeyDown(KeyCode.Alpha2)) { gameActor = gameActor2; } if (gameActor != null) { inputHandler.HandleInput(gameActor); } } }
此脚本执行的情况是,当玩家按下数字“1”时,选择控制角色1(gameActor1),再按下“W”或"S"键时则会控制角色1的Y轴的位置。当玩家按下数字“2”时,选择控制角色2(gameActor2),再按下“W”或"S"键时则会控制角色2的Y轴的位置。
总结:
目前为止,我们都是在考虑玩家如何去驱动角色,但是对于游戏中世界中的其他角色呢?它们由游戏的AI来驱动。我们则可以按照上述模式来作为AI引擎和角色之间的接口。
其次,如果我们把这些命令序列化,我们便可以通过网络发送数据流。我们可以吧玩家的输入,通过网络发送到另外一台机器上,然后进行回放。
后记:在写完这篇日志后,我发现,其实并不需要用到时间委托,代码如下:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Command { public virtual void Execute(GameObject actor) { Debug.Log("Command.Execute"); } } //W public class JumpCommond : Command { public override void Execute(GameObject actor) { actor.transform.position = actor.transform.position + Vector3.up; } } //S public class FireCommond : Command { public override void Execute(GameObject actor) { actor.transform.position = actor.transform.position - Vector3.up; } }
using System.Collections; using System.Collections.Generic; using UnityEngine; public class InputHandler { JumpCommond buttonX = new JumpCommond(); FireCommond buttonD = new FireCommond(); public Command HandleInput(GameObject actor) { Debug.Log("HandleInput"); if (Input.GetKeyDown(KeyCode.W)) { return buttonX; } else if (Input.GetKeyDown(KeyCode.S)) { return buttonD; } return null; } }
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameRun : MonoBehaviour { public InputHandler inputHandler; public GameObject gameActor1; public GameObject gameActor2; public GameObject gameActor; Command command = new Command(); void Start () { inputHandler = new InputHandler(); gameActor1 = GameObject.Find("Actor1"); gameActor2 = GameObject.Find("Actor2"); } // Update is called once per frame void Update () { Debug.Log("Update"); if (Input.GetKeyDown(KeyCode.Alpha1)) { gameActor = gameActor1; } else if (Input.GetKeyDown(KeyCode.Alpha2)) { gameActor = gameActor2; } if (gameActor != null) { command = inputHandler.HandleInput(gameActor); if (command != null) { command.Execute(gameActor); } } } }
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:#游戏编程模式# — 探究设计模式之命令模式 - Python技术站