2009-09-22 11 views
9

Tengo el siguiente código, piensan tirador simple en C++:¿Cuál es la forma más elegante y eficiente de modelar una jerarquía de objetos del juego? (Diseño molesta)

// world.hpp 
//---------- 
class Enemy; 
class Bullet; 
class Player; 

struct World 
{ 
    // has-a collision map 
    // has-a list of Enemies 
    // has-a list of Bullets 
    // has-a pointer to a player 
}; 

// object.hpp 
//----------- 
#include "world.hpp" 

struct Object 
{ 
    virtual ~Object(); 
    virtual void Update() =0; 
    virtual void Render() const =0; 

    Float xPos, yPos, xVel, yVel, radius; // etc. 
}; 

struct Enemy: public Object 
{ 
    virtual ~Enemy(); 
    virtual void Update(); 
    virtual void Render() const; 
}; 

// Bullet and Player are similar (they render and update differently per se, 
/// but the behavior exposed to World is similar) 

// world.cpp 
//---------- 
#include "object.hpp" 

// object.cpp 
//----------- 
#include "object.hpp" 

dos problemas con este:

1, Los objetos del juego que saben de otros objetos del juego.

Tiene que haber una instalación que conozca todos los objetos. Puede o no tener que exponer TODOS los objetos, tiene que exponer algunos de ellos, dependiendo de los parámetros del investigador (posición, para uno). Se supone que esta instalación es Mundo.

Los objetos deben conocer el mundo en el que se encuentran para buscar información sobre colisiones y otros objetos.

Esto introduce una dependencia donde tanto los Objetos como la implementación Mundial tienen que acceder al encabezado del objeto, por lo que World no incluirá su propio encabezado directamente en lugar de incluir object.hpp (que a su vez incluye world.hpp). Esto me hace sentir incómodo: no creo que world.cpp deba volver a compilarse después de hacer un cambio en object.hpp. El mundo no parece que debería funcionar con Object. Se siente como un mal diseño, ¿cómo puede solucionarse?

2, Polimorfismo: ¿puede y debe usarse para algo más allá de la reutilización de código y la agrupación lógica de entidades de juego (y cómo)?

Enemigos, balas y reproductor se actualizarán y representarán de manera diferente, seguramente, de ahí la funcionalidad virtual Update() y Render() - una interfaz idéntica. Todavía se mantienen en listas separadas y la razón de esto es que su interacción depende de las listas de dos objetos que interactúan: dos enemigos rebotando entre sí, una bala y un enemigo se destruyen entre sí, etc.

Esto es funcionalidad eso está más allá de la implementación de Enemy and Bullet; esa no es mi preocupación aquí. Siento que más allá de sus interfaces idénticas hay un factor que separa Enemigos, balas y jugadores, y esto podría y debería expresarse de otra manera, permitiéndome crear una lista única para todos ellos, ¡ya que su interfaz es idéntica!

El problema es cómo distinguir una categoría de objeto de otra si está dentro de la misma lista. La identificación tidida u otra forma de identificación del tipo está fuera de discusión, pero ¿qué otra manera de hacerlo? O tal vez no hay nada de malo con el enfoque?

Respuesta

5

Este es probablemente el mayor problema que encuentro al diseñar programas similares. El enfoque en el que me he asentado es darse cuenta de que a un objeto realmente no le importa dónde está en términos absolutos. Lo único que le importa es qué hay a su alrededor. Como resultado, el objeto World (y prefiero el enfoque de objeto a un singleton por muchas buenas razones que puede buscar en Internet) mantiene donde están todos los objetos, y pueden preguntarle al mundo qué objetos están cerca, donde otros los objetos están en relación con él, etc. World no debería preocuparse por el contenido de Object; sostendrá casi cualquier cosa, y los Object s mismos definirán cómo interactúan entre sí. Los eventos también son una excelente manera de manejar los objetos que interactúan, ya que proporcionan un medio para World de informar Object de lo que está sucediendo sin importar qué es Object.

ocultación de información de un Object sobre todos los objetos es una buena idea, y no debe confundirse con la ocultación de información sobre cualquier Object. Piense en términos de personas: es razonable que conozca y retenga información sobre muchas personas diferentes, aunque solo puede encontrar esa información encontrándolas o haciendo que otra persona le cuente sobre ellas.

nuevo EDIT:

bien. Para mí, es bastante claro lo que realmente quieres, y eso es el envío múltiple: la capacidad de manejar una situación de forma polimórfica en tipos de muchos parámetros para la llamada a la función, en lugar de solo uno. Desafortunadamente, C++ no admite múltiples envíos de forma nativa.

Aquí hay dos opciones. Puede intentar reproducir despacho múltiple con doble envío o el patrón de visitante, o puede usar dynamic_cast. Lo que quiere usar depende de las circunstancias. Si tiene muchos tipos diferentes para usar esto, dynamic_cast es probablemente el mejor enfoque. Si tiene solo unos pocos, el envío doble o el patrón de visitante probablemente sea más apropiado.

+0

Gracias por los primeros dos párrafos; tiene mucho sentido y me ayudó a aclarar mi mente. He reescrito mi pregunta para aclarar qué quería hacer con el polimorfismo. – zyndor

+0

El patrón Visitor es impresionante, gracias por señalarlo. Supongo que los métodos OnHit() son análogos al método Visit() del patrón y los Objects serían sus propios visitantes, ¿no? – zyndor

+1

Ha sido difícil elegir una respuesta para aceptar como la mejor, ya que todos ellos obtuvieron buenos puntos, pero ninguno de ellos fue realmente completo. Sentí que el esquema de diseño sobre la interacción Objeto-Mundo fue la información más útil y la más relevante para mi pregunta. - – zyndor

6

Creo que le gustaría dar una referencia al mundo del objeto del juego. Este es un principio bastante básico de Dependency Injection. Para colisiones, World puede usar el Strategy pattern para resolver cómo interactúan los tipos específicos de objetos.

Esto mantiene el conocimiento de diferentes tipos de objetos fuera de los objetos primarios y los encapsula en objetos con conocimiento específico para la interacción.

+0

¿Es una solución demasiado mala (diseño OO y rendimiento) que, en lugar de pasar la referencia a Mundo cada vez, simplemente almacene un puntero de miembro estático en Mundo, accesible para todos los Objetos? (No en la forma pública sin editar que se presentó, tal vez, podría ser un puntero protegido y un método público estático para establecerlo.) – zyndor

+0

Cómo usted almacena la referencia al objeto World depende de usted. Dado que estos objetos probablemente no se instanciarán en múltiples mundos simultáneamente, almacenar la referencia en la clase probablemente sea correcto. La idea clave es que el mundo todavía se proporciona a los objetos individuales en el tiempo de creación de instancias por lo que si usted quiere tener dos objetos del mismo tipo en diferentes mundos, la API de ejemplo no cambia y el único refactorización que necesita ocurrir está dentro de la implementación del objeto mismo. –

3

Los objetos tienen que saber sobre el mundo que están, para consultar colisión información y otros objetos.

En resumen, no, no es así. Las clases como el Mundo pueden manejar la mayor parte de eso y todo lo que el Objeto tiene que hacer es comportarse apropiadamente cuando el Mundo dice que ha habido una colisión.

A veces puede necesitar que el Objeto tenga algún tipo de contexto para tomar otro tipo de decisión. Puedes pasar el objeto Mundo en ese punto cuando sea necesario. Sin embargo, en lugar de pasar el mundo, es mejor pasar un objeto más pequeño y más relevante dependiendo de qué tipo de consulta se está realizando. Es probable que su objeto World realice varios roles diferentes y que los Objetos solo necesiten acceso transitorio a uno o dos de esos roles. En ese caso, es bueno dividir el objeto World en subobjetos, o si eso no es práctico, haga que implemente varias interfaces distintas.

0

Después de pensarlo mucho me di cuenta de que a pesar de objetos DO necesidad de tener acceso a un Mundial, es NO responsabilidad del mundo para servir cercanas objetos a objetos.

Esto es lo que tenía el Arena no objeto (nunca instanciado + todos los miembros estáticos, contiene listas de las categorías de objetos requeridas - balas, enemigos, visuales, etc.) Para, sin embargo, presenta una estructura similar a la dependencia que tienen las listas de categorías como parte del mundo (por lo que no se sentía como una buena solución):

// object.hpp 
//----------- 
#include "world.hpp" 

// NOTE: the obvious virtual and pure virtual methods are omitted in the following code 
class Object 
{...}; 

class Enemy: public Object 
{...}; 

class Bullet: public Object 
{...}; 

class Player: public Object 
{...}; 

// arena.hpp 
//----------- 
#include "object.hpp" 

struct Arena 
{ 
    // has-a lists of object categories and a (pointer to a) player 
} 

// object.cpp 
//----------- 
#include "arena.hpp" // for the damn dependencies 

// arena.cpp 
//----------- 
#include "arena.hpp" 

Por lo tanto, la solución, o lo que parece estar en este punto, es tener la totalidad del objeto (categoría) s no un nivel de compilación por encima o por debajo de la declaración de los objetos, sino en el mismo nivel.

// object.hpp 
//----------- 
#include "world.hpp" 

class Object 
{ 
    static World *pWorld; 
    ... 
}; 

class Enemy: public Object 
{ 
    typedef std::list<Enemy*> InstList; 
    static InstList insts; 
    ... 
}; 

class Bullet: public Object 
{ 
    typedef std::list<Bullet*> InstList; 
    static InstList insts; 
    ... 
}; 

class Player: public Object 
{ 
    static Player *pThePlayer; 
    ... 
}; 

// object.cpp 
//----------- 
#include "object.hpp" 

Incluso si hay otras cabeceras para especializada enemigos, balas, etc. sus (y de otros) se enumeran las categorías 'están totalmente disponibles para ellos mediante la inclusión de object.hpp que obviamente tienen que de todos modos.

Sobre el bit de polimorfismo y por qué las diferentes categorías se mantienen en listas separadas: las clases base de las categorías de objeto (Bullet, Enemy, Player) pueden proporcionar "manejadores de eventos" para golpear objetos de ciertos tipos; ellos, sin embargo, no están declarados en el nivel de objeto en lugar de en el nivel de categoría. (Por ejemplo, que no se preocupan por las colisiones de balas vs-bala, que no están comprobados y no se manejan.)

// object.hpp 
//----------- 
#include "world.hpp" 

class Object 
{ 
    static World *pWorld; 
    ... 
}; 

class Bullet: public Object 
{ 
    typedef std::list<Bullet*> InstList; 
    static InstList insts; 
    ... 
}; 

class Player: public Object 
{ 
    static Player *pThePlayer; 

    void OnHitBullet(const Bullet *b); 
    ... 
}; 

class Enemy: public Object 
{ 
    typedef std::list<Enemy*> InstList; 
    static InstList insts; 

    virtual void OnHitBullet(const Bullet *b); 
    virtual void OnHitPlayer(const Player *p); 
    ... 
}; 

EDITAR, De forma complementaria:

// game.hpp 
//----------- 
#include "object.hpp" 

struct Game 
{ 
    static World world; 

    static void Update(); // update world and objects, check for collisions and 
         /// handle them. 
    static void Render(); 
}; 
Cuestiones relacionadas