服务端游戏编程之设计模式(上)

最近利用碎片的时间读到了《游戏编程模式》(作者:Robert Nystrom)一书,感叹于作者对游戏编程模式这块的理解之深,总结出来的内容深入浅出,样例也是从经典的场景中抽象出来,恰到好处。

虽然整本书的内容都是从游戏客户端的角度出发,同时并不影响其他端人员的阅读,所以说我写这篇文章的目的也十分简单:想从游戏服务端的角度出发,参照原书的章节,总结一下我对于游戏编程设计模式理解。

文章未对各种设计模式的经典实现进行讲解,因为这类资料太多,还望见谅。

关于游戏服务端设计模式的个人理解概述:

  • 相比较GOF书里提到的,能完全契合使用的设计模式比较少,一些需要变化后使用;
  • 在满足需求的前提下,使用设计模式的目的是为了实现高内聚、低耦合,增加可维护性;
  • 合适、合理、优雅的使用;切勿强行滥用、炫技(为了解耦而解,导致代码已经支离破碎,不利于阅读维护)!

命令模式

游戏中通常会有一个充满UI交互界面,通过点击UI上面的控件发送命令字与服务端交互,参考下面这个拙劣的比喻图:

命令模式拙劣的比喻图

每种交互基本与一个服务端的命令字对应(xxxx Command)。原始的实现大致如下:

class Command
{
public:
    virtual ~Command() {}
    //命令字的核心执行,纯虚函数需要在子类实现
    virtual void execute() = 0;
    //命令字的操作撤销,纯虚函数需要在子类实现
    virtual void undo() = 0;
};

class MoveUnitCommand : public Command
{
    //MoveUnit命令的实现
};

作为服务端,撤销操作这种需求显然是不适合在命令字处理侧单独实现的;其次,为了每个命令字单独实现一个类,成本有点高。当然并不是说这种方式不好,只是说我们核心需求是“execute”这个执行函数,完全可以简化一下:

class Command
{
typedef int32_t (*cmdFunc)(PlayerData *, const string &, string &);

public:
    int32_t regCmd()
    {
        //在cmdFuncMap注册命令字,可以定义成虚函数,继承时重写
    }

    int32_t procCmd(int32_t cmdId, PlayerData *p, const string &req, string &rsp)
    {
        map<int32_t, cmdFunc>::iterator iter = cmdFuncMap.find(cmdId);
        if (iter == cmdFuncMap.end()) {
            return -1;
        }
        return iter->second(p, req, rsp);
    }

private:
    map<int32_t, cmdFunc> cmdFuncMap;
};

把原本需要实现的类退化成函数,在满足需求的同时简化了整体结构。当然你也可以自己封装类似于class PlayerCommand和class FightCommand继承类,在内部添加一些特定处理。

 

享元模式

享元模式的存在是为了解决有些含有共性数据的问题,将内部状态(the intrinsic state)与外部状态(the extrinsic state)区分开来。其中内部状态是一些共性的数据,外部数据是每个实例特有的数据。

就服务端来说我们不关心图形的绘制以及场景的生成步骤,但是需要关心场景由什么组成,场景长宽高(3D场景)以及场景中存在哪些的实例。这些实例可能会达到数以千计,以怪物实例来说,不推荐把实例实现成这样(注意这是不推荐的例子):

class MonsterModel
{
    //怪物内部状态
};

class MonsterA
{
public:
    MonsterModel *model;
    //A怪物的外部状态
};

或许直接写成这样,直接依靠ID来区分怪物会更加直观

class Monster
{
    int64_t uid;//全局唯一id
    int32_t id; //怪物的类型id
    //...其他属性
};

虽然可能会说这样增加耦合度:A拥有的属性B不一定拥有,会造成内存与效率上的“浪费”。

但是对于服务端来说这样聚合度更高,更加方便于局部的管理,这也是一个寻求平衡的过程。

 

观察者模式

如果你是一个有经验的游戏服务端从业人员,并且游戏的主逻辑是放在服务端来算的话,那么肯定会对“统计类”系统(例如成就系统)的“插入式”代码苦恼不已。

观察者模式契合了这类需求:变化->通知改变->接收处理。

但是对于游戏这类需求的几宗罪:

  • 强行耦合原有的系统代码;
  • 需求加入的点比较分散,甚至会出现一需多点的情况;

个人觉得这个模式在游戏服务端来说不是必须的,有三个原因:

  • 观察者模式并不能解决上面的问题,但是可以利用这个模式降低耦合度;
  • 每个类需要强行加了一层继承关系,实现特有的Update方法;
  • 有些异步callback接口没有类从属关系,会导致代码逻辑统一性的问题;

另外从搜索引擎的结果来看,这个模式没有十分优雅的应用。当然,如果您有更好关于观察者模式的游戏服务端实现样例欢迎交流一下:D。

 

这篇文章的是游戏服务端设计模式系列的上篇,章节结构是根据《游戏编程模式》,下篇会谈谈个人对原型、单例、状态模式在游戏服务端中的使用心得。

(全文结束)


转载文章请注明出处:漫漫路 - lanindex.com

Leave a Comment

Your email address will not be published.

鄂公网安备42018502003990号