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クラスを作成し、Shipの属性となるShipSpecificsのような別のクラス階層を作成するというストラテジーデザインパターンの使用を検討します。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 クラスが何をするかを説明する単一のヘッダーを提供します。コメントはありますが、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 ビットによって導入された認知的不協和は別として、別の名前で修正可能です)。特に、パターンがアプリケーション全体で繰り返される場合はそうです。

私が見た唯一の欠点は、クライアント専用関数のシグネチャを 2 回繰り返す必要があることですが、失敗した場合、コンパイラ エラーが表示されたら、修正するのは明らかで簡単です。

于 2012-08-07T08:30:20.733 に答える