【构】游戏命令模式的那些事儿

来源:互联网 发布:网站域名备案查询系统 编辑:程序博客网 时间:2024/05/17 06:50

游戏中,经常会出现各种不同模块之间的调用,如玩家通过手柄Joystick的方向键控制角色Player的移动,或者角色Player调用Weapon的开火等等。面对这类调用,最简单的方法就是 被调用者 暴露出相关接口,提供给 调用者 使用。然而这种看似简单有效的方法(会写程序的都知道),在实际使用中却隐藏了很大的问题。本文针对这些问题,谈一谈游戏中关于 命令 的那些事儿。

1 我们的问题

让我们来考虑一个游戏中很常见的功能:一个手柄四个按钮,控制四个功能,四个功能分别如下图所示。
这里写图片描述

图示很明白,四个按钮分别控制玩家换枪、射击等行为。
好,需求很简单,基本不需要其他抽象。

2 第一个设计

现在我们来实现这些功能。

// 角色类,继承自Actorpublic class Player : Actor{    public void Jump(){...}    public void FireGun(){...}    public void SwapWeapon(){...}    public void ReleaseSkill(){...}}// 输入类public class InputHandler{    protected Player m_Player;    public void SetPlayer(Player player)    {        m_Player = player;    }    public void HandleInput()    {        if(Input.IsPressed(Button_X) m_Player.Jump();        else if(Input.IsPressed(Button_Y) m_Player.FireGun();        else if(Input.IsPressed(Button_A) m_Player.SwapWeapon();        else if(Input.IsPressed(Button_B) m_Player.ReleaseSkill();    }}

OK,很简单吧,上面创建了一个被调用者Player类以及一个调用者InputHandler类,通过HandleInput中调用Player的相关方法,实现我们的功能。
如果功能就这样了,并且未来需求不会发生变化(你信吗?),上面的代码未尝不是一个简单的可维护性高的良好设计(抛开具体情况谈设计架构没有任何意义)。但是,你不可能寄希望于如果,变化总是要来的。

3 命令模式

3.1 需求变化

考虑这样一种情况,玩家可以控制三个不同的角色,每次只能操作一个(类似Trine 中法师、盗贼、武士),这样四个按键,对应不同的角色,可能呈现不同的功能。例如X键对于它们都是跳,但是A键控制法师是移动物品,控制盗贼是攀爬,控制武士是防御,而不管玩家当前控制的是哪一个角色,当角色碰到宝箱时,我们希望A键可以开启宝箱,碰到机关时,A键又可以打开机关。
如果仍然按照上面的实现方式,就需要在按键判断条件中加一堆switch-case来区分当前角色以及当前角色所处的状态。这就使得这段代码后续维护起来非常麻烦。(忘了吗,良好的设计应该对修改关闭)

3.2 新的设计

OK,让我们找到变化的部分——角色行为,并把它们抽象成对象(抽象成对象后,修改就变成的增加对象,并且可以保持通用逻辑部分不变)

// 抽象Command基类,封装了抽象方法Executepublic abstract class Command{    public abstract void Execute();}// 具体的Command类,实现具体的功能public class FireCommand : Command{    public override void Execute()    {        m_Player.Fire();    }}public class OpenChestCommand : Command{    public override void Execute()    {        m_Player.OpenChest();    }}public class InputHandler{    protected Command[] m_Commands = new Command[4];    // 根据外界情况动态绑定命令    public void SetCommand(Command cmd, int idx)    {        m_Commands[idx] = cmd;    }    public void HandleInput()    {        if(Input.IsPressed(Button_X) m_Commands[0].Execute();        else if(Input.IsPressed(Button_Y) m_Commands[1].Execute();        else if(Input.IsPressed(Button_A) m_Commands[2].Execute();        else if(Input.IsPressed(Button_B) m_Commands[3].Execute();    }}

3.3 命令模式定义

新的设计中,通过增加一层Command类,解耦了InputHandler和Player两个模块,当两个模块之间的调用关系发生变化时,只需要动态改变中间层Command。于是,一个基本的命令模式完成了。GoF对命令模式的定义如下。

把请求封装成对象,从而可以使用户可以将不同的请求进行参数化,进而实现命令队列、返回上一步操作等等

3.4 更进一步

上面需求的变化虽然具有一定说服力,但是变化的规模和影响都较小,只能略微看到命令模式的些许好处,前菜上完,下面要上主菜了。你会发现在下面的情况中,如果不使用命令模式,整个架构将会一团糟。
新的需求。
1. 正常情况下,角色由玩家控制,但是玩家可以点击托管按钮,让角色由内置AI控制。
2. 异步PVP中,对方玩家由AI控制。
3. 同步PVP时,希望玩家的操作能够上传到服务器,然后再由服务器下发到每一个客户端,从而同步各个客户端,让客户端中的显示结果相同。
4. 实现录像回放或类似Braid里的时间倒退功能。
针对上面的需求,我们继续优化之前的设计。

// 抽象Command基类,封装了抽象方法Executepublic abstract class Command{    public abstract void Execute(Actor actor);}// 具体的Command类,实现具体的功能public class FireCommand : Command{    public override void Execute(Actor actor)    {        actor.Fire();    }}// 盗贼开宝箱public class OpenChestCommand : Command{    public override void Execute(Actor actor)    {        Thief thief = actor as Thief;        if(thief != null)            thief.OpenChest();    }}// 创建Command的接口public interface ICreateCommand(){    void Update(float dt);    Command CreateCommand();}// 摇杆实现创建命令接口public class InputHandler : ICreateCommand{    protected Command[] m_Commands = new Command[4];    // 根据外界情况动态绑定命令    public void SetCommand(Command cmd, int idx)    {        m_Commands[idx] = cmd;    }    public Command CreateCommand()    {        if(Input.IsPressed(Button_X) return m_Commands[0];        else if(Input.IsPressed(Button_Y) return m_Commands[1];        else if(Input.IsPressed(Button_A) return m_Commands[2];        else if(Input.IsPressed(Button_B) return m_Commands[3];        return null;    }}// AI发布命令public class AI : ICreateCommand{}// 服务器代理发布命令public class ServerCmdAgent : ICreateCommand{}public class Actor{    protected List<Command> m_CmdList = new List<Command>();    // 当前可以向Actor发送命令的模块    protected ICreateCommand m_CmdCreator;    // 动态设置发送命令的模块,手柄、AI或者server下发下来的。    public void SetCommandCreator(ICreateCommand cmdCreator)    {        m_CmdCreator = cmdCreator;    }    public void Update(float dt)    {        m_CmdCreator.Update(dt);        Command cmd = m_CmdCreator.CreateCommand()        if(cmd != null)         {            // 加入命令队列,此处保存命令以便实现Undo,Redo功能            m_CmdList.Enqueue(cmd);            // 此处判断是否是否是网络命令,如果是,则发往服务器,这部分也可以直接封装到Command中            if(isLocal)            {                cmd.Execute(this);            }            else            {                GlobalServer.SendCmdToServer(cmd);            }        }    }}

对于需求1、2,我们只需把Actor中m_CmdCreator切换成AI即可,AI在更新过程中,发出一系列命令(AI可以用上一篇里的状态机实现),简单的AI命令简单,复杂的AI命令复杂。对于需求3,不直接执行命令,而是将命令传到服务器端,同时需要把m_CmdCreator换成ServerCmdAgent,让其获得服务器的命令,代理执行。对于需求4,上面并没有实现,但是我们已经将整个命令序列保存下来,重新执行一遍或者倒着执行(需要命令中提供反向执行的操作,如MoveCmd中记录移动前的位置,而Fire这种行为命令如果想进行Undo操作,还需要将发出去的子弹运动倒回来,甚至是动画系统的倒放,这么想想Braid真是逆天的佳作)。
整体类图如下所示。
这里写图片描述
这里只针对具体需求设计出来一个适用的命令模式,并没有进一步抽象Actor。事实上,Actor还可以统一抽象为一类命令执行者,对于本游戏,此处抽象没有必要,我们认为可接受命令的只有怪物、角色之类。而对于Braid,命令的执行者基本上是游戏中任何可移动的物体,这时,就需要再抽象Actor的层次。

4 问题扩展

如果一个命令,需要多个执行者依次执行,那么可以构造出一种职责链。执行者甚至可以在执行过程中修改命令参数,传给其下的执行者。职责链可以解耦不同的执行者,即当前执行者只需要知道下一个执行者,而不需要知道命令传递过程的整个流程。
当然,一个复杂的命令,也可以通过几个简单的命令组合而成,这样就把命令拆解成一个一个的原子命令。
由于命令列表的存在,发出的命令可以不立即执行,比如创建Monster这个命令,可能非常耗时(没有缓存时,需要从磁盘读取Monster的资源,IO耗时较长,申请内存也会消耗时间),如果一帧内同时创建10个Monster,就会出现非常严重的卡顿,这时,可以将10个创建命令分到不同帧去运行,让游戏运行更加流畅。

5 参考文献

  1. 关于之前的《游戏状态机的那些事儿》,参见http://blog.csdn.net/zhou8jie/article/details/47786917
  2. 一个较好的游戏设计模式网站:http://gameprogrammingpatterns.com/command.html#classy-and-dysfunctional
  3. 职责链相关内容wiki:https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern
0 0
原创粉丝点击