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.behavioursnet.designnation.data
net.designnation.events
net.designnation.physics
net.designnation.PoppingDrops ( game classpath )