背景:我帮助开发一款多人游戏,主要使用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
}
正如您所看到的,在仅编译服务器时,预处理器定义用于排除图形和声音代码(这看起来很丑陋)。
问题:
保持客户端 - 服务器架构中的代码有条理和清洁的一些最佳做法是什么?
谢谢!
编辑:欢迎使用良好组织的开源项目示例:)
答案 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位引入的认知不一致,可用不同的名称修复),特别是如果在整个应用程序中重复该模式。
我看到的唯一缺点是你需要重复两次仅限客户端的功能签名,但是如果你搞砸了,一旦你看到编译器错误就会明显而且微不足道。