保持客户端 - 服务器游戏中的代码组织

时间:2012-08-06 01:55:13

标签: c++ oop design-patterns architecture code-separation

背景:我帮助开发一款多人游戏,主要使用C ++编写,使用标准的客户端 - 服务器架构。服务器可以自己编译,客户端使用服务器编译,因此您可以托管游戏。

问题

游戏将客户端和服务器代码组合到同一个类中,这开始变得非常麻烦。

例如,以下是您可能在公共类中看到的一小部分示例:

// Server + client
Point Ship::calcPosition()
{
  // Do position calculations; actual (server) and predictive (client)
}

// Server only
void Ship::explode()
{
  // Communicate to the client that this ship has died
}

// Client only
#ifndef SERVER_ONLY
void Ship::renderExplosion()
{
  // Renders explosion graphics and sound effects
}
#endif

标题:

class Ship
{
    // Server + client
    Point calcPosition();

    // Server only
    void explode();

    // Client only
    #ifndef SERVER_ONLY
    void renderExplosion();
    #endif
}

正如您所看到的,在仅编译服务器时,预处理器定义用于排除图形和声音代码(这看起来很丑陋)。

问题:

保持客户端 - 服务器架构中的代码有条理和清洁的一些最佳做法是什么?

谢谢!

编辑:欢迎使用良好组织的开源项目示例:)

3 个答案:

答案 0 :(得分:3)

我会考虑使用Strategy design pattern,您将拥有一个具有客户端和服务器通用功能的Ship类,然后创建另一个类层次结构,称为ShipSpecifics,它将是Ship的一个属性。 ShipSpecifics将使用服务器或客户端具体派生类创建并注入Ship。

看起来像这样:

class ShipSpecifics
{
    // create appropriate methods here, possibly virtual or pure virtual
    // they must be common to both client and server
};

class Ship
{
public:
    Ship() : specifics_(NULL) {}

    Point calcPosition();
    // put more common methods/attributes here

    ShipSpecifics *getSpecifics() { return specifics_; }
    void setSpecifics(ShipSpecifics *s) { specifics_ = s; }

private:
    ShipSpecifics *specifics_;
};

class ShipSpecificsClient : public ShipSpecifics 
{
    void renderExplosion();
    // more client stuff here
};

class ShipSpecificsServer : public ShipSpecifics 
{
    void explode();
    // more server stuff here
};

Ship和ShipSpecifics类将位于客户端和服务器共有的代码库中,ShipSpecificsServer和ShipSpecificsClient类显然分别位于服务器和客户端代码库中。

用法可能如下所示:

// client usage
int main(int argc, argv)
{
    Ship *theShip = new Ship();
    ShipSpecificsClient *clientSpecifics = new ShipSpecificsClient();

    theShip->setSpecifics(clientSpecifics);

    // everything else...
}

// server usage
int main(int argc, argv)
{
    Ship *theShip = new Ship();
    ShipSpecificsServer *serverSpecifics = new ShipSpecificsServer();

    theShip->setSpecifics(serverSpecifics);

    // everything else...
}

答案 1 :(得分:2)

定义具有客户端API的客户端存根类。

定义实现服务器的服务器类。

定义将传入消息映射到服务器调用的服务器存根。

除了通过您正在使用的协议向服务器代理命令之外,存根类没有任何实现。

您现在可以在不更改设计的情况下更改协议。

使用MACE-RPC之类的库从服务器API自动生成客户端和服务器存根。

答案 2 :(得分:1)

为什么不采取简单的方法?提供一个标题来描述Ship类将执行的操作,包含注释但不包含ifdef。然后像在你的问题中一样在ifdef中提供客户端实现,但是提供一组备用(空)实现,这些实现将在未编译客户端时使用。

令我感到震惊的是,如果您的评论和代码结构清晰明了,这种方法将比提出的更“复杂”的解决方案更容易阅读和理解。

这种方法还有一个额外的好处,即如果共享代码(这里是calcPosition())需要在客户端与服务器上采用略微不同的执行路径,并且客户端代码需要调用另一个仅客户端的函数(见下面的例子),你不会遇到构建复杂性。

部首:

class Ship
{
    // Server + client
    Point calcPosition();

    // Server only
    void explode();
    Point calcServerActualPosition();

    // Client only
    void renderExplosion();
    Point calcClientPredicitedPosition();
}

体:

// Server + client
Point Ship::calcPosition()
{
  // Do position calculations; actual (server) and predictive     (client)
   return isClient ? calcClientPredicitedPosition() :
                     calcServerActualPosition();
}

// Server only
void Ship::explode()
{
  // Communicate to the client that this ship has died
}

Point Ship::calcServerActualPosition()
{
  // Returns ship's official position
}


// Client only
#ifndef SERVER_ONLY

void Ship::renderExplosion()
{
  // Renders explosion graphics and sound effects
}

Point Ship::calcClientPredicitedPosition() 
{
  // Returns client's predicted position
}

#else

// Empty stubs for functions not used on server
void  Ship::renderExplosion()              { }
Point Ship::calcClientPredicitedPosition() { return Point(); }

#endif

这段代码看起来很可读(除了客户端/ #ifndef SERVER_ONLY位引入的认知不一致,可用不同的名称修复),特别是如果在整个应用程序中重复该模式。

我看到的唯一缺点是你需要重复两次仅限客户端的功能签名,但是如果你搞砸了,一旦你看到编译器错误就会明显而且微不足道。