¿Cómo deben ser conscientes los objetos del juego?


18

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 GameObjectque 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 updatemé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_castpuede 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?


1
Creo que está buscando una implementación personalizada de C ++ RTTI versátil. Sin embargo, su pregunta no parece ser solo sobre mecanismos RTTI juiciosos. Casi todos los middleware que usará el juego requieren lo que pides (sistema de animación, física, por nombrar algunos). Dependiendo de la lista de consultas admitidas, puede hacer trampa en RTTI utilizando ID e índices en matrices, o terminará diseñando un protocolo completo para admitir alternativas más económicas a dynamic_cast y type_info.
Teodron

Aconsejaría no usar el sistema de tipos para la lógica del juego. Por ejemplo, en lugar de depender del resultado de dynamic_cast<Human*>, implemente algo como a bool GameObject::IsHuman(), que regresa falsepor defecto pero se anula para regresar trueen la Humanclase.
congusbongus

un extra: casi nunca envías una tonelada de objetos entre sí que podrían estar interesados ​​en ellos Esa es una optimización obvia que tendrá que considerar realmente.
teodron el

@congusbongus El uso de una IsAanulació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.
teodron

44
@Jefffrey: idealmente no escribes código específico de tipo. Usted escribe un código específico de la interfaz ("interfaz" en el sentido general). Su lógica para un TeamASoldiery TeamBSoldieres realmente idéntica: disparó a cualquiera en el otro equipo. Todo lo que necesita de otras entidades es un GetTeam()método en su forma más específica y, por el ejemplo de congusbongus, que pueda resumirse aún más en una IsEnemyOf(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.
Sean Middleditch

Respuestas:


11

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.


1
Probablemente, esto también se conoce como el "sistema" que administra la lógica de ciertos tipos de componentes en una arquitectura de entidad-componente.
teodron el

Eso suena como una solución de estilo C. Los componentes se agrupan en std::mapsy 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?
Zapato

1
@Jefffrey "Eso suena como una solución de estilo C" - incluso cuando eso sería cierto, ¿por qué necesariamente sería algo malo? Las otras preocupaciones pueden ser válidas, pero hay soluciones para ellas. Lamentablemente, un comentario es demasiado corto para abordar cada uno de ellos correctamente.
Philipp

1
@Jefffrey Usar el enfoque en el que los componentes en sí mismos no tienen lógica y los "sistemas" son responsables de manejar toda la lógica no crea dependencias entre componentes ni requiere un sistema de mensajería súper complejo (al menos, no tan complejo) . Ver por ejemplo: gamadu.com/artemis/tutorial.html

1

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.


1

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/


1

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.


1

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:

 class Location { 
     float x, y, z; // Or a special Coordinates class, or a vec3 or whatever.
     SpacialIndex& spacialIndex; // Note this could be the area/level/map/whatever here
 };

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.

typedef std::pair<Object, Location> SpacialBinding.

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.

class RenderBinding {
    Object& object;
    RenderInformation& renderInfo;
    Location& location // This could just be a coordinates class.
}

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:

class Game {
    weak_ptr<Camera> camera;
    Level level1;

    void init() {
        Camera camera(75.0_deg, 1.025_ratio, 1000_meters);
        auto template_player = loadObject("Player.json")
        auto player = level1.addObject(move(player), Position(1.0, 2.0, 3.0));
        level1.addObject(move(camera), getRelativePosition(player));

        auto template_bad_guy = loadObject("BadGuy.json")
        level1.addObject(template_bad_guy, {10, 10, 20});
        level1.addObject(template_bad_guy, {10, 30, 20});
        level1.addObject(move(template_bad_guy), {50, 30, 20});
    }

    void render() {
        camera->getFrustrum();
        auto level = camera->getLocation()->getLevel();
        auto object = level.getVisible(camera);
        for(object : objects) {
            render(objects);
        }
    }

    void render(Object& object) {
        auto ri = object.getRenderInfo();
        renderVBO(ri.getVBO());
    }

    Object loadObject(string file) {
        Object object;
        // Load file from disk and set the properties
        // Upload mesh data, textures to GPU. Load shaders whatever.
        object.setHitPoints(// values from file);
        object.setRenderInfo(// data from 3D api);
    }
}

class Level {
    Octree octree;
    vector<ObjectPtr> objects;
    // NOTE: If your level is mesh based there might also be a BSP here. Or a hightmap for an openworld
    // There could also be a physics scene here.
    ObjectPtr addObject(Object&& object, Position& pos) {
        Location location(pos, level, object);
        objects.emplace_back(object);
        object->setLocation(location)
        return octree.addObject(location);
    }
    vector<Object> getVisible(Camera& camera) {
        auto f = camera.getFtrustrum();
        return octree.getObjectsInFrustrum(f);
    }
    void updatePosition(LocationPtr l) {
        octree->updatePosition(l);
    }
}

class Octree {
    OctreeNode root_node;
    ObjectPtr add(Location&& object) {
        return root_node.add(location);
    }
    vector<ObjectPtr> getObjectsInRadius(const vec3& position, const float& radius) { // pass to root_node };
    vector<ObjectPtr> getObjectsinFrustrum(const FrustrumShape frustrum;) {//...}
    void updatePosition(LocationPtr* l) {
        // Walk up from l.octree_node until you reach the new place
        // Check if objects are colliding
        // l.object.CollidedWith(other)
    }
}

class Object {
    Location location;
    RenderInfo render_info;
    Properties object_props;
    Position getPosition() { return getLocation().position; }
    Location getLocation() { return location; }
    void collidedWith(ObjectPtr other) {
        // if other.isPickup() && object.needs(other.pickupType()) pick it up, play sound whatever
    }
}

class Location {
    Position position;
    LevelPtr level;
    ObjectPtr object;
    OctreeNote octree_node;
    setPosition(Position position) {
        position = position;
        level.updatePosition(this);
    }
}

class Position {
    vec3 coordinates;
    vec3 rotation;
}

class RenderInfo {
    AnimationState anim;
}
class RenderInfo_OpenGL : public RenderInfo {
    GLuint vbo_object;
    GLuint texture_object;
    GLuint shader_object;
}

class Camera: public Object {
    Degrees fov;
    Ratio aspect;
    Meters draw_distance;
    Frustrum getFrustrum() {
        // Use above to make a skewed frustum box
    }
}

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 ().


Wow, esto se ve muy bien. Creo que he entendido la idea básica, pero sin más ejemplos, no puedo obtener una visión externa de esto. ¿Tienes más referencias o ejemplos en vivo sobre esto? (Mientras tanto, seguiré leyendo esta respuesta).
Zapato

Realmente no tengo ningún ejemplo, es justo lo que planeo hacer cuando tenga tiempo. Agregaré algunas más de las clases generales y veré si eso ayuda. Hay esto y esto . se trata más de clases de objetos que de cómo se relacionan o el renderizado. Como no lo he implementado yo mismo, puede haber dificultades, bits que deben resolverse o cosas de rendimiento, pero creo que la estructura general está bien.
David C. Bishop el
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.