« macromedia.com para dispositivos móviles | Inicio | Artículo en gotoAndPlay( ) »

El patrón State aplicado al desarrollo de juegos ( IV y final )

Por fin ( todo llega ) un ejemplo de cómo puede desarrollar un juego a partir de una máquina de estados.

La descripción del juego es la siguiente. Van apareciendo gotas de agua en pantalla, en posiciones aleatorias. Durante el tiempo del que disponemos, tenemos que evitar que el número de gotas en pantalla sobrepase un umbral determinado. Para eliminar una gota de pantalla, basta con hacer click sobre ella. Una vez terminado el tiempo, se nos ofrece la opción de volver a jugar, o de terminar el juego. Si volvemos a jugar, el número máximo de gotas que puede haber en pantalla disminuye. Se va repitiendo este patrón hasta que el jugador decida abandonar el juego, o hasta que se sobrepase el umbral máximo de gotas en pantalla, momento en que el juego termina, con derrota.

Aquí puede verse el diagrama de estados correspondiente.

En nuestro ejemplo, hemos construido el juego en tres capas distintas. No es exactamente un MVC, aunque es algo parecido. Tendremos una clase DropsController que hará las funciones de controllador ( instanciar el modelo del juego, y hacer de conexión entre la capa de presentación y dicho modelo ), DropsWorld, que es el modelo propiamente dicho, y la clase que tiene agregada la máquina de estados del juego. Finalmente, tendremos una vista ( que en este caso es muy ligera, ya que consiste solamente en un “bocadillo” de información y un contador de tiempo ). Por lo tanto, el mundo no tiene conocimiento de la existencia de la vista, ni la vista de la existencia del mundo. No es una implementación estricta de ninguno de los patrones conocidos, pero es con la que más cómodo trabajo. Por lo tanto, es la utilizada.

El proceso de inicialización del juego es el siguiente: Al cargar el swf, se instancia el controlador, que a su vez instancia el mundo, registra los listeners al mismo, y manda arrancar su máquina de estados. Para que el mundo pueda emitir eventos hacemos que extienda de la clase EventSource ( en el package net.designnation.events ). Dicha clase no es más que una parte de nuestra implmentación del patrón observer para poder emitir y registrarnos a eventos. De esta forma, los eventos emitidos por el mundo son escuchados por el controlador. El controlador también crea un clip vacío, cuyo onEnterFrame servirá de generador de pulsos para las máquinas de estados del juego

Una vez instanciado el mundo, se arranca su máquina de estados:

public function initWorld( param: Object ) { this.stageMC = param.baseline; this.initBehaviour( ); var theClass: DropsWorld = this; this.base_MC.onEnterFrame = function( ) { theClass.doProcess( ); } this.mySMachine.startMachine( ); } private function doProcess( ) { this.BEngineVal.doProcess( ); }

Hemos delegado la ejecución del ciclo de la máquina de estados en el el onEnterFrame del clip base_MC.

La máquina de estados la definimos en el método initBehaviour( )

private function initBehaviour( ) { var initGame: State= new State( "initGame", new CallbackDecl( this, "initGameAction" ) ); var startGame : State= new State( "startGame", new CallbackDecl( this, "startGameAction" ) ); var createDrop : State= new State( "createDrop", new CallbackDecl( this, "createDropAction" ) ); var overDrops : State = new State( "overDrops", new CallbackDecl( this, "overDropsAction" ) ); var endOfTime : State= new State( "endOfTime", new CallbackDecl( this, "endOfTimeAction" ) ); var defeat : State= new State( "defeat", new CallbackDecl( this, "defeatAction" ) ); var victory : State= new State( "victory", new CallbackDecl( this, "victoryAction" ) ); var endOfGame : State= new State( "endOfGame", new CallbackDecl( this, "endOfGameAction" ) ); new Transition( "initGameToStartGame", initGame, startGame, new CallbackDecl( this, "initGameToStartGameEval" ) ); //---------------------------------------------------------- new Transition( "startGameToCreateDrop", startGame, createDrop, new CallbackDecl( this, "startGameToCreateDropEval" ) ); //---------------------------------------------------------- new Transition( "createDropToEndOfTime", createDrop, endOfTime, new CallbackDecl( this, "createDropToEndOfTimeEval" ) ); new Transition( "createDropToOverDrops", createDrop, overDrops, new CallbackDecl( this, "createBubbleToOverDropsEval" ) ); new Transition( "createDropToSelf", createDrop, createDrop, new CallbackDecl( this, "createDropToSelfEval" ) ); //---------------------------------------------------------- new Transition( "overDropsToDefeat", overDrops, defeat, new CallbackDecl( this, "overDropsToDefeatEval" ) ); //----------------------------------------------------------- new Transition( "endOfTimeToVictory", endOfTime, victory, new CallbackDecl( this, "endOfTimeToVictoryEval" ) ); new Transition( "endOfTimeToStartGame", endOfTime, startGame, new CallbackDecl( this, "endOfTimeToStartGameEval" ) ); //------------------------------------------------------------- new Transition( "defeatToEndOfGame", defeat, endOfGame, new CallbackDecl( this, "defeatToEndOfGameEval" ) ); //------------------------------------------------------------- new Transition( "victoryToEndOfGame", victory, endOfGame, new CallbackDecl( this, "victoryToEndOfGameEval" ) ); this.mySMachine.resetToInit( initGame ); }

Y una vez instanciados tanto los estados como las transiciones, debemos implementar los callbacks que llevan asociados.

La clase que se utiliza para presentar las gotas es un controlador bastante ligero. También extiende a EventSource, por lo que será capaz de emitir un evento cuando se haga click sobre el clip que lleva agregado. Seguro que este punto puede dar lugar a discusión, pero una gota es una gota, no un MovieClip, por lo tanto he optado por que la clase Drop, dada su funcionalidad, tenga un clip con el gráfico de la gota agregado, y no sea en sí misma una subclase de MovieClip.

El código en general se explica por sí solo, por lo que una lectura atenta del mismo puede ser suficiente para comprender el funcionamiento del mismo.

También quisiera resaltar que ésta no debería considerarse como una implementación definitiva. Gran parte del código de DropsWorld es común a todos los juegos que se desarrollen de esta forma, por lo que debería subclasificarse para crear un mundo base que implementara las operaciones básicas de la máquina de estados ( arranque, parada, inicialización, etc ). Por lo tanto, el código presentado puede ( y debería ) mejorarse considerablemente. Igualmente, el juego en sí requiere de un poco más de trabajo ( no hay feedback sobre cuál es el número máximo de gotas que puede haber en pantalla, sobre si estamos lejos o cerca de ese umbral, etc. ). Además., dada la forma en la que se registran los listeners, antes de destruir las gotas deberían de-registrase los listeners que tengan asociados, para evitar referencias circulares.

Aún así, también quisiera resaltar que el tiempo del desarrollo del juego ( dejando aparte el framework base, por su puesto ), no ha llegado a dos horas. Y el resultado final es éste:

Quisiera también dejar constancia de mi agradecimiento a Celia Carracedo por los gráficos del juego.

El código fuente del juego puede descargase aquí. No olvides definir el classpath apropiado para compilarlo. Los packages son:

net.designnation.behaviours
net.designnation.data
net.designnation.events
net.designnation.physics
net.designnation.PoppingDrops ( game classpath )