2012-08-06 16 views
9

Antecedentes: Ayudo a desarrollar un juego multijugador, escrito principalmente en C++, que usa una arquitectura estándar cliente-servidor. El servidor puede compilarse por sí mismo, y el cliente se compila con el servidor para que pueda alojar juegos.Mantener el código en un juego cliente-servidor organizado

Problema

El juego combina código de cliente y servidor en las mismas clases, y esto está empezando a ser muy engorroso.

Por ejemplo, la siguiente es una pequeña muestra de algo que se puede ver en una clase común:

// 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 

Y el encabezado:

class Ship 
{ 
    // Server + client 
    Point calcPosition(); 

    // Server only 
    void explode(); 

    // Client only 
    #ifndef SERVER_ONLY 
    void renderExplosion(); 
    #endif 
} 

Como se puede ver, al compilar el servidor solo, las definiciones del preprocesador se utilizan para excluir los gráficos y el código de sonido (lo que parece feo).

Pregunta:

Cuáles son algunas de las mejores prácticas para mantener el código en una arquitectura cliente-servidor organizada y limpia?

Gracias!

Editar: ejemplos de proyectos de código abierto que utilizan una buena organización también son bienvenidos :)

Respuesta

2

Definir una clase stub cliente que tiene la API de cliente.

Defina la clase de servidor que implementa el servidor.

Defina un código auxiliar que correlaciona el mensaje entrante con las llamadas al servidor.

La clase de stub no tiene ninguna implementación, excepto para los comandos de proxy para el servidor a través de cualquier protocolo que esté utilizando.

Ahora puede cambiar los protocolos sin cambiar su diseño.

o

utilizar una biblioteca como MACE-RPC para generar automáticamente los recibos de cliente y servidor de la API de servidor.

+1

¿Conoces algún ejemplo de tu diseño? – faffy

3

Consideraría usar un Strategy design pattern por el cual tendría una clase Ship con funcionalidad común tanto para el cliente como para el servidor, luego crearía otra jerarquía de clase llamada algo así como ShipSpecifics que sería un atributo de Ship. ShipSpecifics se crearía con una clase derivada concreta de servidor o cliente y se inyectaría en Enviar.

Podría ser algo como esto:

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 
}; 

La nave clases y ShipSpecifics estarían en la base de código común tanto para el cliente y el servidor, y las clases ShipSpecificsServer y ShipSpecificsClient serían obviamente en el servidor y el cliente bases de código respectivamente.

El uso podría ser algo como lo siguiente:

// 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

por qué no tomar un enfoque simple? Proporcione un encabezado único que describa lo que hará la clase Ship, con comentarios pero no ifdefs. A continuación, proporcione la implementación del cliente dentro de un archivo ifdef como lo hizo en su pregunta, pero proporcione un conjunto alternativo de implementaciones (vacías) que se utilizarán cuando el cliente no se compila.

Me parece que si está claro en sus comentarios y en la estructura del código, este enfoque será mucho más fácil de leer y entender que las soluciones más "sofisticadas" propuestas.

Este enfoque tiene la ventaja adicional de que si el código compartido, aquí calcPosition(), necesita tomar una ruta de ejecución ligeramente diferente en el cliente vs. el servidor, y el código del cliente necesita llamar a una función (vea el ejemplo a continuación), no encontrará complicaciones de construcción.

Cabecera:

class Ship 
{ 
    // Server + client 
    Point calcPosition(); 

    // Server only 
    void explode(); 
    Point calcServerActualPosition(); 

    // Client only 
    void renderExplosion(); 
    Point calcClientPredicitedPosition(); 
} 

cuerpo:

// 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 

Este código parece bastante legible (aparte de la disonancia cognitiva introducida por el cliente solo/#ifndef SERVER_ONLY bits, que se puede fijar con diferentes nombres), especialmente si el patrón se repite a lo largo de la aplicación.

El único inconveniente que veo es que tendrá que repetir dos veces las firmas de función del cliente, pero si lo arruina, será obvio y trivial corregirlo una vez que vea el error del compilador.

+1

Bueno, este es el enfoque más simple hasta ahora, pero es apenas menos engorroso que el código que ya está allí. – faffy

Cuestiones relacionadas