Tácticas para mover la lógica de renderizado de la clase GameObject

10

Al crear juegos, a menudo crea el siguiente objeto de juego del que heredan todas las entidades:

public class GameObject{
    abstract void Update(...);
    abstract void Draw(...);
}

Entonces, al actualizar el ciclo, itera sobre todos los objetos del juego y les da la oportunidad de cambiar de estado, luego, en el siguiente ciclo de dibujo, itera nuevamente sobre todos los objetos del juego y les da la oportunidad de dibujar ellos mismos.

Aunque esto funciona bastante bien en un juego simple con un renderizador avanzado simple, a menudo conduce a algunos objetos gigantes del juego que necesitan almacenar sus modelos, texturas múltiples y, lo peor de todo, un método de dibujo gordo que crea un acoplamiento estrecho entre el objeto del juego, la estrategia de renderizado actual y cualquier clase de renderizado relacionada

Si tuviera que cambiar la estrategia de renderizado de adelante a diferido, tendría que actualizar muchos objetos del juego. Y los objetos del juego que hago no son tan reutilizables como podrían ser. Por supuesto, la herencia y / o la composición pueden ayudarme a combatir la duplicación de código y hacer que sea un poco más fácil cambiar la implementación, pero aún me parece que falta.

Una mejor manera, tal vez, sería eliminar el método Draw de la clase GameObject por completo y crear una clase Renderer. GameObject aún necesitaría contener algunos datos sobre sus imágenes, como con qué modelo representarlo y qué texturas se deben pintar en el modelo, pero cómo se hace esto se dejaría al renderizador. Sin embargo, a menudo hay muchos casos de borde en el renderizado, por lo que, aunque esto eliminaría el acoplamiento estrecho del GameObject con el Renderer, el Renderer aún tendría que estar al tanto de todos los objetos del juego que lo engordarían, todo el conocimiento y estrechamente acoplado. Esto violaría algunas buenas prácticas. Tal vez el diseño orientado a datos podría hacer el truco. Los objetos del juego sin duda serían datos, pero ¿cómo sería impulsado el renderizador por esto? No estoy seguro.

Así que estoy perdido y no puedo pensar en una buena solución. Intenté usar los principios de MVC y en el pasado tuve algunas ideas sobre cómo usar eso en los juegos, pero recientemente no parece tan aplicable como pensaba. Me encantaría saber cómo abordan este problema.

De todos modos, vamos a recapitular, estoy interesado en cómo se pueden lograr los siguientes objetivos de diseño.

  • Sin lógica de renderizado en el objeto del juego
  • Acoplamiento flojo entre los objetos del juego y el motor de render
  • No todo el renders conocedor
  • Preferiblemente, el tiempo de ejecución cambia entre motores de render

La configuración ideal del proyecto sería una 'lógica de juego' separada y un proyecto de lógica de renderizado que no necesitan referenciarse entre sí.

Todo este tren de pensamiento comenzó cuando escuché a John Carmack decir en Twitter que tiene un sistema tan flexible que puede cambiar los motores de render en tiempo de ejecución e incluso puede decirle a su sistema que use ambos renderizadores (un procesador de software y un procesador acelerado por hardware) al mismo tiempo para que pueda inspeccionar las diferencias. Los sistemas que he programado hasta ahora ni siquiera son tan flexibles

Roy T.
fuente

Respuestas:

7

Un primer paso rápido para desacoplar:

Los objetos del juego hacen referencia a un identificador de cuáles son sus imágenes pero no los datos, digamos algo simple como una cadena. Ejemplo: "human_male"

Renderer es responsable de cargar y mantener referencias "human_male" y de devolver a los objetos un identificador para usar.

Ejemplo en pseudocódigo horrible:

GameObject( initialization parameters )
  me.render_handle = Renderer_Create( parameters.render_string )

- elsewhere
Renderer_Create( string )

  new data handle = Resources_Load( string );
  return new data handle

- some time later
GameObject( something happens to me parameters )
  me.state = something.what_happens
  Renderer_ApplyState( me.render_handle, me.state.effect_type )

- some time later
Renderer_Render()
  for each renderable thing
    for each rendering back end
        setup graphics for thing.effect
        render it

- finally
GameObject_Destroy()
  Renderer_Destroy( me.render_handle )

Perdón por ese lío, de todos modos tus condiciones se cumplen con ese simple cambio de OOP puro basado en mirar cosas como objetos del mundo real y en OOP basado en responsabilidades.

  • No hay lógica de representación en el objeto del juego (hecho, todo lo que el objeto sabe es un controlador para que pueda aplicar efectos a sí mismo)
  • Acoplamiento flojo entre los objetos del juego y el motor de renderizado (hecho, todo contacto es a través de un controlador abstracto, estados que se pueden aplicar y no qué hacer con esos estados)
  • No todo el renderizador conocedor (hecho, solo sabe de sí mismo)
  • Preferiblemente, el tiempo de ejecución cambia entre motores de renderizado (esto se hace en la etapa Renderer_Render (), sin embargo, debe escribir ambos extremos)

Las palabras clave que puede buscar para ir más allá de una simple refactorización de clases serían "sistema de entidad / componente" e "inyección de dependencia" y potencialmente patrones de GUI "MVC" solo para hacer girar los viejos engranajes cerebrales.

Patrick Hughes
fuente
Esto es extremadamente diferente de lo que haya hecho antes, parece que tiene bastante potencial. Afortunadamente, no estoy limitado por ningún motor existente, así que puedo jugar. También buscaré los términos que mencionó, aunque la inyección de dependencia siempre me duele el cerebro: P.
Roy T.
2

Lo que hice para mi propio motor es agrupar todo en módulos. Entonces tengo mi GameObjectclase y tiene un control para:

  • ModuleSprite - dibujo de sprites
  • ModuleWeapon - armas de fuego
  • ModuleScriptingBase - scripting
  • ModuleParticles - efectos de partículas
  • ModuleCollision - detección y respuesta de colisión

Entonces tengo una Playerclase y una Bulletclase. Ambos derivan GameObjecty se agregan a Scene. Pero Playertiene los siguientes módulos:

  • ModuleSprite
  • ModuleWeapon
  • MóduloPartículas
  • Módulo de colisión

Y Bullettiene estos módulos:

  • ModuleSprite
  • Módulo de colisión

Esta forma de organizar las cosas evita el "Diamante de la Muerte" donde tienes un Vehicle, a VehicleLandy a VehicleWatery ahora quieres un VehicleAmphibious. En cambio, tienes un Vehicley puede tener un ModuleWatery un ModuleLand.

Bonificación adicional: puede crear objetos utilizando un conjunto de propiedades. Todo lo que tiene que saber es el tipo de base (Player, Enemy, Bullet, etc.) y luego crear identificadores para los módulos que necesita para ese tipo.

En mi escena, hago lo siguiente:

  • Llame al Updatepara todas las GameObjectmanijas.
  • Haga una verificación de colisión y una respuesta de colisión para aquellos que tienen un ModuleCollisionidentificador.
  • Llame al UpdatePostpara todas las GameObjectmanijas para informar sobre su posición final después de la física.
  • Destruye los objetos que tienen su bandera establecida.
  • Agregue nuevos objetos de la m_ObjectsCreatedlista a la m_Objectslista.

Y podría organizarlo más: por módulos en lugar de por objeto. Luego renderizaría una lista de ModuleSprite, actualizaría un montón ModuleScriptingBasey colisionaría con una lista de ModuleCollision.

caballero666
fuente
Suena como composición al máximo! Muy agradable. Sin embargo, no veo muchos consejos de representación específicos aquí. ¿Cómo manejas eso, simplemente agregando diferentes módulos?
Roy T.
Oh si. Esa es la desventaja de este sistema: si tiene un requisito específico para GameObject(por ejemplo, una forma de representar una "serpiente" de Sprites), deberá crear un hijo ModuleSpritepara esa funcionalidad específica ( ModuleSpriteSnake) o agregar un módulo nuevo por completo ( ModuleSnake) Afortunadamente, solo son punteros, pero he visto código donde GameObjectliteralmente hizo todo lo que un objeto podía hacer.
knight666