lightCyan parte 2: Añadir gestión de módulos y crear módulo de FPS
Antes de empezar, es recomendable que hayas leído (y entendido) la Parte 1:
¿Cual es el objetivo de este post?
Crear la gestión de módulos de nuestro framework para juegos en HTML5 y Canvas. Una vez finalizado, crear un módulo que nos muestre los FPS actuales a los que se ejecuta la aplicación.
- Resultado final del tutorial AQUI.
- Código fuente: https://github.com/pmphp/lightCyan (version 0.2)
¿Qué es un módulo?
Un módulo es un trozo de código que se encarga de realizar X tarea. Dicho módulo únicamente realiza esa tarea. La ventaja de esto, es poder tener nuestro código bien dividido en módulos y de esta forma conseguir una aplicación escalable y fácil de mantener.
¿Algún pre-requisito?
Está claro que sí, sino no lo hubiera preguntado
. Necesitamos que los módulos puedan comunicarse entre sí a la vez que también puedan comunicarse con nuestro framework. Sí no acabáis de entender a qué me refiero con “comunicarse”, más adelante y con el código en mano, será más fácil entenderlo.
Crear el método público “addModule”:
Como de costumbre, empezaremos desarrollando el método público que nos permita añadir módulos:
(function (window) { var oModules = {}; /* ... */ window.lightCyan = { /* ... */ addModule : function (sModuleId, fpBuilder) { if (typeof sModuleId === 'string') { if (typeof oModules[sModuleId] === 'undefined') { oModules[sModuleId] = { fpBuilder : fpBuilder, oInstance : null }; } } } }; }(window)); |
Lo primero que hacemos es añadir una nueva variable privada llamada “oModules“. Esta variable va a ser la que almacenará nuestros módulos.
Seguidamente, creamos el método público “addModule“. El método es muy parecido a “addGameObject“. Con la diferencia de que NO estamos ejecutando el módulo, solamente almacenándolo en “oModules“. Los módulos van a ejecutarse por primera vez más adelante
!
El método “addModule” espera recibir dos parámetros:
- sModuleId: Identificador del módulo.
- fpBuilder: Función constructora de nuestro módulo.
Por lo tanto, si quisieramos añadir un módulo la estructura sería la siguiente:
lightCyan.addModule("fps", function () { return {}; }); |
Modificar método público “startGame”:
Vamos a tener que modificar el método público “startGame“, ya que los módulos van a construirse justo antes de la ejecución del juego.
/* ... */ startGame : function () { oPreStart.buildModules(); oPreStart.startGame(); } /* ... */ |
Ahora el método “startGame” va a lanzar dos métodos que, si habéis estado atentos, sabréis que aún no existen… ¡adivinad qué es lo siguiente!
Crear objeto “oPreStart” y sus dos métodos “buildModules” y “startGame”:
(function (window) { /* variables privadas */ var oPreStart = { buildModules : function () { // Definimos las variables necesarias. var sModule, oModule, // #3 oSandbox = { meeting : oMeeting, // #4 canvas : oCanvas, settings : { fps : nFps } }; // Build Modules. for (sModule in oModules) { if (oModules.hasOwnProperty(sModule)) { oModule = oModules[sModule]; if (typeof oModule !== 'undefined') { // #1 oModule.oInstance = oModule.fpBuilder(oSandbox); // #2 if (oModule.oInstance.init) { oModule.oInstance.init(); } } } } }, startGame : function () { nDrawInterval = 1 / nFps * 1000; setInterval(fpGameInterval, nDrawInterval); } }; /* ... */ window.lightCyan = { /* ... */ }; }(window)); |
El método “startGame” de “oPreStart” como podéis ver es exactamente lo mismo que teníamos antes en el método público “startGame“, así que no hace falta explicarlo.
El método “buildModules” és algo más complejo:
- #1 Recorremos todos los elementos que hay en la variable privada “oModules” (que van a ser los módulos que hayamos introducido con la función “lightCyan.addModule(“id”, fpBuilder)“). Para cada modulo ejecutamos la función constructora y le pasamos como parámetro “oSandbox” (!!!!!! de momento quedaos con que “oSandbox” es una especie de “caja de herramientas”).
- #2 Si el módulo que acabamos de cargar tiene un método “init”, lo ejecutamos. Esto puede sernos útil para módulos que necesiten inicializar variables, funciones, etc…
- #3 El “oSandbox” es un objeto que contiene métodos o propiedades que pueden ser útiles para nuestros módulos. A medida que el Framework crezca, es muy probable que el sandbox también crezca. Algo que seguro van a necesitar muchos módulos es acceso al canvas, al buffer y a sus respectivos contextos.
- #4 ¿Meeting?
! Al principio del post dije que uno de los requisitos era que los módulos pudieran comunicarse entre sí. Pués eso va a ser el objeto “oMeeting“, una herramienta para que los módulos puedan mandar y recibir mensajes. Lo mejor de todo es que, un módulo no tiene porqué conocer que hacen los otros módulos cuando lanza un mensaje, ni los otros módulos van a saber qué hace él con los mensajes que recibe.
Un ejemplo: Imaginémonos que estamos en una conferencia. Como ponentes, nosotros transmitimos un mensaje: “Flash está muerto”. A nosotros realmente nos da igual lo que haga o lo que sienta el público de la conferencia. El señor “A” con esa información quizá decida que al llegar a casa borrará “Flash” de su máquina. El señor “B” es poseedor de un iPhone así que ni se inmuta… etc.
Esa es la idea principal del objeto “oMeeting“, que todos puedan “decir algo”, y que todos puedan “hacer algo” con los mensajes recibidos.
Hecha la introducción, veamos el objeto “oMeeting“:
(function (window) { /* variables privadas */ var oMeeting = { speak : function (oNotification) { var nListenersLength, nCount, oListener, aMeetingList = oMeeting[oNotification.message]; if (typeof aMeetingList !== 'undefined') { nListenersLength = aMeetingList.length; for (nCount = 0; nCount < nListenersLength; nCount += 1) { oListener = aMeetingList[nCount]; oListener.handler.call(oListener.module, oNotification); } } }, listen : function (aMessages, fpHandler, oModule) { var sMessage = '', nMessage = 0, nMessages = aMessages.length; for (nMessage = 0; nMessage < nMessages; nMessage += 1) { sMessage = aMessages[nMessage]; // Si el mensaje no existe, creamos un array para // dicho mensaje. if (typeof oMeeting[sMessage] === 'undefined') { oMeeting[sMessage] = []; } // Añadimos el módulo dentro del array del mensaje. oMeeting[sMessage].push({ module: oModule, handler: fpHandler }); } } }; /* ... */ window.lightCyan = { /* ... */ }; }(window)); |
Antes de nada, decir que la idea de este sistema de comunicación entre módulos la extraje de la librería Hydra.js.
Cómo veis, “oMeeting” se compone de dos métodos: speak y listen. ¿Bastante descriptivo verdad?
Método listen:
Parámetros que espera recibir:
- aMessages : Un array con los mensajes que el módulo quiere escuchar.
- fpHandler : Método del módulo que va a gestionar los mensajes.
- oModule : Módulo interesado en escuchar los mensajes.
Veamos un ejemplo para que quede más claro:
lightCyan.addModule("fps", function (sandbox) { return { init : function () { // Como hemos visto en el buildModules, los módulos reciben // el parámetro sandbox con utilidades varias, una de ellas // el objeto "oMeeting". sandbox.meeting.listen(["start", "frame"], this.messageHandler, this); }, messageHandler : function (message) { // Este método se va a ejecutar SIEMPRE que otro módulo "hable" sobre // "start" o "frame". // Más tarde veremos cómo manejar los mensajes. } }; }); |
Él método “listen” lo único que hace es crear (en caso de no existir) un array para el mensaje, e ir añadiendo todos los objetos que hagan una petición de “listen” de ese mensaje en el array.
Método speak:
Parámetros que espera recibir:
- oNotification: Objeto con las propiedades “message” y “data”.
Ejemplo de speak:
lightCyan.addModule("fps", function (sandbox) { return { init : function () { sandbox.meeting.speak({ message : "start", data : null }); } }; }); |
En este ejemplo, el módulo “fps” está lanzando el mensaje “start”. El método “speak” lo que hace es acceder al objeto “oMeeting”, coger el listado de objetos que están escuchando ese mensaje, y ejecutar el método encargado de gestionar el mensaje para cada objeto.
Como de costumbre, veamos un ejemplo para clarificar… Añadamos el siguiente código en nuestro index.html, justo antes del “lightCyan.startGame()”:
lightCyan.addModule("Module_1", function (sandbox) { return { init : function () { // Como hemos visto en el buildModules, los módulos reciben // el parámetro sandbox con utilidades varias, una de ellas // el objeto "oMeeting". sandbox.meeting.listen(["start", "frame"], this.messageHandler, this); }, messageHandler : function (notification) { if (notification.message === "start") { console.log("Module_1 ha escuchado 'start'"); } else if (notification.message === "frame") { console.log("Module_1 ha escuchado 'frame'"); } } }; }); lightCyan.addModule("Module_2", function (sandbox) { return { init : function () { sandbox.meeting.speak({ message : "start", data : null }); } }; }); |
Veremos como en la consola de nuestro explorador nos aparece “Module_1 ha escuchado ‘start’”.
¡En resumen! “Module_2” ha lanzado un mensaje “start” y sin él saberlo, ha provocado que otro módulo (“Module_1“) del que ni tan sólo conoce su existencia haya ejecutado una acción.
Podríamos añadir un método de “stopListen” dentro de nuestro objeto “oMeeting”, pero para no alargarme lo añadiré en futuros tutoriales dónde sea necesario.
Como hemos tocado bastantes cosas os recomiendo que, aunque os funcione, descarguéis la versión lightCyan.0.2.js de Github, para tenerlo todo bien ordenado y limpio.
Ahora que ya tenemos nuestro gestor de módulos funcionando… ¡es el momento de crear el módulo FPS y ver la utilidad real de todo el tinglado!
Módulo “FPS” para nuestro Framework:
En su día ya redacté un tutorial para calcular los FPS, así que lo único que vamos a hacer es adaptar ese tutorial al formato “módulo” que hemos creado y añadir un par de cosas a nuestro lightCyan.js.
En resumidas cuentas, el módulo quedaría de la siguiente manera:
lightCyan.addModule("fps", function (sandbox) { var currentFps = 0, frameCount = 0, lastFps = new Date().getTime(), oBuffer = sandbox.canvas.bufferContext; return { init : function () { sandbox.meeting.listen(["#draw#"], this.drawFps, this); }, drawFps : function() { var thisFrame = new Date().getTime(); var diffTime = Math.ceil((thisFrame - lastFps)); if (diffTime >= 1000) { currentFps = frameCount; frameCount = 0.0; lastFps = thisFrame; } oBuffer.save(); oBuffer.fillStyle = '#000'; oBuffer.font = 'bold 12px sans-serif'; oBuffer.fillText('FPS: ' + currentFps + '/' + sandbox.settings.fps, 10, 15); oBuffer.restore(); frameCount += 1; } }; }); |
Lo dicho, no voy a explicar cómo funciona el método (cualquier duda: tutorial para calcular los FPS).
Si habéis prestado atención al código, en el método “init” estamos diciendo que queremos escuchar el mensaje “#draw#”, y que “drawFps” va a ser el método encargado de hacer “algo” cada vez que el mensaje “#draw#” llegue a los oídos del módulo.
El problema está en que ahora mismo no hay nadie que esté hablando y mucho menos diciendo “#draw#”. Así que vamos a adaptar el Framework para que a través de mensajes, vaya informando a los módulos de los distintos procesos por los que se encuentra el juego.
El primer mensaje que lanzaremos desde el Framework es cuando el juego va a empezar:
/* ... */ oPreStart = { buildModules : function () { /* ... */ }, startGame : function () { // Lanzamos el mensaje #start#. oMeeting.speak({ message : "#start#", data : null }); nDrawInterval = 1 / nFps * 1000; setInterval(fpGameInterval, nDrawInterval); } }; /* ... */ |
Y para finalizar, lanzaremos un mensaje desde el método “draw” del objeto “oGameExecution”:
/* ... */ draw : function () { oCanvas.bufferContext.clearRect(0, 0, oCanvas.buffer.width, oCanvas.buffer.height); fpCallGameObjectMethods("draw", oCanvas); // Lanzamos el mensaje #draw#. oMeeting.speak({ message : "#draw#", data : null }); oCanvas.mainContext.clearRect(0, 0, oCanvas.main.width, oCanvas.main.height); oCanvas.mainContext.drawImage(oCanvas.buffer, 0, 0); } /* ... */ |
Podríamos lanzar mensajes desde muchos otros sitios, pero cómo de momento no lo necesitamos, lo dejaremos así.
El motivo para poner # al final y al principio de la palabra, es para evitar colisiones con futuros mensajes que puedan lanzar los módulos.
Si ahora ejecutamos el index.html…. ¡Voilà! Tenemos nuestro querido rectángulo con un contador de FPS en la esquina superior izquierda.
Otros módulos que podríamos construir serían, por ejemplo:
- Módulo para la gestión y precarga de imágenes
- Módulo para la gestión de colisiones.
- Módulo para la gestión de físicas
Supongo que tarde o temprano, caerán los tutoriales para el desarrollo de dichos módulos
!
Recordad:
- Resultado final del tutorial AQUI.
- Código fuente: https://github.com/pmphp/lightCyan (version 0.2).
Hola, estoy siguiendo estos tutoriales para la creacion de un framework para juegos en HTML5, y la verdad esta muy bueno, ahora quiero ver de implementar un modulo para la deteccion de colisiones, no tengo demasiada idea de como encararlo, se me ocurria que en cada “update” mande un mensaje al modulo de deteccion, el cual checkea cada objeto de aGameObjects si estan colisionando con otro, y cuando encuentra una colision envia el mensaje correspondiente a cada objeto… ¿Te parece viable, o existe una manera mas “correcta” de implementar las colisiones? Desde ya muchas gracias por todo
Hola Mauro,
La verdad es que no existe una forma más “correcta” que otra para crear el módulo.
Te explicaré un poco por encima cómo lo tengo hecho en cyanJS:
Los objetos en el método “init” se “subscriben” al módulo de colisiones.
EJ:
init : function () {
CYANJS.collisionManager.add(this, this.handleCollision);
}
collisionManager lo que hace es guardar el objeto y el método al que va a notificar (con toda la información de la colisión que necesite) cada vez que haya una colisión.
Calculo las colisiones de todos los objetos antes de hacer ningún update. Una vez resueltas las colisiones, hago los updates de todos los objetos.
En mi caso, todos los objetos tienen un objeto “velocity” con las propiedades “x” y “y”. El update en muchos casos sólo suma el velocity a las posiciones “x” y “y”. Por lo que, el método “handleCollision” és el que realmente se preocupa de reajustar la velocidad del objeto y de corregir las posiciones “x” y “y” en caso de que sea necesario.
Espero que te haya servido de algo mi miniexplicación!
Hola!
Estoy tratando de aprender algo de HTML5 y esto me viene al pelo…
por ahora solo bajé el ejemplo y lo voy modificando, como práctica.
Sin embargo algo debo estar entendiendo mal, a ver si me podés ayudar:
estoy tratando de limitar el movimiento del rectangulo para que no se salga del borde del canvas, lo quiero hacer editando en rectangle.js la funcion update de la siguiente forma (pongo solo la linea del mov a la derecha):
if (bMoveRight === true & (nAxisX + nWidth) < canvas.width) {
nAxisX += nSpeed;
esto no funciona, no obtengo tampoco ningun error ni excepcion, simplemente deja de funcionar el movimiento a la derecha, pero si cambio canvas.width por 500:
if (bMoveRight === true & (nAxisX + nWidth) < 500) {
nAxisX += nSpeed;
ahora si funciona!! pero no se porque, ya que la variable canvas.width con tiene 500 si le hago un watch.
tambien probé otras cosas como oCanvas.width, oCanvas.main.width, y algunas mas, pero tampoco funcionan.
espero que se entienda mi duda.
muchas gracias por dedicar tiempo a enseñarnos!
saludos
Hola Greg,
Al crear los objetos, les estamos pasando por parámetro un objeto “Sandbox” que contiene objetos varios, uno de ellos el canvas.
Deberías modificar el rectangulo.js para que quedara:
lightCyan.addGameObject(“rectangulo”, function (sandbox) { …… });
(Añadida la parte en negrita “sandbox”).
Entonces podrías acceder al canvas a través de:
sandbox.canvas;
y para el width:
sandbox.canvas.width;
El Sandbox lleva también algo muy útil: el objeto “meeting” que es el que nos sirve para comunicarnos entre objetos en caso de necesitar información de otros objetos del juego.
¡Saludos!
Wow! gracias por responder tan rápido, haciendo ese cambio tampoco funciona…
yo pensé que podía usar directamente canvas.width, porque vi que en draw se accede directamente a canvas.
no te preocupes por buscarle la vuelta, era solo para cambiar algo sencillo, que resulto ser un poco mas complicado, cuando me haga amigo del lenguaje ya lo voy a entender… supongo
gracias de nuevo!
Cierto, fallo mio!
El framework no tiene aún implementada la parte para que los objetos también reciban el Sandbox (pensé que sí ya que en cyanJS lo tengo así).
update : function (canvas) {
console.log(canvas.main.width);
}
El método update recibe el canvas como parametro, yo haciendo lo que te pongo arriba SI consigo el width.
bueno, no hay problema!
finalmente probé poniendo eso: console.log(canvas.main.width) y obtenia el mismo resultado que al poner canvas.width, pero solo por probar probé con canvas.main.width y funcionó!!
no me gusta mucho hacer las cosas sin entenderlas, pero por ahora me sirve…
muchas gracias una vez más, y a seguir aprendiendo!
Hola Pere,
me parece interesantisimo el Blog, aunque me cuesta un poco entender las cosas (vengo de C#) y me pierdo en la sintaxis ;P
Veo que creas el fichero rectángulo y se pinta en la ejecución, pero lo que no veo es donde lo agregas al array de Objetos.
Gracias.