Modelo MVC en juegos multiplayer

13 01 2008

Actualmente me encuentro desarrollando un juego multiplayer, para lo cual me veo obligado a ponerme al tanto de muchas cosas que jamás tome en cuenta antes a la hora de hacer videojuegos.


Para comenzar, es esencial separar el modelo del juego de su representación: Imaginemos que tenemos un juego multiplayer en tiempo real en flash, en el cual, en cada actualización de pantalla movemos el personaje 5 pixeles hacia la dirección que indica el jugador. Como la actualización depende del framerate, el jugador que tenga la computadora mas ponente(mas frames por segundo) se movería mas rápido que los demás.

Lo que se hace en estos casos, es recurrir al modelo MVC(sus detalles van mas allá del alcance de este post). En un juego, el modelo(en nuestro caso, la posición del jugador) es actualizada cada cierto intervalo de tiempo fijo, mientras que la representación(mostrar el avatar del jugador en pantalla) se hace tan pronto como sea posible. En el caso que no pueda garantizarse que la actualización del modelo sea llevada a cabo en un lapso de tiempo fijo, lo que se hace es calcular la posición del jugador en base al tiempo transcurrido desde la ultima actualización del modelo. Supongamos que el personaje se mueve a 10 pixeles por segundo y la rutina actualización del modelo se encuentra que la ultima vez que actualizó la posición del jugador fué hace 2 segundos, moverá este a una distancia de 20 píxeles.

Después les sigo contando que pasa con la diferencia de tiempos debido al lag y como hacer para que, siendo que SIEMPRE hay un retraso en la comunicación, todos los jugadores vean lo mismo, o casi.

Saludos.



Optimizando las entradas por teclado en ActionScript 3.0

1 01 2008

Resulta que estaba haciendo un juego donde tenía que capturar la entrada por teclado, algo muy simple, hay muchisimos ejemplos en la web. Básicamente agregaba un listener al stage para que, cuando una tecla era presionada, se cambie la dirección del personaje en pantalla. Una vez implementado esto, me encontré con un problema:

Cuando cambiaba de dirección, por ejemplo, iba a la derecha y giraba a la izquierda, el personaje tardaba en reaccionar, algo así como 0.5 de segundo.

Entonces, en lugar de meter todo el código dentro del evento “onKeyPress”, el cual tenía metido un switch donde testeaba por el código de tecla presionada y pasaba a modificar la posición del sprite en pantalla, agregué dos listeners al objeto stage: onKeyDown y onKeyUp.

Estos métodos lo único que hacían era actualizar un elemento de una matriz de 255 elementos, poeniendolo en true en el evento onKeyDown y en false en el evento onKeyUp.

La matriz la puse dentro de un singleton, para poder acceder a ella de cualquier parte. Después, solo tenía que fijarme si el elemento del array que correspondía a la tecla que quería testear estaba a true, si esto era cierto, actualizaba la posición del objeto.

Así fue como obtuve una respuesta del personaje mas fluida. Espero que esta experiencia les sea de utilidad.

Saludos!



Consideraciones del modelo de eventos en ActionScript 3

1 01 2008

Las siguientes consideraciones deben tenerse en cuenta cuando se programa usando el modelo de eventos de actionscript 3, es decir, clases que heredan de EventDispatcher o que implementan la clase IEventDispatcher.

En este post, llamo listeners a las funciones que "escuchan" eventos. Las mismas tienen la siguiente forma:

Actionscript:
  1. [public|private] function myListener(evt:Event):void{
  2.         //bla bla bla
  3. }

1. Los eventos son identificados por la propiedad type, por lo que, si tenemos un listener que escucha al evento "onMyListenerDispatch", para llamarla desde el objeto hacemos dispatchEvent(new Event("onMyListenerDispatch"));.

Si tenemos...

Actionscript:
  1. //Mas código
  2.  
  3. myClase.addEventlistener("myDispatchEvent", myEventListener);
  4.  
  5. //Mas código
  6.  
  7. public function myEventListener(evt:Event){
  8. }

para llamar a la funcion "myEventListener" hacemos:

Actionscript:
  1. //Desde dentro de la función que dispara eventos
  2.  
  3. dispatchEvent(new Event("myDispatchEvent"));

2. Si tenemos una clase1 que dispara un evento e implementa una función1 que lo escucha, y otra clase2 que la extiende, es decir, que hereda de ella y sobreescribe la función1. Cuando la clase1 en uno de sus miembros no sobreescritos dispara un evento, el listener que se disaparará será el sobreescrito por la clase2.

por ejemplo, si tenemos:

Clase padre:

Actionscript:
  1. public class Clase1 extends EventDispatcher
  2. {
  3.     public function FSSceneStateGeneric()
  4.         {
  5.             super();
  6.             addEventListener("onBeginUpdate", onBeginUpdate);
  7.             addEventListener("onEndUpdate", onEndUpdate);
  8.         }
  9.  
  10.         public function onBeginUpdate(evt:Event):void{
  11.             trace("super: on Begin Update!");
  12.         }
  13.        
  14.         public function onEndUpdate(evt:Event):void{
  15.             trace("super: on End Update!")
  16.         }
  17.        
  18.         public function update():void{
  19.             dispatchEvent(new Event("onBeginUpdate"));
  20.         }
  21. }

Clase derivada

Actionscript:
  1. public class FSSceneStateFadding extends FSSceneStateGeneric
  2.     {
  3.         public function Clase2()
  4.         {
  5.    
  6.         }
  7.        
  8.         public override function onBeginUpdate(evt:Event):void{
  9.             trace("STATE: ON BEGIN UPDATE");
  10.         }
  11.        
  12.        
  13.         public override function update():void{
  14.             super.update();
  15.         }
  16. }

Lo que obtendremos en el trace será:

STATE: ON BEGIN UPDATE

Ampliaremos.



Tips para la organización de código

1 01 2008

Esto es mas bien una nota para mi, pero pueden usarla si creen que los saca de un apuro. Estas consideraciones me ayudan a decidir donde poner tal o cual código en un juego.

Por ejemplo, hace poco me surgió la siguiente duda mientras programaba un juego multiplayer de disparos:

Es la bala la que debe darse cuenta si le dió a un elemento que puede ser destruido a balazos(e informarlo al servidor), cada objeto destruible debe darse cuenta que fué impactado por una bala o debe haber una entidad(porción de código) que realice estas comprobaciones y lo informe al servidor?

El problema es que cada uno de los objetos propuestos debe tener referencias a los objetos con los que interactúa, pero ninguna opción es necesariamente mala. Uno puede pensar que el código que realiza todas estas comprobaciones debe estar en la clase principal, que iniclializa todos los objetos y, por ende, tiene la referencia a todos ellos, evitando crear singletons o pasando por referencia las colecciones de objetos durante al creación, complicando la interface de los constructores. Pero también puede querer que cada elemento (como las balas, las granadas y los objetos destructibles) sean autonomos y que ellos encapsulen las complejidades de su comportamiento y que cada objeto se de cuenta que si debe enviar o no datos por la red(si la posición de un jugador no cambia, no debería transmitirse).

Estas consideraciones pueden quitarnos gran cantidad de tiempo si no queremos arrepentirnos mas adelante si tenemos que tocar el código o pasárselo a alguien para que lo modifique.

Estos tips no son en absoluto sustituto de la experiencia, pero al menos nos ayudará a encontrar "la punta del ovillo".

1. Fundamentar las decisiones de diseño.
Si decimos "la bala debe darse cuenta si impacta contra un elemento que puede ser destruido o dañado" documentemos en el código por que tomamos esta decisión. Esto nos ayudará a no tentarnos a cambiar mas adelante el modelo por otro que nos guste mas.

2. Que todas las operaciones similares sean llevadas a cabo de la misma manera en todos lados.
Si tenemos una bala que se da cuenta cuando impacta a un jugador (con lo cual debe actualizar las propiedades como la energía del mismo o su velocidad) y luego tenemos que agregar la capacidad de lanzar granadas, que la granada se de cuenta también si explotó lo suficientemente cerca de un jugador como para dañarlo.

3. Si debemos cambiar un comportamiento, asegurarnos que es totalmente necesario y cambiar todos los comportamientos similares.
Este punto tiene como objetivo evitar cambios innecesarios y mantener la homogeneidad del diseño. Supongamos que el hecho de que bala se de cuenta de que impactó contra algo es antiperformante y nos tira abajo los frames por segundo. Por lo tanto, debemos crear una matriz de colisiones, manejada desde una entidad que contenga todas las referencias a las balas que hay volando en el escenario. Deberemos extraer el comportamiento de comprobación de colisiones no solo de la bala, sino también de la granada aunque esta ultima operación no afecte la performance del juego. De esta manera, la bala y la granada(proyectiles) se comportarán de la misma manera, lo cual allanará el camino para tomar futuras decisiones sobre proyectiles(como un misil), recordemos que puede ser que no seamos nosotros los que en un futuro tengamos que modificar o ampliar el código.

Por ahora eso es todo lo que tenía necesidad de bloguear. Ampliaremos.