c++ - pensamiento - lecturas revista ethos gubernamental
¿La mejor forma de organizar entidades en un juego? (5)
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.
Esto es lo que he pensado hasta ahora: puedo tener una matriz global 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 lo he intentado 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 sabía su tipo, incluso si una función miembro dada tenía el mismo nombre: (* Object5) .render () <-doesn no funciona?
¿Hay una mejor manera? ¿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.
Debería 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.
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 personaje, prop y NPC tiene dos partes: un asistente de base de datos de recursos y una instancia de personaje. 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.
Entonces necesitas 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 olvides tener un objeto interno que represente tu 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 la esencia de cómo se van a mostrar en una pantalla física, y más o menos ajeno a la esencia de cómo obtener datos de imagen de la 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, ¿verdad? 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, intercambia un juego completamente nuevo con nuevo contenido que reutilice la mayor parte de las interacciones de los personajes / detección de colisión / ruta código de buscador que escribió en la capa intermedia.
No estoy seguro de entender completamente la pregunta, pero creo que quieres 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. Primero necesita configurar una clase base para derivar sus objetos de:
class BaseObject
{
public:
virtual void Render() = 0;
};
Luego crea la matriz de punteros. Utilizo un conjunto de STL porque facilita la tarea de agregar y eliminar miembros al azar:
#include <set>
typedef std::set<BaseObject *> GAMEOBJECTS;
GAMEOBJECTS g_gameObjects;
Para agregar un objeto, crea una clase derivada y crea una instancia:
class Enemy : public BaseObject
{
public:
Enemy() { }
virtual void Render()
{
// Rendering code goes here...
}
};
g_gameObjects.insert(new Enemy());
Luego, para acceder a los objetos, simplemente repítelos:
for(GAMEOBJECTS::iterator it = g_gameObjects.begin();
it != g_gameObjects.end();
it++)
{
(*it)->Render();
}
Para crear diferentes tipos de objetos, solo obtenga más clases de la clase BaseObject. No olvides eliminar los objetos cuando los elimines de la colección.
¿Hay una mejor manera? ¿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.
Los juegos comerciales en 3D usan una variación en el gráfico de escena . 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.
Para mi próximo proyecto de juego personal, uso un sistema de entidad 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 Cowboy.
En mi sistema, las entidades son solo identificadores, sin signo largo, 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 como eso:
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. Aquí hay 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í tienes 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 representará visibleObject. No hay desperdicio de procesamiento para componentes no representables.
También puede dividir mi clase de juego 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 parece bien definir mi lógica de juego en términos de componentes, y tener entidades que solo tienen la funcionalidad que necesitan: no hay más renderizado vacío () o inútiles controles de detección de colisiones.