Cómo escribir un juego BrikBlock con los elementos SVG y Canvas de HTML5.
El starter y la solución completa las puedes encontrar aquí.
Resumen
Introducción
Requisitos previos
Preparación del fondo
Preparación del juego
Conclusión
Introducción
En este tutorial trataremos de desvelar algunas de las características del desarrollo de gráficos con los elementos SVG y Canvas (que son dos tecnologías destacadas de HTML5)
Para ello vamos a programar juntos un juego rompe-ladrillos (también conocido como Arkanoid o Blockout en el ambiente). Estará compuesto de un fondo animado (que haremos utilizando el Canvas) y con SVG crearemos los ladrillos, la bola y la raqueta.
El fondo es solamente una excusa para utilizar un canvas. Nos va a permitir dibujar pixels en un área concreta. Por eso vamos a utilizarlo para dibujar un agujero de gusano espacial (¡ah, como me gusta Stargate!). Los usuarios tendrán la posibilidad de mostrarlo en pantalla o no con el botón de Modo (Mode):
Como puedes ver, vamos a añadirle un contador en la esquina superior derecha (solo para demostrar la potencia de los gráficos acelerados por hardware )
Diseño de la página HTML5
Empecemos con el archivo index.htm, vamos a añadirle el canvas como un elemento hijo del div llamado gameZone:
1. <canvas id="backgroundCanvas">
2. Tu navegador no soporta HTML5. Instala Internet Explorer 9 :
3. <br />
4. <a href="http://windows.microsoft.com/en-US/internet-explorer/products/ie/home?ocid=ie9_bow_Bing&WT.srch=1&mtag=SearBing">
5. http://windows.microsoft.com/en-US/internet-explorer/products/ie/home?ocid=ie9_bow_Bing&WT.srch=1&mtag=SearBing</a>
6. </canvas>
Incorporar el código Javascript
El fondo se maneja desde el archivo background.js (¡qué sorpresa!). Así que tenemos que registrarlo dentro de index.htm. Justo antes del cierre de la etiqueta
Antes que nada, necesitamos unas constantes para controlar la presentación en pantalla:
1. var circlesCount = 100; // Número de círculos utilizados por el agujero de gusano
2. var offsetX = 70; // offset del centro del agujero (X)
3. var offsetY = 40; // offset del centro del agujero (Y)
4. var maxDepth = 1.5; // Distancia máxima para un círculo
5. var circleDiameter = 10.0; // Diámetro del círculo
6. var depthSpeed = 0.001; // Velocidad del círculo
7. var angleSpeed = 0.05; // Velocidad angular de rotación del círculo
Por supuesto, puedes modificar estas constantes si quieres cambiar el aspecto del agujero de gusano.
Creación de elementos
También necesitamos guardar referencias a los principales elementos de la página HTML:
1. var canvas = document.getElementById("backgroundCanvas");
2. var context = canvas.getContext("2d");
3. var stats = document.getElementById("stats");
¿Cómo dibujamos un círculo?
El agujero de gusano consiste únicamente en una secuencia de círculos con distintas posiciones y tamaños. Para dibujarlo utilizaremos la función circle que se basa en valores de profundidad, ángulo e intensidad (el color de base).
1. function Circle(initialDepth, initialAngle, intensity) {
2. }
El ángulo y la intensidad son privados, pero la profundidad es público, para que el agujero de gusano pueda cambiarla.
1. function Circle(initialDepth, initialAngle, intensity) {
2.
3. var angle = initialAngle;
4. this.depth = initialDepth;
5. var color = intensity;
6. }
También necesitamos una función draw pública para dibujar el círculo y actualizar los valores de profundidad y ángulo. Por eso tenemos que definir el punto donde se va a dibujar el círculo. Para ello definimos dos variables (x e y):
1. var x = offsetX * Math.cos(angle);
2. var y = offsetY * Math.sin(angle);
Dado que x e y son coordenadas del espacio, necesitamos proyectarlas en la pantalla:
1. function perspective(fov, aspectRatio, x, y) {
2. var yScale = Math.pow(Math.tan(fov / 2.0), -1);
3. var xScale = yScale / aspectRatio;
4.
5. var M11 = xScale;
6. var M22 = yScale;
7.
8. var outx = x * M11 + canvas.width / 2.0;
9. var outy = y * M22 + canvas.height / 2.0;
10.
11. return { x: outx, y: outy };
12. }
Así, la posición final del círculo se calcula con el código siguiente::
1. var x = offsetX * Math.cos(angle);
2. var y = offsetY * Math.sin(angle);
3.
4. var project = perspective(0.9, canvas.width / canvas.height, x, y);
5. var diameter = circleDiameter / this.depth;
6.
7. var ploX = project.x - diameter / 2.0;
8. var ploY = project.y - diameter / 2.0;
Y con esta posición ya podemos dibujar el círculo de manera muy sencilla:
Como puedes ver, el círculo es más opaco cuanto más cercano.
Y finalmente:
1. function Circle(initialDepth, initialAngle, intensity) {
2. var angle = initialAngle;
3. this.depth = initialDepth;
4. var color = intensity;
5.
6. this.draw = function () {
7. var x = offsetX * Math.cos(angle);
8. var y = offsetY * Math.sin(angle);
9.
10. var project = perspective(0.9, canvas.width / canvas.height, x, y);
11. var diameter = circleDiameter / this.depth;
12.
13. var ploX = project.x - diameter / 2.0;
14. var ploY = project.y - diameter / 2.0;
15.
16. context.beginPath();
17. context.arc(ploX, ploY, diameter, 0, 2 * Math.PI, false);
18. context.closePath();
19.
20. var opacity = 1.0 - this.depth / maxDepth;
21. context.strokeStyle = "rgba(" + color + "," + color + "," + color + "," + opacity + ")";
22. context.lineWidth = 4;
23.
24. context.stroke();
25.
26. this.depth -= depthSpeed;
27. angle += angleSpeed;
28.
29. if (this.depth < 0) {
30. this.depth = maxDepth + this.depth;
31. }
32. };
33. };
Inicialización
Con nuestra función circle() ya podemos crear un array de círculos que vamos a inicializar cada vez más cerca de nosotros con un ligero desplazamiento del ángulo en cada iteración:
1. // Initialization
2. var circles = [];
3.
4. var angle = Math.random() * Math.PI * 2.0;
5.
6. var depth = maxDepth;
7. var depthStep = maxDepth / circlesCount;
8. var angleStep = (Math.PI * 2.0) / circlesCount;
9. for (var index = 0; index < circlesCount; index++) {
10. circles[index] = new Circle(depth, angle, index % 5 == 0 ? 200 : 255);
11.
12. depth -= depthStep;
13. angle -= angleStep;
14. }
Cálculo de Frames por Segundo (FPS)
Podemos calcular los frames por segundo midiendo la cantidad de tiempo entre dos llamadas a una función dada. En nuestro caso, la función se llamará computeFPS. Nos guardará las últimas 60 mediciones y calculará el promedio para producir el resultado deseado:
1. // FPS
2. var previous = [];
3. function computeFPS() {
4. if (previous.length > 60) {
5. previous.splice(0, 1);
6. }
7. var start = (new Date).getTime();
8. previous.push(start);
9. var sum = 0;
10.
11. for (var id = 0; id < previous.length - 1; id++) {
12. sum += previous[id + 1] - previous[id];
13. }
14.
15. var diff = 1000.0 / (sum / previous.length);
16.
17. stats.innerHTML = diff.toFixed() + " fps";
18. }
Dibujo y animaciones
El canvas es una herramienta de modo directo. Esto quiere decir que tenemos que reproducir todo el contenido del canvas cada vez que queramos cambiar algo.
Y lo primero que hay que hacer es borrar todo el contenido antes de dibujar cada frame. La mejor solución para ello es utilizar clearRect:
Para simplificar un poco este tutorial, el código de manejo del ratón ya está hecho. Puedes encontrar todo lo necesario en el archivo mouse.js.
Añadir el archivo Javascript del juego
El fondo lo manejamos mediante el archivo game.js. Tenemos que declararlo dentro de nuestra página index.htm. Así que antes del cierre de la etiqueta </body> añadimos esto:
El juego utilizará SVG (iniciales de Gráficos Vectoriales Escalables) para dibujar en pantalla los ladrillos, la raqueta y la bola. El SVG es una herramienta en modo retenido. No necesitamos redibujar los elementos completamente cada vez que queramos moverlos o modificar su aspecto.
Para añadir un SVG a nuestra página solo tenemos que insertar el código siguiente (después del canvas):
Como puedes ver, el SVG empieza con dos objetos ya definidos: un círculo para la pelota y un rectángulo para la raqueta.
Definición de constantes y variables
En el archivo game.js vamos a empezar añadiendo algunas variables:
1. // Elementos necesarios
2. var pad = document.getElementById("pad");
3. var ball = document.getElementById("ball");
4. var svg = document.getElementById("svgRoot");
5. var message = document.getElementById("message");
La pelota queda definida por los valores:
Posición
Radio
Velocidad
Dirección
Su posición anterior
1. // Pelota
2. var ballRadius = ball.r.baseVal.value;
3. var ballX;
4. var ballY;
5. var previousBallPosition = { x: 0, y: 0 };
6. var ballDirectionX;
7. var ballDirectionY;
8. var ballSpeed = 10;
La raqueta queda definida por los valores de:
Anchura
Altura
Posición
Velocidad
Inercia (solo para que el movimiento sea más suave )
1. // Raqueta
2. var padWidth = pad.width.baseVal.value;
3. var padHeight = pad.height.baseVal.value;
4. var padX;
5. var padY;
6. var padSpeed = 0;
7. var inertia = 0.80;
Los ladrillos se guardan en un array y se definen mediante estos valores:
Anchura
Altura
Distancia entre ellos (margen)
Número de líneas
Número de columnas
También necesitamos un desplazamiento y una variable para llevar la cuenta de los ladrillos destruidos.
1. // Ladrillos
2. var bricks = [];
3. var destroyedBricksCount;
4. var brickWidth = 50;
5. var brickHeight = 20;
6. var bricksRows = 5;
7. var bricksCols = 20;
8. var bricksMargin = 15;
9. var bricksTop = 20;
Y finalmente necesitamos los límites del campo de juego y una fecha de inicio para calcular la duración de la sesión.
1. // Otros valores.
2. var minX = ballRadius;
3. var minY = ballRadius;
4. var maxX;
5. var maxY;
6. var startDate;
Manejo de los ladrillos
Para crear un ladrillo vamos a necesitar una función que añada un nuevo elemento a la raíz de svg. Además configurará cada ladrillo con la información necesaria:
1. var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
2. svg.appendChild(rect);
3.
4. rect.setAttribute("width", brickWidth);
5. rect.setAttribute("height", brickHeight);
6.
7. // Color verde aleatorio
8. var chars = "456789abcdef";
9. var color = "";
10. for (var i = 0; i < 2; i++) {
11. var rnd = Math.floor(chars.length * Math.random());
12. color += chars.charAt(rnd);
13. }
14. rect.setAttribute("fill", "#00" + color + "00");
La función brick contiene también una función drawAndCollide para mostrar un ladrillo y comprobar si choca con la pelota:
1. // Función brick
2. function Brick(x, y) {
3. var isDead = false;
4. var position = { x: x, y: y };
5.
6. var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
7. svg.appendChild(rect);
8.
9. rect.setAttribute("width", brickWidth);
10. rect.setAttribute("height", brickHeight);
11.
12. // Color verde aleatorio
13. var chars = "456789abcdef";
14. var color = "";
15. for (var i = 0; i < 2; i++) {
16. var rnd = Math.floor(chars.length * Math.random());
17. color += chars.charAt(rnd);
18. }
19. rect.setAttribute("fill", "#00" + color + "00");
20.
21. this.drawAndCollide = function () {
22. if (isDead)
23. return;
24. // Dibujo
25. rect.setAttribute("x", position.x);
26. rect.setAttribute("y", position.y);
27.
28. // Colisión
29. if (ballX + ballRadius < position.x || ballX - ballRadius > position.x + brickWidth)
30. return;
31.
32. if (ballY + ballRadius < position.y || ballY - ballRadius > position.y + brickHeight)
33. return;
34.
35. // Muere
36. this.remove();
37. isDead = true;
38. destroyedBricksCount++;
39.
40. // Redibuja la pelota
41. ballX = previousBallPosition.x;
42. ballY = previousBallPosition.y;
43.
44. ballDirectionY *= -1.0;
45. };
46.
47. // Destruye el ladrillo
48. this.remove = function () {
49. if (isDead)
50. return;
51. svg.removeChild(rect);
52. };
53. }
Colisiones con la raqueta y el fondo
La pelota también tiene funciones de colisión que manejan las colisiones con la raqueta y el fondo. Estas funciones tienen que modificar la dirección de la pelota cuando se detecta un choque.
collideWithWindow comprueba los límites del campo de juego, y collideWithPad comprueba os límites de la raqueta (nosotros añadimos un sutil cambio aquí: la velocidad horizontal de la pelota se calcula utilizando la distancia con respecto al centro de la raqueta).
Movimiento de la raqueta
La raqueta se controla con el ratón o con las teclas de flecha izquierda y derecha. La función movePad es la encargada de controlar el movimiento de la raqueta y también el valor de inercia:
1. // Movimiento de la raqueta
2. function movePad() {
3. padX += padSpeed;
4.
5. padSpeed *= inertia;
6.
7. if (padX < minX)
8. padX = minX;
9.
10. if (padX + padWidth > maxX)
11. padX = maxX - padWidth;
12. }
El código que se encarga de las entradas es muy sencillo:
Antes de escribir el bucle del juego necesitamos una función que defina el tamaño del campo de juego. Llamaremos a esta función si hay que cambiar el tamaño de la ventana.
Por último, añadimos dos funciones para controlar el inicio y la finalización del juego:
1. var gameIntervalID = -1;
2. function lost() {
3. clearInterval(gameIntervalID);
4. gameIntervalID = -1;
5.
6. message.innerHTML = "Game over !";
7. message.style.visibility = "visible";
8. }
9.
10. function win() {
11. clearInterval(gameIntervalID);
12. gameIntervalID = -1;
13.
14. var end = (new Date).getTime();
15.
16. message.innerHTML = "Victory ! (" + Math.round((end - startDate) / 1000) + "s)";
17. message.style.visibility = "visible";
18. }
Conclusión
¡Ya te has convertido en desarrollador de juegos!. Con la potencia de la aceleración de gráficos por hardware hemos desarrollado un jueguecito sencillo, ¡pero con efectos especiales muy interesantes!
Y ahora te toca a ti variar el juego para convertirlo en la última versión del blockbuster!