13

背景: 我帮助开发了一个多人游戏,主要用 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
}

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

问题:

在客户端-服务器架构中保持代码有条理和整洁的最佳实践是什么?

谢谢!

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

4

3 回答 3

4

我会考虑使用一种策略设计模式,您将拥有一个具有客户端和服务器通用功能的 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...
}
于 2012-08-06T12:48:15.540 回答
2

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

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

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

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

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

或者

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

于 2012-08-06T03:12:15.280 回答
2

为什么不采取简单的方法呢?提供一个描述 Ship 类将做什么的标题,带有注释但没有 ifdefs。然后在 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

这段代码看起来非常易读(除了由 Client only / #ifndef SERVER_ONLY bit 引入的认知失调,可以用不同的名称修复),尤其是在整个应用程序中重复该模式的情况下。

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

于 2012-08-07T08:30:20.733 回答