viernes, 4 de diciembre de 2009

MVC en la creación de videojuegos II - Esquema general

En el post anterior expliqué la razón que tuve para utilizar MVC en mi proyecto de inteligencia artificial. Pero si alguien leyó el post completo, seguramente el final fue anticlimático, por no explicarse nada concreto. En este post me encargo de dar una vista a ojo de pájaro de como implementé un sistema MVC. La idea me surgió gracias al tutorial de sjbrown para escribir juegos en pygame (en ingles), por lo que hay varias similitudes. Sin embargo esa guia es un tutorial básico de pygame, que no implementa mucho mas que lo mínimo y necesario para entender el sistema MVC con Mediador. Como expliqué en el anterior post, la idea es hacer un sistema para implementar algoritmos de inteligencia artificial (IA), pero siempre con la creación de videojuegos en mente.

La primer versión de mi programa se pude representar esquemáticamente de la siguiente manera:



Esquema MVC general para un sistema de videojuegos. (click para agrandar)

El Modelo
La parte mas importante de este programa es el modelo, por ser (junto a la IA) la razón de la existencia de todo el programa. Esencialmente es una simulación física. Dentro de lo que son las simulaciones físicas, este modelo es por el momento un simulador de dinámica molecular. Básicamente un conjunto de partículas puntuales que tienen posición, velocidad y fuerzas. Hay varias formas de implementar esto, y para muchas partículas es uno de los puntos críticos de eficiencia del sistema.
La función del modelo es actualizar las posiciones y velocidades de todas las partículas en cada frame. También permite acceder agregar, quitar y acceder a las partículas y aplicarle fuerzas.
Por el momento, el modelo es 2D. Pero puede ser fácilmente adaptado a 3D.
Con este modelo se pueden implementar juegos de varios tipos: Shooters, Tower Defense, Pacman, FPSs a la antigua como el Wolfenstein y sidescrollers. Pero en general va a ser necesaria una abstracción del concepto de "Mapa" y mas adelante de "cuerpo rígido" que incluya la posibilidad de colisiones para realizar juegos con física mas avanzada.

La Vista
La vista es una representación del modelo. Actualmente hay dos vistas, una vista gráfica y una vista que solamente almacena los valores del modelo en un archivo de texto. La primera es la pantalla gráfica que todos conocemos y la segunda es utilizada para hacer un análisis mas detallado del modelo y su correcto funcionamiento.
En cada cuadro, la representación gráfica se encarga de obtener directamente del modelo la información necesaria para actualizar la pantalla. Por una razón de eficiencia, la vista accede al modelo directamente.
La vista tiene también una "cámara" que abstrae las coordenadas del modelo de las coordenadas de la pantalla. La cámara es una simple transformación de rotación, translación y escala que convierte de las coordenadas del modelo a las coordenadas de la pantallas. La cámara puede ser manipulada en tiempo real para hacer zoom, rotaciones y translaciones.
La vista actual es 2D, implementada en pygame, por ser lo mas simple. Manteniendo la misma interfaz, se puede realizar una implementación en Java o incluso una implementación 2D+1 (2D isometrica con una tercer dimensión falsa) utilizando pygame mismo o mejor aun python-ogre u otra librería 3D para python.
Actualmente, debido a que pygame posee una implementación, la colisión entre objetos se calcula en la vista. En el futuro esta funcionalidad se va a pasar al modelo, agregando el concepto de "forma" al mismo. La colisión solo se usa por el momento para funciones del mouse y otros controladores. No hay colisiones estrictas (o sea, las partículas pueden superponerse).

El Procesador de Eventos
El procesador de eventos es una implementación del patrón de diseño Mediador. Este objeto es un mensajero que media en la comunicación entre todas las partes del sistema. Es muy importante para generar una aplicación modular y de comportamiento altamente configurable. Este patrón de diseño se suele utilizar por ejemplo para programar GUIs, donde uno crea widgets que generan eventos al ser activados.
Utilizando el procesador de eventos, se pueden registrar tipo de eventos, que son asociados a funciones que "escuchan" estos eventos. Durante la ejecución del programa, los distintos componentes del programa publican eventos en el procesador de eventos, el procesador de eventos se encarga de distribuir estos eventos a las distintas funciones registradas.
Por ejemplo, se puede asociar una función que termina el programa para que se ejecute cuando se publique un evento "Tecla escape apretada", durante la ejecución del programa; cuando el usuario presione la tecla escape, el controlador de teclado publicará el evento en el procesador de eventos y el programa terminará. La ventaja de este esquema es que el teclado no tiene que conocer ningún detalle del resto de los componentes, ni quién escucha el componente. Por otro lado, se puede cambiar el comportamiento del programa fácilmente sin afectar los componentes involucrados, simplemente cambiando qué objeto escucha qué tipo de eventos.
La implementación básica del procesador de eventos no distingue entre tipos de eventos (todos los componentes escuchan todos los eventos). Mi implementación agrega "tipos de eventos". En el futuro tengo planeado implementar el concepto de prioridad y orden de eventos (el orden en que se llama a las AI puede ser importante, como descubrí recientemente).

Los controladores
Los controladores son objetos que manipulan el modelo. En un diseño general del patrón MVC un controlador puede acceder a la vista y al modelo directamente. En la práctica depende del controlador. Yo intenté mantener los acoples directos entre MVC al mínimo necesario, utilizando el Procesador de Eventos donde fuera posible.

El mouse y el teclado
Los controladores obvios son el mouse y el teclado. El mouse es por el momento el único objeto que requiere acceder a la vista en forma directa. Mas allá de eso, ambos controladores son completamente basados en eventos. En la sección de Procesador de Eventos explico las funciones.
En cada update del mouse, el objeto lee la posición del mouse y el estado de los botones y de haber alguna actualización, envía un mensaje utilizando el Procesador de Eventos. El mouse recibe del sistema operativo, posiciones en coordenadas de la pantalla, por esta razón se necesita el View para convertir estas coordenadas a coordenadas del modelo.El mouse emite eventos cuando se presiona un botón, cuando se lo deja de presionar y cuando se mueve el mouse. Cada botón tiene un tipo de evento distinto, así hay eventos "BUTTON_1_DOWN", "BUTTON_1_UP", "BUTTON_2_DOWN", etc.
El teclado es puramente basado en eventos. Genera mensajes indicando si se ha presionado/soltado una tecla en particular del mouse. Se emiten mensajes cuando se presiona una tecla y cuando se la suelta. En el mismo mensaje se indica si hay teclas modificadoras (ctrl, shift, alt) presionadas. Cada combinación de teclas tiene un tipo de evento distinto.

La inteligencia artificial
Este objeto es una abstracción del "cerebro" de las entidades del modelo. No es obvia la razón por la que la IA es un controlador en lugar de ser parte del modelo. Se puede pensar que la IA es parte de las reglas del modelo. Así como en el modelo las fuerzas modifican en forma determinista las posiciones de las partículas, uno puede pensar que la inteligencia artificial es una regla más, que modifica el modelo en forma determinista. Esta forma de ver la IA es obvia en reglas simples como los Steering Algorithms (que voy a explicar en futuros posts). Pero a medida que uno usa técnicas de IA mas elaboradas (redes neuronales, partículas), esta forma de ver las cosas se torna menos obvia. Aun mas, separar la IA del modelo es atractivo por varias razones:
  1. Facilita simular el desconocimiento que tiene una IA del mundo, si la IA fuera parte del modelo sería muy tentador hacer IAs omniscientes. 
  2. La IA se maneja en escalas temporales distintas al modelo. Por un lado, es útil que el modelo se actualice con la mayor frecuencia posible, por ejemplo 60 cuadros por segundo. Es fácil darse cuenta que una persona no tiene reacciones inmediatas, si un enemigo aparece en el rango de visión, la persona tardará en reaccionar (por ejemplo dispararle). Es sumamente atractivo transladar esta limitación a las IA, para ponerlas en igualdad de condiciones. En lugar de llamar al update de las IA 30 veces por segundo se puede llamar 10 veces por segundo, simulando un retardo en las reacciones de la IA similar al de un humano.
  3. Una misma IA puede actuar con los mismos algoritmos sobre distintos modelos. Asi, si en el futuro hago un modelo 3D, las IA pueden permanecer (en muchos casos, pero no todos) sin cambio y aun ser útiles.
  4. La IA no es tan prioritaria como el modelo. Se pueden aplicar técnicas avanzadas sobre las IA para balanceo de carga del CPU. [1] Ya que la evaluación de las IA no es prioritaria en ciertos contextos.
Por una razón de eficiencia, la IA posee acceso directo al modelo.
Para explicar el esquema MVC mas fácilmente supuse que hay una sola IA en este post. En realidad hay muchos tipos de IA implementadas en mi proyecto (10 al momento de escribir esto), todas pertenecientes a la categoría "Steering Algorithms". En un post futuro voy a explicar en detalle eso.

El reloj de sistema
Este objeto es el "corazón" del programa. Es un objeto muy simple que solamente se encarga de generar eventos indicando un nuevo frame del programa. Es una abstracción ingeniosa del bucle principal del programa. En lugar de hacer un bucle infinito en el cual se llama a cada objeto del programa, se generan eventos a una frecuencia dada, que luego son distribuidos por el procesador de eventos entre los distintos componentes que requieren un update periódico. Para inicial el programa, se ejecuta el reloj de sistema. Para pausar el programa, se detiene el reloj.
En la implementación actual del programa el reloj de sistema emite un mensaje "TICK" en cada frame del programa, a un ritmo predefinido. En próximas implementaciones, el reloj de sistema va a tener distintos tipos de TICK para distintas frecuencias, permitiendo actualizar distintos componentes del sistema a distintas frecuencias. Así será posible actualizar, por ejemplo, el modelo a 40 FPS, el view a 30FPS y los controllers a 10FPS.

Conclusión
Como dije al principio, esta es una vista general del programa que estamos programando. El código se puede descargar del repositorio SVN público.
En próximos posts voy a explicar mas concretamente varios puntos que no necesariamente están relacionados con el esquema MVC. Voy a explicar las bases de una simulación física de dinámica molecular, incluyendo algunos algoritmos concretos, voy a explicar también qué es un Steering Algorithm. Volviendo al tema MVC también voy a explicar algunos problemas que encontré en las implementaciones iniciales y como fuí solucionandolos.


[1] "An Efficient AI Architecture Using Prioritized Task Categories" Alex W. McLean - AI Game Programming Wisdom 1. Capitulo 6-3 p290 [volver]

5 comentarios:

  1. Me agrada encontrar a alguien de habla hispana haciendo un análisis sobre MVC en juegos.

    Tengo una pregunta:

    Imaginate que tenes una clase CollisionDetector.
    Que se encarga de detectar colisiones entre partículas.
    Esta clase tiene un método CollisionDector::Detect()

    Que devuelve una lista 0..* de CollisionInfo donde hay información sobre pares de partículas que han colisionado.

    Ahora... imaginate que quiero que una partícula cambie su color a Rojo, cuando se produce la colisión.

    La vista debería conocer al CollisionDetector y recorrer la lista de CollisionInfo para poder determinar las partículas que deben cambiar su color?

    Si mi vista general WorldView, tiene una colección de ParticleView. Donde cada ParticleView conoce a su model (Particle).
    Si recorro la lista de CollisionInfo, voy a obtener instancias de Particle, sin embargo no voy a tener instancias de ParticleView para modificar su color.

    Se entiende a donde apunta mi pregunta?
    No pido que me soluciones todo, pero al menos una opinión al respecto...
    Gracias!!

    Germán

    ResponderEliminar
  2. Hola,

    como dije en los posts, no soy un programador profesional. Pero paso a explicar como solucionaría yo el problema que me presentas.

    Primero hay que entender que el View se actualiza solo, leyendo la información del modelo. Esto es, en cada frame se ejecuta la función update del view, que se encarga de leer los models y generar una representación del mismo.

    Esto significa que lo correcto sería tener en cada Particle un campo relacionado con lo que se quiere representar. En tu ejemplo, cada Particle debería tener un campo llamado "colisionando" o algo asi. Entonces, en cada frame el view leería el campo colisionando, y si es True (la particula está colisionando con otra) entonces usaría una representación adecuada (partícula roja).

    Otra posibilidad es emitir eventos de colisión, y registrar el view para que escuche ese tipo de eventos y se actualice en forma adecuada.

    Mi solución preferida es la primera, porque permite detectar cuando dos partículas están en contacto y cuándo no lo están mas. En la segunda solución, en cambio, tendrías que emitir eventos de "fin de colisión" para que la partícula deje de estar roja.

    La primer solución también tiene la ventaja de que es generalizable para manejar cualquier cambio del modelo que influye en el view, y te da la pauta de como es la relación entre el modelo y el view: El modelo tiene información física relevante y no sabe nada del view, el view lee el modelo y genera una representación. Este esquema es el que uso yo para representar la orientación de un sprite, cada entidad del modelo tiene un campo de orientación (un ángulo) y en cada frame el view lee este ángulo y muestra el sprite adecuado para la orientación.

    Espero que te haya servido esta explicación.

    Estoy ocupado con algunas cosas de mi trabajo, pero en estas semanas voy a crear un nuevo post.

    ResponderEliminar
  3. Tu explicación me sirvió mucho y se acerca a lo que tenía en mente.

    Coincido con que la mejor solución es tener un campo "colisionando" de tipo booleano.

    En principio quería esquivarle a esa solución, debido a que el campo se lo estoy agregando solo porque lo necesito para poder hacer la View. De otra manera me hubiese alcanzado con que el CollisionDetector se encargue de detectar, y alguien mas se encargue de aplicar la "Collision Response" sin necesidad que Particle se entere que colisionó.

    Te resulta muy "incorrecto" adaptar el Model (agregar campo colisionando) para que la View pueda representarlo mas fácilmente, por mas que el Model pueda funcionar sin ese campo?


    Espero con ansias tu nuevo post!. Y seguro te estaré haciendo mas preguntas jeje.

    Saludos!

    ResponderEliminar
  4. Hola Germán,

    me parece que sería una cuestión mas bien filosófica y la solución depende mas que nada del "significado" que le quieras dar a la colisión.

    Mi respuesta asumía que la colisión era significativa desde el punto de vista del modelo. O sea que era la representación de un choque físico de partículas. Con lo cual eventualmente podías llegar a simular rebotes, o daño generado por la colisión.

    Ahora, si la colisión es solo interesantes desde el punto de vista del view, la otra solución es detectar colisiones en el view, colisiones entre ParticleView.

    Esto puede sonar chocante, pero si el modelo no contempla el concepto de colisiones entonces puede significar que la colisión es solamente un recurso de visualización.

    Tendrías que tener en cuenta que una colisión en el view no necesariamente se corresponde con una colisión en el modelo. El caso mas evidente es el de un modelo 3d con un view (obviamente) 2d. Es posible que haya una colisión en el view (dos objetos compartiendo coordenadas X-Y de la pantalla) sin que haya una colisión en el modelo. También va a haber una discrepancia entre las colisiones si el sprite (un objeto del view) tiene una geometría distinta al modelo (que puede ser tan simple como un punto).

    Incluso es posible que el concepto de colisión tenga muy poco significado en el modelo. Por ejemplo si el modelo son partículas puntuales sin una geometria asociada, rara vez va a haber una colisión... matemáticamente la probabilidad es 0 si las partículas se mueven al azar y son solo puntos. En cambio, en el view la cuestión puede ser totalmente diferente, ya que vos podes estar representando las partículas con círculos, con lo cual la probabilidad de colisión ya no es 0.

    El ejemplo que planteaste es un tanto abstracto. Si realmente lo que querés hacer es convertir una partícula en roja, solamente para mostrar al usuario que hay una superposición de partículas, entonces probablemente sea solo una colisión entre sprites del view. Ahora, si en realidad lo que vas a hacer es generar un "sonido de choque", hacer una animación que muestre que la partícula chocó (quizá una explosión) y/o restar daño de las partículas o afectar el juego de alguna manera, entonces la solución es colisiones en el modelo.

    Otra cosa que te podes preguntar es qué pasaría en el hipotético caso de que quieras programar otro view usando una tecnología distinta o con un objetivo distinto, sería indispensable que vuelvas a programar esta característica de colisión? por que? O sea, la colisión tiene existencia independientemente del view? Si es asi, entonces probablemente sea una característica del modelo.

    Mi experiencia con el tema patterns es que no hay una solución única para cada problema. Y muchas veces uno se mata pensando y no encuentra una solución simple. Una cosa para tener en cuenta también es que uno muchas veces tiene que cortar por la solución pragmática (googleá duck-tape programming) si quiere avanzar en un tiempo razonable.

    ResponderEliminar
  5. Hola,
    Como siempre voy a tirar laureles a mi amigo Ezequiel. La explicación no e solo fasicnante desde lo filósifico sino que muestra como al aplicar una esquema claro de ideas a un producto, la generalización (si uno no cambia el esquema) se da casi automaticamente.
    La solución que presenta Ezequiel, y que es la filosofía básica de nuestro proyecto, es que un Modelo en particular se puede visualizar de muchas maneras diferentes (por medio de Views). Cada visualización corresponde a una interpretación de los campos qeu existen en el Modelo. Imaginemos un modelo que consite en plabras usando las letras {l,a,o,d}. Supongamos que los views son "lectores" que convierten las palabras en el Model a sonidos (uso este ejemplo para explicitar que le View, puede ser mas que una visualizacion) y supongamos que cada Vie tiene un idioma nativo, digamos, uno es Inglés y el otro Castellano, y solo "leen" palabras de ese idioma. El Model tiene una "física", un mecanismo, que permuta las letras de las palabras de vez en cuando. Cuando por azar se arme la palabra "load" el Viewer Inglés generará un sonido, puesto que esta palabra es de su incumbencia. El Viewer Castellano no genera sonido, a pesar de que el la palabra "load" está en el modelo, uesto que no tiene "sentido" para ese viewer. La situción se invierte si por azar aparece la palabra "ola".
    Con este ejmplo trato de resaltar la libertad de un Viewer ara interpretar los contenidos de un modelo y que tiene como consecuencia un infinito de aplicaciones. Cortar esa libertad ene l view significa restringir la extensibilidad delsistema en general y acotar las exploraciones posibles utilizando diferentes combinaciones de Models y Viewers.
    Si aceptamos la creencia de que en nuestra realidad existen reglas que regulan el comportamiento del mundo, podemos pensar que nuestra realidad es tambine un Model (Wow que filosofico!), esta idea no es solo una extensión sin sentido, sino que pensando de esa manera reinterpretamos a los Viewers como las INTERFACES entre dos modelos...desde este punto de vista los Controllers juegan un rol interesatisimo...

    Ja, espero que la filosofía no aburra demasiado.

    JPi

    ResponderEliminar

Podés comentar lo que quieras. Si puteas, perdés.

Licencia


Creative Commons License
Esta obra de Ezequiel Pozzo se encuentra bajo una licencia Creative Commons Atribución-No Comercial-Compartir Obras Derivadas Igual 2.5 Argentina License.
Se puede obtener permisos mas allá de los otorgados por esta licencia en ezequielpozzo.blogspot.com.