Организация кода в клиент-серверной игре

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

Я хотел бы рассмотреть возможность использования шаблона проектирования стратегии, в соответствии с которым у вас будет класс 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...
}

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

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

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

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

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

или же

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

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

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

Другие вопросы по тегам