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

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
}

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

问题:

在客户端-服务器架构中,保持代码组织和清晰的最佳实践是什么?

谢谢!

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

3个回答

4
我会考虑使用策略设计模式,其中您将拥有一个具有客户端和服务器通用功能的船舶类,然后创建另一个类层次结构,称为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...
}

2
为什么不采用简单的方法?提供一个描述Ship类将要执行的单一头文件,包括注释但不包括条件编译。然后,在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的部分带来的认知失调,可以通过使用不同的名称解决),特别是如果该模式在整个应用程序中重复出现。

我唯一看到的缺点是你需要重复两次客户端函数签名,但是如果出错,一旦看到编译器错误,就会很明显且容易修复。


1
这是目前最简单的方法,但它几乎和已经存在的代码一样繁琐。 - faffy

2
定义一个客户端存根类,具有客户端API。
定义实现服务器的服务器类。
定义一个服务器存根,将传入的消息映射到服务器调用。
存根类除了通过您正在使用的任何协议代理命令到服务器之外,没有实现。
现在,您可以更改协议而不更改设计。
或者,
使用类似MACE-RPC的库自动生成客户端和服务器存根。

1
你是否有你设计的一个例子? - faffy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接