Me resulta difícil encontrar una manera de organizar los objetos del juego para que sean polimórficos, pero al mismo tiempo no polimórficos.
Aquí hay un ejemplo: suponiendo que queremos todos nuestros objetos update()
y draw()
. Para hacerlo, necesitamos definir una clase base GameObject
que tenga esos dos métodos puros virtuales y permita que entre en juego el polimorfismo:
class World {
private:
std::vector<GameObject*> objects;
public:
// ...
update() {
for (auto& o : objects) o->update();
for (auto& o : objects) o->draw(window);
}
};
Se supone que el método de actualización se encarga de cualquier estado que necesite actualizar el objeto de clase específico. El hecho es que cada objeto necesita saber sobre el mundo que lo rodea. Por ejemplo:
- Una mina necesita saber si alguien está chocando con ella.
- Un soldado debe saber si el soldado de otro equipo está cerca
- Un zombie debe saber dónde está el cerebro más cercano, dentro de un radio
Para las interacciones pasivas (como la primera), estaba pensando que la detección de colisión podría delegar qué hacer en casos específicos de colisiones al objeto mismo con un on_collide(GameObject*)
.
La mayoría de las otras informaciones (como los otros dos ejemplos) podrían ser consultadas por el mundo del juego pasado al update
método. Ahora el mundo no distingue los objetos en función de su tipo (almacena todos los objetos en un solo contenedor polimórfico), por lo que de hecho lo que devolverá con un ideal world.entities_in(center, radius)
es un contenedor de GameObject*
. Pero, por supuesto, el soldado no quiere atacar a otros soldados de su equipo y un zombi no se preocupa por otros zombis. Entonces necesitamos distinguir el comportamiento. Una solución podría ser la siguiente:
void TeamASoldier::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
// shoot towards enemy
}
void Zombie::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<Human*>(e))
// go and eat brain
}
pero, por supuesto, el número de dynamic_cast<>
fotogramas por fotograma puede ser terriblemente alto, y todos sabemos lo lento que dynamic_cast
puede ser. El mismo problema también se aplica al on_collide(GameObject*)
delegado que discutimos anteriormente.
Entonces, ¿cuál es la forma ideal de organizar el código para que los objetos puedan conocer otros objetos y puedan ignorarlos o realizar acciones en función de su tipo?
fuente
dynamic_cast<Human*>
, implemente algo como abool GameObject::IsHuman()
, que regresafalse
por defecto pero se anula para regresartrue
en laHuman
clase.IsA
anulación de vtable y personalizada demostró ser solo marginalmente mejor que el casting dinámico en la práctica para mí. Lo mejor que puede hacer es que el usuario tenga, siempre que sea posible, listas de datos ordenadas en lugar de iterar ciegamente sobre todo el conjunto de entidades.TeamASoldier
yTeamBSoldier
es realmente idéntica: disparó a cualquiera en el otro equipo. Todo lo que necesita de otras entidades es unGetTeam()
método en su forma más específica y, por el ejemplo de congusbongus, que pueda resumirse aún más en unaIsEnemyOf(this)
especie de interfaz. El código no necesita preocuparse por las clasificaciones taxonómicas de soldados, zombis, jugadores, etc. Céntrese en la interacción, no en los tipos.Respuestas:
En lugar de implementar la toma de decisiones de cada entidad en sí misma, también puede optar por el patrón controlador. Tendría clases de controlador central que son conscientes de todos los objetos (que les importan) y controlan su comportamiento.
Un MotionController manejaría el movimiento de todos los objetos que pueden moverse (hacer la búsqueda de ruta, actualizar las posiciones en función de los vectores de movimiento actuales).
Un MineBehaviorController verificaría todas las minas y todos los soldados, y ordenaría que explotara una mina cuando un soldado se acerca demasiado.
Un ZombieBehaviorController verificaría a todos los zombies y a los soldados en su vecindad, elegiría el mejor objetivo para cada zombie y le ordenaría que se moviera allí y lo atacara (el movimiento es controlado por el MotionController).
Un SoldierBehaviorController analizaría toda la situación y luego propondría instrucciones tácticas para todos los soldados (te mueves allí, disparas esto, sanas a ese tipo ...). La ejecución real de estos comandos de nivel superior también sería manejada por controladores de nivel inferior. Cuando pones un poco de esfuerzo, puedes hacer que la IA sea capaz de tomar decisiones cooperativas bastante inteligentes.
fuente
std::map
sy las entidades son solo ID y luego tenemos que hacer algún tipo de sistema de tipos (tal vez con un componente de etiqueta, porque el renderizador necesitará saber qué dibujar); y si no queremos hacerlo, necesitaremos un componente de dibujo: pero necesita el componente de posición para saber dónde dibujar, por lo que creamos dependencias entre componentes que resolvemos con un sistema de mensajería súper complejo. ¿Es esto lo que estás sugiriendo?En primer lugar, intente implementar características para que los objetos permanezcan independientes entre sí, siempre que sea posible. Especialmente quieres hacer eso para subprocesos múltiples. En su primer ejemplo de código, el conjunto de todos los objetos podría dividirse en conjuntos que coincidan con el número de núcleos de CPU y actualizarse de manera muy eficiente.
Pero como dijiste, la interacción con otros objetos es necesaria para algunas características. Eso significa que el estado de todos los objetos debe estar sincronizado en algunos puntos. En otras palabras, su aplicación debe esperar a que todas las tareas paralelas terminen primero y luego aplicar cálculos que involucren interacción. Es bueno reducir el número de estos puntos de sincronización, ya que siempre implican que algunos subprocesos deben esperar a que otros terminen.
Por lo tanto, sugiero almacenar esa información sobre los objetos que se necesitan desde dentro de otros objetos. Dado un búfer global de este tipo, puede actualizar todos sus objetos independientemente uno del otro, pero solo dependiendo de ellos mismos y del búfer global, que es más rápido y más fácil de mantener. En un paso de tiempo fijo, por ejemplo, después de cada cuadro, actualice el búfer con el estado de los objetos actuales.
Entonces, lo que debe hacer una vez por cuadro es 1. almacenar el estado actual de los objetos globalmente, 2. actualizar todos los objetos basados en ellos mismos y el almacenamiento intermedio, 3. dibujar sus objetos y luego comenzar de nuevo renovando el almacenamiento intermedio.
fuente
Utilice un sistema basado en componentes, en el que tenga un GameObject básico que contenga 1 o más componentes que definan su comportamiento.
Por ejemplo, supongamos que se supone que algún objeto se mueve hacia la izquierda y hacia la derecha todo el tiempo (una plataforma), puede crear dicho componente y adjuntarlo a un GameObject.
Ahora, supongamos que un objeto del juego debe girar lentamente todo el tiempo, podría crear un componente separado que haga exactamente eso y adjuntarlo al GameObject.
¿Qué pasaría si quisieras tener una plataforma móvil que también girara, en una jerarquía de clases tradicional que se hace difícil sin duplicar el código?
La belleza de este sistema es que, en lugar de tener una clase Rotatable o MovingPlatform, adjuntas ambos componentes al GameObject y ahora tienes una MovingPlatform que gira automáticamente.
Todos los componentes tienen una propiedad, 'requireUpdate' que, si bien es cierto, GameObject llamará al método 'update' en dicho componente. Por ejemplo, supongamos que tiene un componente que se puede arrastrar, este componente al colocar el mouse hacia abajo (si estaba sobre GameObject) puede establecer 'requireUpdate' en verdadero, y luego, al levantar el mouse, establecerlo en falso. Permitir que siga al mouse solo cuando el mouse está abajo.
Uno de los desarrolladores de Tony Hawk Pro Skater tiene el escrito de facto sobre él, y vale la pena leerlo: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
fuente
Favorecer la composición sobre la herencia.
Mi consejo más fuerte aparte de esto sería: No te dejes llevar por la mentalidad de "Quiero que esto sea sumamente flexible". La flexibilidad es excelente, pero recuerde que en algún nivel, en cualquier sistema finito como un juego, hay partes atómicas que se utilizan para construir el todo. De una forma u otra, su procesamiento se basa en esos tipos atómicos predefinidos. En otras palabras, atender "cualquier" tipo de datos (si eso fuera posible) no lo ayudaría a largo plazo, si no tiene un código para procesarlo. Básicamente, todo el código debe analizar / procesar datos basados en especificaciones conocidas ... lo que significa un conjunto predefinido de tipos. ¿Qué tan grande es ese conjunto? Depende de usted.
Este artículo ofrece información sobre el principio de Composición sobre herencia en el desarrollo de juegos a través de una arquitectura de entidad-componente robusta y eficaz.
Al construir entidades a partir de subconjuntos (diferentes) de algún superconjunto de componentes predefinidos, ofrece a sus IA formas concretas y poco sistemáticas de comprender el mundo y los actores que los rodean, leyendo los estados de los componentes de esos actores.
fuente
Personalmente, recomiendo mantener la función de dibujar fuera de la clase Object. Incluso recomiendo mantener la ubicación / coordenadas de los objetos fuera del propio objeto.
Ese método draw () tratará con API de renderizado de bajo nivel de OpenGL, OpenGL ES, Direct3D, su capa de ajuste en esas API o una API de motores. Es posible que deba cambiar entre ellos (si desea admitir OpenGL + OpenGL ES + Direct3D, por ejemplo.
Ese GameObject solo debe contener la información básica sobre su apariencia visual, como una malla o tal vez un paquete más grande que incluya entradas de sombreado, estado de animación, etc.
También vas a querer una tubería gráfica flexible. ¿Qué sucede si desea ordenar objetos en función de su distancia a la cámara? O su tipo de material. ¿Qué sucede si desea dibujar un objeto 'seleccionado' de un color diferente? ¿Qué pasa si en lugar de desgarrar realmente como se llama una función de dibujo en un objeto, en su lugar, lo coloca en una lista de comandos de acciones para que el render tome (puede ser necesario para enhebrar)? Puede hacer ese tipo de cosas con el otro sistema, pero es un PITA.
Lo que recomiendo es que en lugar de dibujar directamente, enlace todos los objetos que desee a otra estructura de datos. Ese enlace solo necesita tener una referencia a la ubicación de los objetos y la información de representación.
Sus niveles / fragmentos / áreas / mapas / centros / mundo entero / lo que sea que se les dé un índice espacial, este contiene los objetos y los devuelve en función de consultas de coordenadas y podría ser una lista simple o algo así como un Octree. También podría ser una envoltura para algo implementado por un motor de física de terceros como una escena de física. Le permite hacer cosas como "Consultar todos los objetos que están a la vista de la cámara con un área adicional a su alrededor", o para juegos más simples en los que simplemente puede hacer que todo tome toda la lista.
Los índices espaciales no tienen que contener la información de posicionamiento real. Funcionan almacenando objetos en estructuras de árbol en relación con la ubicación de otros objetos. Pueden considerarse como una especie de caché con pérdida que permite una búsqueda rápida de un objeto en función de su posición. No hay necesidad real de duplicar sus coordenadas X, Y, Z reales. Habiendo dicho eso, podrías si quisieras seguir
De hecho, los objetos de tu juego ni siquiera necesitan contener su propia información de ubicación. Por ejemplo, un objeto que no se ha puesto en un nivel no debe tener coordenadas x, y, z, eso no tiene sentido. Puede contener eso en el índice especial. Si necesita buscar las coordenadas del objeto en función de su referencia real, entonces querrá tener un enlace entre el objeto y el gráfico de escena (los gráficos de escena son para devolver objetos basados en coordenadas pero son lentos para devolver coordenadas basadas en objetos) .
Cuando agrega un objeto a un nivel. Hará lo siguiente:
1) Crear una estructura de ubicación:
Esto también podría ser una referencia a un objeto en motores de física de terceros. O podría ser un desplazamiento de coordenadas con una referencia a otra ubicación (para una cámara de seguimiento o un objeto adjunto o ejemplo). Con el polimorfismo, podría depender de si es un objeto estático o dinámico. Al mantener una referencia al índice espacial aquí cuando se actualizan las coordenadas, el índice espacial también puede estarlo.
Si le preocupa la asignación dinámica de memoria, use un grupo de memoria.
2) Un enlace / enlace entre su objeto, su ubicación y el gráfico de la escena.
3) El enlace se agrega al índice espacial dentro del nivel en el punto apropiado.
Cuando te estés preparando para renderizar.
1) Obtenga la cámara (solo será otro objeto, excepto que su ubicación rastreará al personaje del jugador y su renderizador tendrá una referencia especial, de hecho, eso es todo lo que realmente necesita).
2) Obtenga el enlace espacial de la cámara.
3) Obtenga el índice espacial del enlace.
4) Consultar los objetos que son (posiblemente) visibles para la cámara.
5A) Necesita que se procese la información visual. Texturas cargadas en la GPU y así sucesivamente. Esto se haría mejor de antemano (como en carga nivelada), pero tal vez podría hacerse en tiempo de ejecución (para un mundo abierto, podría cargar cosas cuando se está acercando a un fragmento, pero aún debe hacerse con anticipación).
5B) Opcionalmente, construya un árbol de renderizado en caché, si desea clasificar en profundidad / material o realizar un seguimiento de los objetos cercanos, podría ser visible en un momento posterior. De lo contrario, puede consultar el índice espacial cada vez que dependerá de sus requisitos de juego / rendimiento.
Es probable que su renderizador necesite un objeto RenderBinding que se vincule entre el objeto y las coordenadas.
Luego, cuando renderice, simplemente ejecute la lista.
He usado las referencias anteriores, pero podrían ser punteros inteligentes, punteros sin formato, identificadores de objetos, etc.
EDITAR:
En cuanto a hacer las cosas 'conscientes' el uno del otro. Esa es la detección de colisión. Probablemente se implementaría en octubre. Debería proporcionar alguna devolución de llamada en su objeto principal. Estas cosas se manejan mejor con un motor de física adecuado como Bullet. En ese caso, simplemente reemplace Octree con PhysicsScene y Position con un enlace a algo como CollisionMesh.getPosition ().
fuente