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