2009-04-19 29 views
14

Digamos que estoy creando un juego OpenGL en C++ que tendrá muchos objetos creados (enemigos, personajes de jugador, elementos, etc.). Me pregunto cuál es la mejor manera de organizarlos, ya que se crearán y destruirán en tiempo real en función del tiempo, la posición/acciones del jugador, etc.¿La mejor manera de organizar entidades en un juego?

Esto es lo que he pensado hasta ahora: Puedo tener un matriz para almacenar punteros a estos objetos. Las texturas/contexto para estos objetos se cargan en sus constructores. Estos objetos tendrán diferentes tipos, por lo que puedo convertir los punteros para obtenerlos en la matriz, pero quiero tener una función renderObjects() que usará un ciclo para llamar a una función ObjectN.render() para cada objeto existente.

Creo que he intentado esto antes, pero no sabía con qué tipo inicializar la matriz, así que seleccioné un tipo de objeto arbitrario y luego eché todo lo que no era de ese tipo. Si mal no recuerdo, esto no funcionó porque el compilador no quería que desmarcara los punteros si ya no conocía su tipo, incluso si una función miembro dada tenía el mismo nombre: (* Object5) .render() < -doesn no funciona?

¿Hay una manera mejor? ¿Cómo manejan esto los juegos comerciales como HL2? Me imagino que debe haber algún módulo, etc. que haga un seguimiento de todos los objetos.

+0

también puede verificar cómo está estructurado un motor 3D, p. este: http://www.ogre3d.org/docs/manual/manual_4.html#SEC4 – galambalazs

Respuesta

10

No estoy seguro de entender por completo la pregunta, pero creo que está queriendo crear una colección de objetos polimórficos. Al acceder a un objeto polimórfico, siempre debe consultarlo con un puntero.

Aquí hay un ejemplo. Lo primero que necesita para configurar una clase base para derivar sus objetos a partir de:

class BaseObject 
{ 
public: 
    virtual void Render() = 0; 
}; 

A continuación, crear la matriz de punteros. Yo uso un conjunto STL porque eso hace que sea fácil para agregar y quitar miembros al azar:

#include <set> 

typedef std::set<BaseObject *> GAMEOBJECTS; 
GAMEOBJECTS g_gameObjects; 

Para añadir un objeto, crear una clase derivada y instanciarlo:

class Enemy : public BaseObject 
{ 
public: 
    Enemy() { } 
    virtual void Render() 
    { 
     // Rendering code goes here... 
    } 
}; 

g_gameObjects.insert(new Enemy()); 

continuación para acceder a objetos, simplemente iterar a través de ellos:

for(GAMEOBJECTS::iterator it = g_gameObjects.begin(); 
    it != g_gameObjects.end(); 
    it++) 
{ 
    (*it)->Render(); 
} 

para crear diferentes tipos de objeto, simplemente derivar más clases de BaseObject clase. No olvides eliminar los objetos cuando los elimines de la colección.

+0

Genial. No había pensado en usar la herencia. –

+3

¡No se olvide de la agregación! – Manuel

2

Debe hacer una superclase de todos sus objetos que tenga un método genérico render(). declare este método como virtual, y haga que cada subclase lo implemente a su manera.

8

La forma en que me he estado acercando a esto es tener una capa de visualización que no sabe nada sobre el mundo del juego en sí. su único trabajo es recibir una lista ordenada de objetos para dibujar en la pantalla que encajen en un formato uniforme para un objeto gráfico. así, por ejemplo, si se trata de un juego en 2D, la capa de visualización recibirá una lista de imágenes junto con su factor de escala, opacidad, rotación, volteo y textura de origen, y cualquier otro atributo que pueda tener un objeto de visualización. La vista también puede ser responsable de recibir interacciones de mouse de alto nivel con estos objetos mostrados y distribuirlos en algún lugar apropiado. Pero es importante que la capa de vista no sepa nada de lo que está mostrando. Solo que es una especie de cuadrado con un área de superficie y algunos atributos.

Luego, la siguiente capa hacia abajo es un programa cuyo trabajo es simplemente generar una lista de estos objetos en orden.Es útil si cada objeto en la lista tiene algún tipo de ID único, ya que hace posibles ciertas estrategias de optimización en la capa de visualización. Generar una lista de objetos de visualización es una tarea mucho menos desalentadora que tratar de descubrir para cada tipo de personaje cómo se renderizará físicamente.

La clasificación Z es bastante simple. El código de generación de objetos de visualización solo necesita generar la lista en el orden que desee, y puede usar cualquier medio que necesite para llegar allí.

En nuestro programa de lista de objetos de visualización, cada carácter, prop y NPC tiene dos partes: un asistente de base de datos de recursos y una instancia de carácter. El asistente de la base de datos presenta para cada personaje una interfaz simple desde la que cada personaje puede obtener cualquier imagen/estadística/animación/disposición, etc. que el personaje necesite. Probablemente desees encontrar una interfaz bastante uniforme para buscar los datos, pero va a variar un poco de un objeto a otro. Un árbol o una roca no necesita tantas cosas como un NPC totalmente animado, por ejemplo.

Luego necesita alguna forma de generar una instancia para cada tipo de objeto. Puede implementar esta dicotomía utilizando los sistemas de clase/instancia integrados de su idioma, o según sus necesidades, es posible que deba trabajar un poco más allá de eso. por ejemplo, que cada base de datos de recursos sea una instancia de una clase de base de datos de recursos, y que cada instancia de caracteres sea una instancia de una clase de "caracteres". Esto le ahorra escribir un trozo de código para cada pequeño objeto en el sistema. De esta forma, solo necesita escribir código para categorías amplias de objetos, y solo cambia pequeñas cosas como la fila de una base de datos para buscar imágenes.

Entonces, no se olvide de tener un objeto interno que represente su cámara. Luego, es tarea de su cámara preguntar a cada personaje dónde están en relación con la cámara. Básicamente, está repasando cada instancia de personaje y preguntando por su objeto de visualización. "¿Cómo te ves y dónde estás?"

Cada instancia de personaje, a su vez, tiene su propio pequeño asistente de base de datos de recursos para consultar. Así que cada instancia de personaje tiene a su disposición toda la información que necesita para decirle a la cámara lo que necesita saber.

Esto te deja con un conjunto de instancias de personajes en un mundo que es más o menos ajeno a los detalles de cómo se van a mostrar en una pantalla física, y más o menos ajeno a la esencia de cómo buscar datos de imagen del disco duro. Esto es bueno, te deja con la pizarra lo más limpia posible para una especie de mundo platónico "puro" de personajes en el que puedes implementar tu lógica de juego sin preocuparte por cosas como caerse del borde de la pantalla. Piensa en qué tipo de interfaz te gustaría si pusieras un lenguaje de scripting en tu motor de juego. Lo más simple posible ¿no? Tan arraigados en un mundo simulado como sea posible, sin preocuparse por los pequeños detalles técnicos de implementación ¿no? Eso es lo que esta estrategia te permite hacer.

Además, la separación de preocupaciones le permite cambiar la capa de visualización con la tecnología que desee: Open GL, DirectX, software de renderizado, Adobe Flash, Nintendo DS, lo que sea- Sin tener que preocuparse demasiado con las otras capas .

Además, puedes cambiar la capa de la base de datos para hacer cosas como reskinar todos los personajes- o dependiendo de cómo lo construyas, cambiar un juego completamente nuevo con nuevo contenido que reutilice la mayor parte de las interacciones de los personajes/código de detección de colisión/buscador de ruta que escribió en la capa intermedia.

+1

Esta es una gran respuesta. Mi juego es 2D y bastante simple (solo tendré de 30 a 40 objetos en existencia en cualquier momento. A menudo he imaginado cómo implementaría un motor que realmente necesita una "base de datos" para mantener un montón de información sobre los objetos, y usted presenta una buena solución. Gracias! –

31

Para mi próximo proyecto de juego personal, utilizo un sistema de entidades basado en componentes.

Puede leer más sobre esto buscando "desarrollo de juegos basado en componentes". Un artículo famoso es Evolve Your Hierarchy del blog de programación de Cowboy.

En mi sistema, las entidades solo son identificadores: sin firmar durante mucho tiempo, algo así como en una base de datos relacional. Todos los datos y la lógica asociados a mis entidades están escritos en Componentes. Tengo sistemas que vinculan identificadores de entidades con sus respectivos componentes. Algo así:

typedef unsigned long EntityId; 

class Component { 
    Component(EntityId id) : owner(id) {} 
    EntityId owner; 
}; 

template <typename C> class System { 
    std::map<EntityId, C * > components; 
}; 

Luego, para cada tipo de funcionalidad, escribo un componente especial. Todas las entidades no tienen los mismos componentes. Por ejemplo, podría tener un objeto de roca estática que tenga el WorldPositionComponent y el ShapeComponent, y un enemigo en movimiento que tenga los mismos componentes más el VelocityComponent. He aquí un ejemplo:

class WorldPositionComponent : public Component { 
    float x, y, z; 
    WorldPositionComponent(EntityId id) : Component(id) {} 
}; 

class RenderComponent : public Component { 
    WorldPositionComponent * position; 
    3DModel * model; 
    RenderComponent(EntityId id, System<WorldPositionComponent> & wpSys) 
     : Component(id), position(wpSys.components[owner]) {} 
    void render() { 
     model->draw(position); 
    } 
}; 

class Game { 
    System<WorldPositionComponent> wpSys; 
    System<RenderComponent> rSys; 
    void init() { 
     EntityId visibleObject = 1; 
     // Watch out for memory leaks. 
     wpSys.components[visibleObject] = new WorldPositionComponent(visibleObject); 
     rSys.components[visibleObject] = new RenderComponent(visibleObject, wpSys); 
     EntityId invisibleObject = 2; 
     wpSys.components[invisibleObject] = new WorldPositionComponent(invisibleObject); 
     // No RenderComponent for invisibleObject. 
    } 
    void gameLoop() { 
     std::map<EntityId, RenderComponent *>::iterator it; 
     for (it = rSys.components.iterator(); it != rSys.components.end(); ++it) { 
      (*it).second->render(); 
     } 
    } 
}; 

Aquí usted tiene 2 componentes, WorldPosition y render. La clase Game tiene los 2 sistemas. El componente Render tiene acceso a la posición del objeto. Si la entidad no tiene un componente WorldPosition, puede elegir valores predeterminados o ignorar la entidad. El método Game :: gameLoop() solo mostrará visibleObject. No hay desperdicio de procesamiento para componentes no representables.

También puede dividir mi clase Game en dos o tres, para separar los sistemas de visualización y entrada de la lógica. Algo así como Modelo, Vista y Controlador.

Me resulta útil definir la lógica de mi juego en términos de componentes, y tener entidades que solo tengan la funcionalidad que necesitan: no hay renderizado vacío() o inútiles controles de detección de colisiones.

+0

Gracias, esto es bastante elegante. Sin embargo, uno piensa que cuando traté de compilarlo, la última línea de código tenía que ser: '(* it) .second-> render();' . – sharvey

+0

Bien, gracias por su corrección. – Splo

+0

¡Buen trabajo, elegante! Tengo curiosidad: ¿está utilizando un EventManager para administrar la comunicación? – Manuel

1

¿Hay una manera mejor? ¿Cómo manejan esto los juegos comerciales como HL2? Imagino que debe haber algún módulo, etc. que haga un seguimiento de todos los objetos.

Los juegos 3D comerciales utilizan una variación en el Scene Graph. Una jerarquía de objetos como la que describe Adam se coloca en lo que generalmente es una estructura de árbol. Para renderizar o manipular objetos, simplemente camina por el árbol.

Varios libros discuten esto, y lo mejor que he encontrado es 3D Game Engine Design and Architecture, ambos de David Eberly.

Cuestiones relacionadas