Поддержание порядка в клиент-серверной игре

Предыстория: я помогаю разрабатывать многопользовательскую игру, написанную в основном на 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
}

Как видите, при компиляции только сервера определения препроцессора используются для исключения графического и звукового кода (что выглядит некрасиво).

Вопрос:

Каковы некоторые из лучших практик для организации и очистки кода в архитектуре клиент-сервер?

Спасибо!

Изменить: примеры проектов с открытым исходным кодом, в которых используется хорошая организация, также приветствуются :)


person faffy    schedule 06.08.2012    source источник


Ответы (3)


Я бы рассмотрел возможность использования шаблона разработки стратегии, в соответствии с которым у вас будет класс Ship с функциональностью, общей для обоих клиентов. и server, затем создайте другую иерархию классов, называемую чем-то вроде 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...
}
person Brady    schedule 06.08.2012

Почему бы не использовать простой подход? Предоставьте один заголовок, описывающий, что будет делать класс 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

Этот код кажется вполне читаемым (если не считать когнитивного диссонанса, вносимого битом Client only / #ifndef SERVER_ONLY, который можно исправить с разными именами), особенно если шаблон повторяется во всем приложении.

Единственный недостаток, который я вижу, заключается в том, что вам нужно будет дважды повторить сигнатуры клиентских функций, но если вы ошибетесь, это будет очевидно и тривиально исправить, как только вы увидите ошибку компилятора.

person Watusimoto    schedule 07.08.2012
comment
Что ж, пока это самый простой подход, но он едва ли менее громоздкий, чем тот код, который уже есть. - person faffy; 09.08.2012

Определите клиентский класс-заглушку с клиентским API.

Определите класс сервера, который реализует сервер.

Определите заглушку сервера, которая сопоставляет входящие сообщения с вызовами сервера.

Класс-заглушка не имеет реализации, кроме как для прокси-команд на сервер через любой протокол, который вы используете.

Теперь вы можете изменять протоколы, не меняя дизайн.

or

Используйте библиотеку, например MACE-RPC, для автоматического создания клиентских и серверных заглушек из серверного API. .

person bytemaster    schedule 06.08.2012
comment
Вы случайно не знаете пример вашего дизайна? - person faffy; 24.08.2012