¿Cuáles son algunas formas de separar la lógica del juego de las animaciones y el ciclo de dibujo?

9

Solo he hecho juegos flash anteriormente, usando MovieClips y demás para separar mis animaciones de la lógica de mi juego. Ahora estoy empezando a intentar hacer un juego para Android, pero la teoría de la programación del juego sobre la separación de estas cosas todavía me confunde. Vengo de un entorno de desarrollo de aplicaciones web que no son de juegos, por lo que conozco más patrones similares a MVC y estoy atrapado en esa mentalidad a medida que me acerco a la programación de juegos.

Quiero hacer cosas como abstraer mi juego teniendo, por ejemplo, una clase de tablero de juego que contenga los datos para una cuadrícula de mosaicos con instancias de una clase de mosaico que contenga propiedades. Puedo darle acceso a mi bucle de dibujo a esto y hacer que dibuje el tablero de juego en función de las propiedades de cada mosaico en el tablero de juego, pero no entiendo a dónde debe ir exactamente la animación. Por lo que puedo decir, la animación se ubica entre la lógica abstraída del juego (modelo) y el ciclo de dibujo (vista). Con mi mentalidad MVC, es frustrante tratar de decidir a dónde se supone que debe ir la animación. Tendría una gran cantidad de datos asociados como un modelo, pero aparentemente necesita estar muy estrechamente asociado con el ciclo de dibujo para tener cosas como la animación independiente del cuadro.

¿Cómo puedo salir de esta mentalidad y comenzar a pensar en patrones que tengan más sentido para los juegos?

TMV
fuente

Respuestas:

6

La animación aún se puede dividir perfectamente entre lógica y renderizado. El estado de los datos abstractos de la animación sería la información necesaria para que su API de gráficos represente la animación.

En los juegos 2D, por ejemplo, podría ser un área rectangular que marca el área que muestra la parte actual de la hoja de sprites que debe dibujarse (cuando tiene una hoja que consta de 30 dibujos de 80x80 que contienen los diversos pasos de su personaje saltar, sentarse, moverse, etc.). También puede ser cualquier tipo de datos que no necesite para renderizar, pero tal vez para administrar los estados de animación, como el tiempo restante hasta que caduque el paso actual de la animación o el nombre de la animación ("caminar", "estar de pie" etc.) Todo eso se puede representar de la forma que desee. Esa es la parte de la lógica.

En la parte de renderizado, simplemente hazlo como de costumbre, obtén ese rectángulo de tu modelo y usa tu renderizador para hacer las llamadas a la API de gráficos.

En código (usando la sintaxis de C ++ aquí):

class Sprite //Model
{
    private:
       Rectangle subrect;
       Vector2f position;
       //etc.

    public:
       Rectangle GetSubrect() 
       {
           return subrect;
       }
       //etc.
};

class AnimatedSprite : public Sprite, public Updatable //arbitrary interface for classes that need to change their state on a regular basis
{
    AnimationController animation_controller;
    //etc.
    public:
        void Update()
        {
            animation_controller.Update(); //Good OOP design ;) It will take control of changing animations in time etc. for you
            this.SetSubrect(animation_controller.GetCurrentAnimation().GetRect());
        }
        //etc.
};

Esa es la información. Su procesador tomará esos datos y los dibujará. Dado que tanto los Sprites normales como los animados se dibujan de la misma manera, ¡puedes usar polimorfía aquí!

class Renderer
{
    //etc.
    public:
       void Draw(const Sprite &spr)
       {
           graphics_api_pointer->Draw(spr.GetAllTheDataThatINeed());
       }
};

TMV:

Se me ocurrió otro ejemplo. Digamos que tienes un juego de rol. Su modelo que representa el mapa mundial, por ejemplo, probablemente necesite almacenar la posición del personaje en el mundo como coordenadas de mosaico en el mapa. Sin embargo, cuando mueves el personaje, caminan unos pocos píxeles a la vez hasta el siguiente cuadrado. ¿Almacena esta posición "entre mosaicos" en un objeto de animación? ¿Cómo se actualiza el modelo cuando el personaje finalmente "llegó" a la siguiente coordenada del mapa?

El mapa mundial no conoce la posición de los jugadores directamente (no tiene un Vector2f o algo así que almacena directamente la posición de los jugadores =, en cambio, tiene una referencia directa al objeto del jugador en sí, que a su vez deriva de AnimatedSprite para que pueda pasarlo fácilmente al renderizador y obtener todos los datos necesarios.

Sin embargo, en general, su mapa de mosaicos no debería ser capaz de hacer todo: tendría una clase "TileMap" que se encarga de administrar todos los mosaicos, y tal vez también detecte colisiones entre los objetos que le entregue y Los azulejos en el mapa. Entonces, tendría otra clase "RPGMap", o como quiera llamarlo, que tiene tanto una referencia a su mapa de mosaico como a la referencia al jugador y hace que la actualización () real llame a su jugador y a su mapa de mosaico.

La forma en que desea actualizar el modelo cuando el jugador se mueve depende de lo que quiera hacer.

¿Tu jugador puede moverse entre fichas independientemente (estilo Zelda)? Simplemente maneje la entrada y mueva el reproductor en consecuencia en cada cuadro. ¿O quieres que el jugador presione "derecha" y tu personaje mueve automáticamente una ficha a la derecha? Deje que su clase RPGMap interpole la posición de los jugadores hasta que llegue a su destino y, mientras tanto, bloquee todo el manejo de entrada de la tecla de movimiento.

De cualquier manera, si desea que sea más fácil para usted, todos sus modelos tendrán métodos Update () si realmente necesitan algo de lógica para actualizarse (en lugar de simplemente cambiar los valores de las variables): no regala el controlador en el patrón MVC de esa manera, simplemente mueve el código de "un paso arriba" (el controlador) hacia el modelo, y todo lo que hace el controlador es llamar a este método Update () del modelo (El controlador en nuestro caso sería el RPGMap). Todavía puede cambiar fácilmente el código lógico: puede cambiar directamente el código de la clase o, si necesita un comportamiento completamente diferente, puede derivar de su clase de modelo y solo anular el método Update ().

Ese enfoque reduce las llamadas a métodos y cosas por el estilo, que solía ser una de las principales desventajas del patrón MVC puro (terminas llamando a GetThis () GetThat () muy a menudo): hace que el código sea más largo y un poco más difícil de leer y también más lento, a pesar de que su compilador podría encargarse de eso, ya que optimiza muchas cosas como esa.

TravisG
fuente
¿Mantendría los datos de animación en la clase que contiene la lógica del juego, la clase que contiene el bucle del juego o separada de ambos? Además, depende completamente del bucle o de la clase que contiene el bucle comprender cómo traducir los datos de animación en dibujar la pantalla, ¿verdad? A menudo no sería tan simple como obtener un rect que representa una sección de la hoja de sprites y usarlo para recortar un dibujo de mapa de bits de la hoja de sprites.
TMV
Se me ocurrió otro ejemplo. Digamos que tienes un juego de rol. Su modelo que representa el mapa mundial, por ejemplo, probablemente necesite almacenar la posición del personaje en el mundo como coordenadas de mosaico en el mapa. Sin embargo, cuando mueves el personaje, caminan unos pocos píxeles a la vez hasta el siguiente cuadrado. ¿Almacena esta posición "entre mosaicos" en un objeto de animación? ¿Cómo se actualiza el modelo cuando el personaje finalmente "llegó" a la siguiente coordenada del mapa?
TMV
Edité la respuesta a su pregunta, ya que los comentarios no permiten suficientes caracteres para eso.
TravisG
Si entiendo todo correctamente:
TMV
Podría tener una instancia de una clase "Animador" dentro de su Vista, y tendría un método público de "actualización" que la vista llama cada cuadro. El método de actualización llama a los métodos de "actualización" de instancias de varios tipos de objetos de animación individuales dentro de él. El animador y las animaciones dentro de él tienen una referencia al Modelo (transmitido a través de sus constructores) para que puedan actualizar los datos del modelo si una animación lo cambiara. Luego, en el ciclo de dibujo, obtienes datos de las animaciones dentro del animador de una manera que la Vista puede entender y dibujar.
TMV
2

Puedo desarrollar esto si lo desea, pero tengo un procesador central que me dice que dibuje en el bucle. Más bien que

handle input

for every entity:
    update entity

for every entity:
    draw entity

Tengo un sistema mas como

handle input (well, update the state. Mine is event driven so this is null)

for every entity:
    update entity //still got game logic here

renderer.draw();

La clase de renderizador simplemente contiene una lista de referencias a componentes dibujables de objetos. Estos se asignan en los constructores por simplicidad.

Para su ejemplo, tendría una clase de GameBoard con varios Tiles. Cada mosaico obviamente conoce su posición, y supongo algún tipo de animación. Factorice eso en algún tipo de clase de animación que posea el mosaico, y haga que pase una referencia de sí mismo a una clase Renderer. Allí, todos separados. Cuando actualiza Tile, llama a Update en la animación ... o la actualiza por sí misma. Cuando Renderer.Draw()se llama, dibuja la animación.

La animación independiente del fotograma no debería tener mucho que ver con el ciclo de dibujo.

El pato comunista
fuente
0

Últimamente he estado aprendiendo los paradigmas, así que si esta respuesta es incompleta, estoy seguro de que alguien la agregará.

La metodología que parece tener más sentido para el diseño del juego es separar la lógica de la salida de la pantalla.

En la mayoría de los casos, desearía utilizar un enfoque de subprocesos múltiples, si no está familiarizado con ese tema, es una pregunta propia, aquí está el manual de wiki . Esencialmente, desea que la lógica de su juego se ejecute en un hilo, bloqueando las variables a las que necesita acceder para garantizar la integridad de los datos. Si su bucle lógico es increíblemente rápido (¿super mega animada 3d pong?), Puede intentar arreglar la frecuencia que ejecuta el bucle durmiendo el hilo durante pequeñas duraciones (se han sugerido 120 hz en este foro para bucles de física de juegos). Al mismo tiempo, el otro hilo vuelve a dibujar la pantalla (se han sugerido 60 hz en otros temas) con las variables actualizadas, nuevamente solicitando un bloqueo en las variables antes de acceder a ellas.

En ese caso, las animaciones o transiciones, etc., entran en el hilo de dibujo, pero debe indicar a través de una bandera de algún tipo (una variable de estado global tal vez) que el hilo lógico del juego no debe hacer nada (o hacer algo diferente ... configurar el nuevos parámetros del mapa tal vez).

Una vez que entiendes la concurrencia, el resto es bastante comprensible. Si no tiene experiencia con la concurrencia, le sugiero que escriba algunos programas de prueba simples para que pueda entender cómo sucede el flujo.

Espero que esto ayude :)

[editar] En los sistemas que no admiten subprocesos múltiples, la animación aún puede entrar en el ciclo de dibujo, pero desea establecer el estado de tal manera que indique a la lógica que está ocurriendo algo diferente y no siga procesando el nivel actual / mapa / etc ...

Stephen
fuente
1
No estoy de acuerdo aquí. En la mayoría de los casos, no querrás hacer varios subprocesos, especialmente si se trata de un juego pequeño.
El pato comunista
@TheCommunistDuck Bastante justo, los gastos generales y la complejidad para subprocesos múltiples pueden ser definitivamente excesivos, además, si el juego es pequeño, debería poder actualizarse rápidamente.
Stephen