|
|
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.
La versión final la puedes ver en: http://www.catuhe.com/ms/en/index.htm

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 )
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>
, añadiremos el código siguiente:
1. <script type="text/javascript" src="background.js"></script>
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.
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");
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:
1. context.beginPath();
2. context.arc(ploX, ploY, diameter, 0, 2 * Math.PI, false);
3. context.closePath();
4.
5. var opacity = 1.0 - this.depth / maxDepth;
6. context.strokeStyle = "rgba(" + color + "," + color + "," + color + "," + opacity + ")";
7. context.lineWidth = 4;
8.
9. context.stroke();
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. };
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. }
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. }
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:
1. // Drawing & Animation
2. function clearCanvas() {
3. context.clearRect(0, 0, canvas.width, canvas.height);
4. }
Y el código completo para dibujar el agujero de gusano quedaría así:
1. function wormHole() {
2. computeFPS();
3. canvas.width = window.innerWidth;
4. canvas.height = window.innerHeight - 130 - 40;
5. clearCanvas();
6. for (var index = 0; index < circlesCount; index++) {
7. circles[index].draw();
8. }
9.
10. circles.sort(function (a, b) {
11. if (a.depth > b.depth)
12. return -1;
13. if (a.depth < b.depth)
14. return 1;
15. return 0;
16. });
17. }
El código de clasificación se utiliza para evitar que los círculos se superpongan.
Para terminar con el fondo, necesitamos colocar un botón de modo que permita mostrar u ocultar el fondo mismo:
1.
2. var wormHoleIntervalID = -1;
3.
4. function startWormHole() {
5. if (wormHoleIntervalID > -1)
6. clearInterval(wormHoleIntervalID);
7.
8. wormHoleIntervalID = setInterval(wormHole, 16);
9.
10. document.getElementById("wormHole").onclick = stopWormHole;
11. document.getElementById("wormHole").innerHTML = "Standard Mode";
12. }
13.
14. function stopWormHole() {
15. if (wormHoleIntervalID > -1)
16. clearInterval(wormHoleIntervalID);
17.
18. clearCanvas();
19. document.getElementById("wormHole").onclick = startWormHole;
20. document.getElementById("wormHole").innerHTML = "Wormhole Mode";
21. }
22.
23. stopWormHole();
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.

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:
1. <script type="text/javascript" src="game.js"></script>
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):
1. <svg id="svgRoot">
2. <circle cx="100" cy="100" r="10" id="ball" />
3. <rect id="pad" height="15px" width="150px" x="200" y="200" rx="10" ry="20"/>
4. </svg>
Como puedes ver, el SVG empieza con dos objetos ya definidos: un círculo para la pelota y un rectángulo para la raqueta.
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:
La raqueta queda definida por los valores de:
Los ladrillos se guardan en un array y se definen mediante estos valores:
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;
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. this.drawAndCollide = function () {
2. if (isDead)
3. return;
4. // Dibujo
5. rect.setAttribute("x", position.x);
6. rect.setAttribute("y", position.y);
7.
8. // Colisión
9. if (ballX + ballRadius < position.x || ballX - ballRadius > position.x + brickWidth)
10. return;
11.
12. if (ballY + ballRadius < position.y || ballY - ballRadius > position.y + brickHeight)
13. return;
14.
15. // Muere
16. this.remove();
17. isDead = true;
18. destroyedBricksCount++;
19.
20. // Redibuja la pelota
21. ballX = previousBallPosition.x;
22. ballY = previousBallPosition.y;
23.
24. ballDirectionY *= -1.0;
25. };
Por último, la función brick completa sería así:
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. }
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.
1. // Colisiones
2. function collideWithWindow() {
3. if (ballX < minX) {
4. ballX = minX;
5. ballDirectionX *= -1.0;
6. }
7. else if (ballX > maxX) {
8. ballX = maxX;
9. ballDirectionX *= -1.0;
10. }
11.
12. if (ballY < minY) {
13. ballY = minY;
14. ballDirectionY *= -1.0;
15. }
16. else if (ballY > maxY) {
17. ballY = maxY;
18. ballDirectionY *= -1.0;
19. lost();
20. }
21. }
22.
23. function collideWithPad() {
24. if (ballX + ballRadius < padX || ballX - ballRadius > padX + padWidth)
25. return;
26.
27. if (ballY + ballRadius < padY)
28. return;
29.
30. ballX = previousBallPosition.x;
31. ballY = previousBallPosition.y;
32. ballDirectionY *= -1.0;
33.
34. var dist = ballX - (padX + padWidth / 2);
35.
36. ballDirectionX = 2.0 * dist / padWidth;
37.
38. var square = Math.sqrt(ballDirectionX * ballDirectionX + ballDirectionY * ballDirectionY);
39. ballDirectionX /= square;
40. ballDirectionY /= square;
41. }
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).
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:
1. registerMouseMove(document.getElementById("gameZone"), function (posx, posy, previousX, previousY) {
2. padSpeed += (posx - previousX) * 0.2;
3. });
4.
5. window.addEventListener('keydown', function (evt) {
6. switch (evt.keyCode) {
7. // flecha izquierda
8. case 37:
9. padSpeed -= 10;
10. break;
11. // Flecha derecha
12. case 39:
13. padSpeed += 10;
14. break;
15. }
16. }, true);
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.
1. function checkWindow() {
2. maxX = window.innerWidth - minX;
3. maxY = window.innerHeight - 130 - 40 - minY;
4. padY = maxY - 30;
5. }
Y finalmente, el bucle del juego es el orquestador, que tienes aquí:
1. function gameLoop() {
2. movePad();
3.
4. // Movimientos
5. previousBallPosition.x = ballX;
6. previousBallPosition.y = ballY;
7. ballX += ballDirectionX * ballSpeed;
8. ballY += ballDirectionY * ballSpeed;
9.
10. // Colisiones
11. collideWithWindow();
12. collideWithPad();
13.
14. // ladrillos
15. for (var index = 0; index < bricks.length; index++) {
16. bricks[index].drawAndCollide();
17. }
18.
19. // pelota
20. ball.setAttribute("cx", ballX);
21. ball.setAttribute("cy", ballY);
22.
23. // Raqueta
24. pad.setAttribute("x", padX);
25. pad.setAttribute("y", padY);
26.
27. // Has ganado ?
28. if (destroyedBricksCount == bricks.length) {
29. win();
30. }
31. }
El primer paso de la inicialización consiste en crear los ladrillos:
1. function generateBricks() {
2. // Eliminar los anteriores
3. for (var index = 0; index < bricks.length; index++) {
4. bricks[index].remove();
5. }
6.
7. // Creamos los ladrillos nuevos
8. var brickID = 0;
9.
10. var offset = (window.innerWidth - bricksCols * (brickWidth + bricksMargin)) / 2.0;
11.
12. for (var x = 0; x < bricksCols; x++) {
13. for (var y = 0; y < bricksRows; y++) {
14. bricks[brickID++] = new Brick(offset + x * (brickWidth + bricksMargin), y * (brickHeight + bricksMargin) + bricksTop);
15. }
16. }
17. }
El paso siguiente será definir las variables que utilizaremos en el juego:
1. function initGame() {
2. message.style.visibility = "hidden";
3.
4. checkWindow();
5.
6. padX = (window.innerWidth - padWidth) / 2.0;
7.
8. ballX = window.innerWidth / 2.0;
9. ballY = maxY - 60;
10.
11. previousBallPosition.x = ballX;
12. previousBallPosition.y = ballY;
13.
14. padSpeed = 0;
15.
16. ballDirectionX = Math.random();
17. ballDirectionY = -1.0;
18.
19. generateBricks();
20. gameLoop();
21. }
Cada vez que el usuario cambie el tamaño de la ventana tendremos que reinicializarlo:
1. window.onresize = initGame;
Después tenemos que vincular un manejador de eventos al botón de “nueva partida”:
1. var gameIntervalID = -1;
2. function startGame() {
3. initGame();
4.
5. destroyedBricksCount = 0;
6.
7. if (gameIntervalID > -1)
8. clearInterval(gameIntervalID);
9.
10. startDate = (new Date()).getTime(); ;
11. gameIntervalID = setInterval(gameLoop, 16);
12. }
13.
14. document.getElementById("newGame").onclick = startGame;
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. }
¡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!