¿Por qué debería separar los objetos del renderizado?


11

Renuncia: Yo sé lo que un patrón de sistema entidad es y estoy no usarlo.

He leído mucho sobre separar objetos y renderizar. Sobre el hecho de que la lógica del juego debe ser independiente del motor de renderizado subyacente. Eso está muy bien y tiene mucho sentido, pero también causa muchos otros dolores:

  • necesidad de sincronización entre el objeto lógico y el objeto de representación (el que mantiene el estado de la animación, los sprites, etc.)
  • necesita abrir el objeto lógico al público para que el objeto de representación lea el estado real del objeto lógico (a menudo, lo que hace que el objeto lógico se transforme fácilmente en un objeto getter y setter tonto)

Esto no me parece una buena solución. Por otro lado, es muy intuitivo imaginar un objeto como su representación en 3D (o 2d) y también es muy fácil de mantener (y posiblemente también mucho más encapsulado).

¿Hay alguna manera de mantener la representación gráfica y la lógica del juego juntas (evitando problemas de sincronización) pero abstrayendo el motor de renderizado? ¿O hay una manera de separar la lógica del juego y el renderizado que no causa los inconvenientes anteriores?

(posiblemente con ejemplos, no soy muy bueno para entender conversaciones abstractas)


1
También sería útil si proporciona un ejemplo de lo que quiere decir cuando dice que no está utilizando el patrón del sistema de la entidad, y cómo cree que eso se relaciona con si debe o no separar la preocupación de renderizar de la entidad / lógica del juego
michael.bartnett

@ michael.bartnett, no estoy separando objetos en pequeños componentes reutilizables que son manejados por los sistemas, como lo hacen la mayoría de las implementaciones de los patrones. Mi código es más un intento por el patrón MVC. Pero en realidad no importa, ya que la pregunta no depende de ningún código (ni siquiera un idioma). Puse el descargo de responsabilidad porque sabía que algunos habrían tratado de convencerme de usar el ECS, que parece curar el cáncer. Y, como puede ver, sucedió de todos modos.
Zapato

Respuestas:


13

Supongamos que tiene una escena compuesta de un mundo , un jugador y un jefe. Ah, y este es un juego en tercera persona, así que también tienes una cámara .

Entonces su escena se ve así:

class Scene {
    World* world
    Player* player
    Enemy* boss
    Camera* camera
}

(Al menos, esos son los datos básicos . La forma en que contenga los datos depende de usted).

Solo quieres actualizar y renderizar la escena cuando estás jugando, no cuando estás en pausa o en el menú principal ... ¡así que lo adjuntas al estado del juego!

State* gameState = new State();
gameState->addScene(scene);

Ahora tu estado de juego tiene una escena. A continuación, desea ejecutar la lógica en la escena y renderizar la escena. Para la lógica, solo ejecuta una función de actualización.

State::update(double delta) {
    scene->update(delta);
}

De esa manera puedes mantener toda la lógica del juego en la Sceneclase. Y solo por el bien de referencia, un sistema de componentes de entidad podría hacerlo así:

State::update(double delta) {
    physicsSystem->applyPhysics(scene);
}

De todos modos, ahora has logrado actualizar tu escena. ¡Ahora quieres mostrarlo! Para lo cual hacemos algo similar a lo anterior:

State::render() {
    renderSystem->render(scene);
}

Ahí tienes. El renderSystem lee la información de la escena y muestra la imagen adecuada. Simplificado, el método para renderizar la escena podría verse así:

RenderSystem::renderScene(Scene* scene) {
    Camera* camera = scene->camera;
    lookAt(camera); // Set up the appropriate viewing matrices based on 
                    // the camera location and direction

    renderHeightmap(scene->getWorld()->getHeightMap()); // Just as an example, you might
                                                        // use a height map as your world
                                                        // representation.
    renderModel(scene->getPlayer()->getType()); // getType() will return, for example "orc"
                                                // or "human"

    renderModel(scene->getBoss()->getType());
}

Realmente simplificado, aún necesitarías, por ejemplo, aplicar una rotación y traducción en función de dónde está tu jugador y dónde está mirando. (Mi ejemplo es un juego en 3D, si vas con 2D, será un paseo por el parque).

Espero que esto sea lo que estabas buscando? Como se puede recordar de lo anterior, al sistema de render no le importa la lógica del juego . Solo usa el estado actual de la escena para renderizar, es decir, extrae la información necesaria para renderizar. ¿Y la lógica del juego? No le importa lo que haga el renderizador. ¡Diablos, no le importa si se muestra en absoluto!

Y tampoco necesita adjuntar información de representación a la escena. Debería ser suficiente que el renderizador sepa que necesita representar un orco. Ya habrá cargado un modelo orco, que el renderizador sabe mostrar.

Esto debería satisfacer sus requisitos. La representación gráfica y la lógica están acopladas , porque ambas usan los mismos datos. Sin embargo, están separados , ¡porque ninguno de los dos depende del otro!

EDITAR: ¿Y solo para responder por qué uno lo haría así? Porque es más fácil es la razón más simple. No necesita pensar en "tal y tal sucedió, ahora debería actualizar los gráficos". En cambio, haces que las cosas sucedan, y cada cuadro del juego mira lo que está sucediendo actualmente, y lo interpreta de alguna manera, dándote un resultado en pantalla.


7

Su título hace una pregunta diferente al contenido de su cuerpo. En el título, pregunta por qué la lógica y el renderizado deberían estar separados, pero en el cuerpo pregunta por implementaciones de sistemas de lógica / gráficos / renderizado.

La segunda pregunta se ha abordado anteriormente , por lo que me centraré en la primera pregunta.

Razones para separar la lógica y la representación:

  1. La idea generalizada de que los objetos deberían hacer una cosa
  2. ¿Qué pasa si quieres pasar de 2D a 3D? ¿Qué sucede si decide cambiar de un sistema de renderizado a otro en la mitad del proyecto? No querrás rastrear todo tu código y hacer grandes cambios en medio de la lógica de tu juego.
  3. Probablemente tenga razones para repetir secciones de código, lo que generalmente se considera una mala idea.
  4. Puede construir sistemas para controlar franjas potencialmente enormes de renderizado o lógica sin comunicarse individualmente con piezas pequeñas.
  5. ¿Qué pasa si quieres asignar una gema a un jugador pero el sistema se ralentiza por cuántas facetas tiene la gema? Si ha abstraído su sistema de renderizado lo suficientemente bien, puede actualizarlo a diferentes velocidades para tener en cuenta las costosas operaciones de renderizado.
  6. Te permite pensar en cosas que realmente importan a lo que estás haciendo. ¿Por qué enrollar su cerebro alrededor de transformaciones matriciales y compensaciones de sprites y coordenadas de pantalla cuando todo lo que quiere hacer es implementar una mecánica de doble salto, robar una carta o equipar una espada? No quieres que el sprite represente tu espada equipada en color rosa brillante solo porque querías moverlo de tu mano derecha a la izquierda.

En una configuración de OOP, crear instancias de nuevos objetos tiene un costo, pero en mi experiencia, el costo de los recursos del sistema es un pequeño precio a pagar por la capacidad de pensar e implementar las cosas específicas que necesito hacer.


6

Esta respuesta es solo para construir una intuición de por qué es importante separar la representación y la lógica, en lugar de sugerir directamente ejemplos prácticos.

Supongamos que tenemos un elefante grande , nadie en la habitación puede ver el elefante completo. tal vez todos no estén de acuerdo en lo que realmente es. Porque todos ven una parte diferente del elefante y solo pueden lidiar con esa parte. Pero al final esto no cambia el hecho de que es un gran elefante.

El elefante representa el objeto del juego con todos sus detalles. Pero en realidad nadie necesita saber todo sobre el elefante (objeto del juego) para poder hacer su funcionalidad.

Acoplando la lógica del juego y el renderizado es en realidad como hacer que todos vean al elefante completo. Si algo cambió, todos deben saberlo. Si bien en la mayoría de los casos solo necesitan ver la parte en la que solo están interesados. Si algo cambió a la persona que lo conoce, solo necesita decirle a la otra persona sobre el resultado de ese cambio, eso es lo que solo es importante para él (piense en esto como comunicación a través de mensajes o interfaces).

ingrese la descripción de la imagen aquí

Los puntos que mencionó no son inconvenientes, solo son inconvenientes si hubiera más dependencias de las que debería haber en el motor, en otras palabras, los sistemas ven partes del elefante más de lo que deberían. Y esto significa que el motor no fue diseñado "correctamente".

Solo necesita sincronización con su definición formal si está utilizando un motor de subprocesos múltiples donde coloca la lógica y la representación en dos subprocesos diferentes, e incluso un motor que necesita mucha sincronización entre sistemas no está especialmente diseñado.

De lo contrario, la forma natural de lidiar con este caso es diseñar el sistema como entrada / salida. La actualización hace la lógica y genera el resultado. El renderizado solo se alimenta con los resultados de la actualización. Realmente no necesitas exponer todo. Solo expone una interfaz que se comunica entre las dos etapas. La comunicación entre diferentes partes del motor debe ser a través de abstracciones (interfaces) y / o mensajes. No debe exponerse ninguna lógica interna o estados.

Tomemos un ejemplo de gráfico de escena simple para explicar la idea.

La actualización generalmente se realiza a través de un solo bucle llamado bucle de juego (o posiblemente a través de múltiples bucles de juego, cada uno de los cuales se ejecuta en un hilo separado). Una vez que el bucle se actualizó alguna vez objeto del juego Solo necesita decir a través de mensajes o interfaces que los objetos 1 y 2 se actualizaron y alimentarlo con la transformación final.

El sistema de representación solo toma la transformación final y no sabe qué cambió realmente sobre el objeto (por ejemplo, ocurrió una colisión específica, etc.). Ahora para renderizar ese objeto solo necesita la ID de ese objeto y la transformación final. Después de eso, el renderizador alimentará la API de renderizado con la malla y la transformación final sin saber nada más.

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.