Aprende a crear juegos en HTML5 Canvas

domingo, 1 de junio de 2014

Graficos direccionales

Muchos juegos para dar la sensación de que un personaje rota al moverse en diferentes direcciones, utiliza múltiples imágenes que solo son dibujadas cuando sprite "voltea" en una dirección. Es común ver esta clase de técnica donde le personaje camina en las cuatro direcciones primarias (arriba, derecha, abajo e izquierda), en las cuatro diagonales, o una combinación de las 8.

En realidad se trata de una técnica sencilla, que muchos de ustedes seguro ya tendrán una idea de como implementar. Pero dado que algunos aun parecen tener dudas al respecto, veremos ahora con detalle como implementarle.

Usaremos la siguiente hoja de sprites para este ejemplo:


Ya sabemos como cargar esta imagen en el código:
    var spritesheet = new Image();

        // Load assets
        spritesheet.src = 'assets/maze-sprites.png';
También usaremos la variable "elapsed" para las animaciones, la cual también sabemos como manejar en el ciclo principal dentro de la función "act":
            // Elapsed time
            elapsed += deltaTime;
            if (elapsed > 3600) {
                elapsed -= 3600;
            }
Para poder dibujar los sprites conocemos también la función. Solo falta agregar las condicionales nuevas para aceptar la cámara:
        drawImageArea: function (ctx, cam, img, sx, sy, sw, sh) {
            if (ctx !== undefined) {
                if (cam !== undefined) {
                    if (img.width) {
                        ctx.drawImage(img, sx, sy, sw, sh, this.left - cam.x, this.top - cam.y, this.width, this.height);
                    } else {
                        ctx.strokeRect(this.left - cam.x, this.top - cam.y, this.width, this.height);
                    }
                } else {
                    if (img.width) {
                        ctx.drawImage(img, sx, sy, sw, sh, this.left, this.top, this.width, this.height);
                    } else {
                        ctx.strokeRect(this.left, this.top, this.width, this.height);
                    }
                }
            }
        }
Para conocer la dirección en que se mueve un rectángulo, agregaremos una variable "dir" dentro de la función "Rectangle2D":
        dir: 0,
Esta dirección tendrá los mismos valores que en el juego de la serpiente que aprendimos a crear cuando apenas comenzábamos con el blog. Asignar dichos valores será sencillo para el jugador, pues solo debemos cambiar la variable dependiendo de la tecla presionada:
            // Move Rect
            if (pressing[KEY_UP]) {
                player.dir = 0;
                player.y -= 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.top = wall[i].bottom;
                    }
                }
            }
            if (pressing[KEY_RIGHT]) {
                player.dir = 1;
                player.x += 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.right = wall[i].left;
                    }
                }
            }
            if (pressing[KEY_DOWN]) {
                player.dir = 2;
                player.y += 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.bottom = wall[i].top;
                    }
                }
            }
            if (pressing[KEY_LEFT]) {
                player.dir = 3;
                player.x -= 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.left = wall[i].right;
                    }
                }
            }
Para conocer la dirección la dirección del enemigo, primero hemos de modificar la función "setMap" para asignar su dirección inicial:
                    if (map[row][col] === 3) {
                        enemy.vx = 5;
                        enemy.dir = 1;
                    } else if (map[row][col] === 4) {
                        enemy.vy = 5;
                        enemy.dir = 2;
                    }
Al momento de tocar pared, sumaremos en dos su dirección. Esto hará que el enemigo de media vuelta. Para contenerlo dentro de los primeros 4 valores, si su valor es mayor a 3, lo restaremos en cuatro. Esto lo repetiremos en las dos condicionales donde el enemigo puede tocar con pared:
                            enemies[i].dir += 2;
                            if (enemies[i].dir > 3) {
                                enemies[i].dir -= 4;
                            }
Finalmente, cambiamos los rectángulos rellenados por las imágenes. Ya sabemos hacer las animaciones, por lo que solo resta saber como cambiar la dirección en la que va el jugador o enemigo. Esto lo hacemos multiplicando su variable "dir" por diez en la coordenada y del área de la hoja de sprites. El código de dibujado quedaría entonces de la siguiente forma:
        // Draw player
        ctx.strokeStyle = '#0f0';
        //player.fill(ctx, cam);
        player.drawImageArea(ctx, cam, spritesheet, (~~(elapsed * 10) % 2) * 10, player.dir * 10, 10, 10);
        
        // Draw walls
        ctx.strokeStyle = '#999';
        for (i = 0, l = wall.length; i < l; i += 1) {
            //wall[i].fill(ctx, cam);
            wall[i].drawImageArea(ctx, cam, spritesheet, 20, 0, 10, 10);
        }
        
        // Draw lava
        ctx.strokeStyle = '#f00';
        for (i = 0, l = lava.length; i < l; i += 1) {
            //lava[i].fill(ctx, cam);
            lava[i].drawImageArea(ctx, cam, spritesheet, 20, 10 + (~~(elapsed * 10) % 3) * 10, 10, 10);
        }
        
        // Draw enemies
        ctx.strokeStyle = '#0ff';
        for (i = 0, l = enemies.length; i < l; i += 1) {
            //enemies[i].fill(ctx, cam);
            enemies[i].drawImageArea(ctx, cam, spritesheet, 30 + (~~(elapsed * 10) % 2) * 10, enemies[i].dir * 10, 10, 10);
        }
De esta forma, tenemos los gráficos para nuestro juego de laberinto, volteando tanto el personaje como los enemigos en la dirección a la que están caminando.

Código final:

[Canvas not supported by your browser]
/*jslint bitwise: true, es5: true */
(function (window, undefined) {
    'use strict';
    var KEY_ENTER = 13,
        KEY_LEFT = 37,
        KEY_UP = 38,
        KEY_RIGHT = 39,
        KEY_DOWN = 40,
        
        canvas = null,
        ctx = null,
        lastPress = null,
        pressing = [],
        pause = false,
        gameover = true,
        currentMap = 0,
        worldWidth = 0,
        worldHeight = 0,
        elapsed = 0,
        cam = null,
        player = null,
        wall = [],
        lava = [],
        enemies = [],
        maps = [],
        spritesheet = new Image();
    
    maps[0] = [
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 2, 2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0, 0, 1],
        [0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0, 0, 0],
        [0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0, 0, 0],
        [1, 1, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    ];
    maps[1] = [
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1],
        [1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 2, 0, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 0, 0, 2, 2, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 1, 0, 2, 2, 2, 2, 0, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 1, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 1, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 2, 1, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 1, 1, 1, 0, 0, 0, 0, 1, 2, 2, 2, 2, 1, 1, 0, 1, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 1, 0, 0, 0, 0, 1, 1, 1, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 1, 0, 0, 0, 0, 1, 2, 2, 2, 2, 1, 1, 0, 1, 1, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 1, 0, 1, 1, 2, 2, 2, 1, 0, 0, 0, 0, 1, 1, 1, 1],
        [1, 0, 0, 1, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 1, 0, 1, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 1, 0, 1, 2, 2, 2, 2, 1, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 1, 0, 2, 2, 2, 2, 0, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 2, 0, 2, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 0, 0, 2, 2, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 2, 2, 2, 0, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 2, 2, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 2, 0, 2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 3, 0, 1],
        [1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1],
        [1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    ];
    maps[2] = [
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
        [1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 1, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 2, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        [1, 0, 2, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 1, 1, 1, 2, 0, 1],
        [1, 2, 2, 1, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 1, 2, 2, 1],
        [1, 1, 1, 1, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1],
        [1, 2, 2, 0, 0, 4, 0, 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 4, 0, 0, 2, 2, 1],
        [1, 2, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 2, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 0, 0, 0, 2, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 1, 1, 2, 2, 0, 0, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 1, 1, 1],
        [1, 1, 2, 2, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
        [1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 1, 2, 2, 2, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 1, 1, 1, 0, 2, 2, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 2, 1, 1, 1, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 1],
        [1, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    ];

    function Camera() {
        this.x = 0;
        this.y = 0;
    }

    Camera.prototype = {
        constructor: Camera,
        
        focus: function (x, y) {
            this.x = x - canvas.width / 2;
            this.y = y - canvas.height / 2;

            if (this.x < 0) {
                this.x = 0;
            } else if (this.x > worldWidth - canvas.width) {
                this.x = worldWidth - canvas.width;
            }
            if (this.y < 0) {
                this.y = 0;
            } else if (this.y > worldHeight - canvas.height) {
                this.y = worldHeight - canvas.height;
            }
        }
    };
    
    function Rectangle2D(x, y, width, height, createFromTopLeft) {
        this.width = (width === undefined) ? 0 : width;
        this.height = (height === undefined) ? this.width : height;
        if (createFromTopLeft) {
            this.left = (x === undefined) ? 0 : x;
            this.top = (y === undefined) ? 0 : y;
        } else {
            this.x = (x === undefined) ? 0 : x;
            this.y = (y === undefined) ? 0 : y;
        }
    }
    
    Rectangle2D.prototype = {
        constructor: Rectangle2D,
        left: 0,
        top: 0,
        width: 0,
        height: 0,
        vx: 0,
        vy: 0,
        dir: 0,
        
        get x() {
            return this.left + this.width / 2;
        },
        set x(value) {
            this.left = value - this.width / 2;
        },
        
        get y() {
            return this.top + this.height / 2;
        },
        set y(value) {
            this.top = value - this.height / 2;
        },
        
        get right() {
            return this.left + this.width;
        },
        set right(value) {
            this.left = value - this.width;
        },
        
        get bottom() {
            return this.top + this.height;
        },
        set bottom(value) {
            this.top = value - this.height;
        },
        
        intersects: function (rect) {
            if (rect !== undefined) {
                return (this.left < rect.right &&
                    this.right > rect.left &&
                    this.top < rect.bottom &&
                    this.bottom > rect.top);
            }
        },
        
        fill: function (ctx) {
            if (ctx !== undefined) {
                if (cam !== undefined) {
                    ctx.fillRect(this.left - cam.x, this.top - cam.y, this.width, this.height);
                } else {
                    ctx.fillRect(this.left, this.top, this.width, this.height);
                }
            }
        },
        
        drawImageArea: function (ctx, cam, img, sx, sy, sw, sh) {
            if (ctx !== undefined) {
                if (cam !== undefined) {
                    if (img.width) {
                        ctx.drawImage(img, sx, sy, sw, sh, this.left - cam.x, this.top - cam.y, this.width, this.height);
                    } else {
                        ctx.strokeRect(this.left - cam.x, this.top - cam.y, this.width, this.height);
                    }
                } else {
                    if (img.width) {
                        ctx.drawImage(img, sx, sy, sw, sh, this.left, this.top, this.width, this.height);
                    } else {
                        ctx.strokeRect(this.left, this.top, this.width, this.height);
                    }
                }
            }
        }
    };

    document.addEventListener('keydown', function (evt) {
        lastPress = evt.which;
        pressing[evt.which] = true;
    }, false);

    document.addEventListener('keyup', function (evt) {
        pressing[evt.which] = false;
    }, false);

    function setMap(map, blockSize) {
        var col = 0,
            row = 0,
            columns = 0,
            rows = 0,
            enemy = null;
        wall.length = 0;
        lava.length = 0;
        enemies.length = 0;
        for (row = 0, rows = map.length; row < rows; row += 1) {
            for (col = 0, columns = map[row].length; col < columns; col += 1) {
                if (map[row][col] === 1) {
                    wall.push(new Rectangle2D(col * blockSize, row * blockSize, blockSize, blockSize, true));
                } else if (map[row][col] === 2) {
                    lava.push(new Rectangle2D(col * blockSize, row * blockSize, blockSize, blockSize, true));
                } else if (map[row][col] > 2) {
                    enemy = new Rectangle2D(col * blockSize, row * blockSize, blockSize, blockSize, true);
                    if (map[row][col] === 3) {
                        enemy.vx = 5;
                        enemy.dir = 1;
                    } else if (map[row][col] === 4) {
                        enemy.vy = 5;
                        enemy.dir = 2;
                    }
                    enemies.push(enemy);
                }
            }
        }
        worldWidth = columns * blockSize;
        worldHeight = rows * blockSize;
    }

    function reset() {
        player.dir = 1;
        player.left = 40;
        player.top = 100;
        gameover = false;
    }

    function paint(ctx) {
        var i = 0,
            l = 0;
        
        // Clean canvas
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Draw player
        ctx.strokeStyle = '#0f0';
        //player.fill(ctx, cam);
        player.drawImageArea(ctx, cam, spritesheet, (~~(elapsed * 10) % 2) * 10, player.dir * 10, 10, 10);
        
        // Draw walls
        ctx.strokeStyle = '#999';
        for (i = 0, l = wall.length; i < l; i += 1) {
            //wall[i].fill(ctx, cam);
            wall[i].drawImageArea(ctx, cam, spritesheet, 20, 0, 10, 10);
        }
        
        // Draw lava
        ctx.strokeStyle = '#f00';
        for (i = 0, l = lava.length; i < l; i += 1) {
            //lava[i].fill(ctx, cam);
            lava[i].drawImageArea(ctx, cam, spritesheet, 20, 10 + (~~(elapsed * 10) % 3) * 10, 10, 10);
        }
        
        // Draw enemies
        ctx.strokeStyle = '#0ff';
        for (i = 0, l = enemies.length; i < l; i += 1) {
            //enemies[i].fill(ctx, cam);
            enemies[i].drawImageArea(ctx, cam, spritesheet, 30 + (~~(elapsed * 10) % 2) * 10, enemies[i].dir * 10, 10, 10);
        }

        // Debug last key pressed
        ctx.fillStyle = '#fff';
        ctx.fillText('Last Press: ' + lastPress, 0, 20);
        
        // Draw pause
        if (pause) {
            ctx.textAlign = 'center';
            if (gameover) {
                ctx.fillText('GAMEOVER', 150, 100);
            } else {
                ctx.fillText('PAUSE', 150, 100);
            }
            ctx.textAlign = 'left';
        }
    }

    function act(deltaTime) {
        var i = 0,
            l = 0,
            j = 0,
            jl = 0;
        
        if (!pause) {
            // GameOver Reset
            if (gameover) {
                reset();
            }

            // Move Rect
            if (pressing[KEY_UP]) {
                player.dir = 0;
                player.y -= 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.top = wall[i].bottom;
                    }
                }
            }
            if (pressing[KEY_RIGHT]) {
                player.dir = 1;
                player.x += 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.right = wall[i].left;
                    }
                }
            }
            if (pressing[KEY_DOWN]) {
                player.dir = 2;
                player.y += 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.bottom = wall[i].top;
                    }
                }
            }
            if (pressing[KEY_LEFT]) {
                player.dir = 3;
                player.x -= 5;
                for (i = 0, l = wall.length; i < l; i += 1) {
                    if (player.intersects(wall[i])) {
                        player.left = wall[i].right;
                    }
                }
            }

            // Out Screen
            if (player.x > worldWidth) {
                currentMap += 1;
                if (currentMap > maps.length - 1) {
                    currentMap = 0;
                }
                setMap(maps[currentMap], 10);
                player.x = 0;
            }
            if (player.y > worldHeight) {
                player.y = 0;
            }
            if (player.x < 0) {
                currentMap -= 1;
                if (currentMap < 0) {
                    currentMap = maps.length - 1;
                }
                setMap(maps[currentMap], 10);
                player.x = worldWidth;
            }
            if (player.y < 0) {
                player.y = worldHeight;
            }

            // Move enemies
            for (i = 0, l = enemies.length; i < l; i += 1) {
                if (enemies[i].vx !== 0) {
                    enemies[i].x += enemies[i].vx;

                    for (j = 0, jl = wall.length; j < jl; j += 1) {
                        if (enemies[i].intersects(wall[j])) {
                            enemies[i].vx *= -1;
                            enemies[i].x += enemies[i].vx;
                            enemies[i].dir += 2;
                            if (enemies[i].dir > 3) {
                                enemies[i].dir -= 4;
                            }
                            break;
                        }
                    }
                }

                if (enemies[i].vy !== 0) {
                    enemies[i].y += enemies[i].vy;

                    for (j = 0, jl = wall.length; j < jl; j += 1) {
                        if (enemies[i].intersects(wall[j])) {
                            enemies[i].vy *= -1;
                            enemies[i].y += enemies[i].vy;
                            enemies[i].dir += 2;
                            if (enemies[i].dir > 3) {
                                enemies[i].dir -= 4;
                            }
                            break;
                        }
                    }
                }

                // Player Intersects Enemy
                if (player.intersects(enemies[i])) {
                    gameover = true;
                    pause = true;
                }
            }

            // Player Intersects Lava
            for (i = 0, l = lava.length; i < l; i += 1) {
                if (player.intersects(lava[i])) {
                    gameover = true;
                    pause = true;
                }
            }

            // Focus player
            cam.focus(player.x, player.y);

            // Elapsed time
            elapsed += deltaTime;
            if (elapsed > 3600) {
                elapsed -= 3600;
            }
        }
        // Pause/Unpause
        if (lastPress === KEY_ENTER) {
            pause = !pause;
            lastPress = null;
        }
    }

    function repaint() {
        window.requestAnimationFrame(repaint);
        paint(ctx);
    }

    function run() {
        setTimeout(run, 50);
        act(0.05);
    }

    function init() {
        // Get canvas and context
        canvas = document.getElementById('canvas');
        ctx = canvas.getContext('2d');
        canvas.width = 300;
        canvas.height = 200;
        worldWidth = canvas.width;
        worldHeight = canvas.height;
        
        // Load assets
        spritesheet.src = 'assets/maze-sprites.png';
        
        // Create camera and player
        cam = new Camera();
        player = new Rectangle2D(40, 40, 10, 10, true);

        // Set initial map
        setMap(maps[0], 10);
        
        // Start game
        run();
        repaint();
    }

    window.addEventListener('load', init, false);
}(window));
Regresar al índice

19 comentarios:

  1. Intersante como lo pusiste, yo lo resolvi de otra, manera, me gustan tus tutoriales, pero me encantaria si continuaras con los de node.js,pero como siempre, gracias!

    ResponderEliminar
    Respuestas
    1. ¡Muchas gracias!

      Con respecto a Node.js, por ahora hay muchos temas pendientes antes de seguir con los avanzados, y creo que las bases de Node.js ya están bastante solidas por ahora, pero si crees que falta algo que necesites, avísame para poder preparar material nuevo al respecto.

      Eliminar
  2. Hola .. me gustan mucho tus blogs .. tienes mucha creatividad y conocimientos :D
    Mi consulta es la siguiente ... el tema de laberintos y muros me hizo recordar a un tema que estuve investigando hace poco : El "Ray Casting" .. un truquillo que se usaba para simular 3D en los juegos antiguos como el "Wolfestein 3d" por ejemplo ... me preguntaba si conocías esta técnica, he visto algunos ejemplos aplicados en Html5 y me parecía que tal vez te interesaría incluirlo en tu blog ... por supuesto si es que no te molesta y tienes un tiempo de sobra

    Aquí hay un poco de teoria por si a alguien le interesa http://lodev.org/cgtutor/raycasting.html

    ResponderEliminar
    Respuestas
    1. Hola Lucas, muchas gracias por tu comentario.

      Antiguamente durante la planeación de los temas, se había considerado expandir este tema precisamente para incluir la técnica de RayCasting. Sin embargo, con la popularidad y poder de WebGL para juegos web en 3D, concluimos que era un tema de interés menor, cuyo tiempo de desarrollo podría ser invertido mejor en otras técnicas que pueden interesar a más gente.

      De igual forma, agradezco mucho tu opinión al respecto, y espero que tengas mucha suerte en tu implementación sobre esta técnica.

      Eliminar
    2. Gracias por la responderlo tan rápido.

      Suele resultar muy interesante el RayCasting cuando lo que buscas es algo como un tributo a la "cultura" de los videojuegos ... yo lo considero algo importante en el camino del aprendizaje porque ... es una técnica que no requiere ayuda externa, obviamente el WebGl va a tener un resultado sumamente superior (sin mencionar que el RayCasting tiene claras limitaciones) pero.... es una de las cosas hermosas de la programación ... código propio.. casi personal...entendiendo las bases(o comienzos mejor dicho) del 3D... la visión de uno mismo sobre los videojuegos y programación de seguro será distinta a la de tener todo servido en un estándar de gráficos avanzado.

      ahh.. solo quería decirloo :D jaja .. este es un muy buen espacio para compartir nuestras opiniones de programación (lo siento si tal vez arruinan la esencia del tutorial)

      Eliminar
    3. ¡Pero por el contrario! Son comentarios como el tuyo los que enriquecen este sitio, y te agradezco mucho por ello.

      Tienes mucha razón en ello, esta técnica es una parte muy importante dentro de la historia de la evolución de los videojuegos, y aún cuando prefiero enfocarme más al desarrollo de técnicas que aún se usan hoy día en muchos juegos, no descarto que en un futuro posterior, pueda llegar a tocar este tema dado el mismo valor que tú comentas sobre él.

      Gracias por tu opinión. Ha sido una experiencia muy positiva para el crecimiento del blog.

      Eliminar
  3. Tengo una pregunta como podría hacer para que el mapa avanzara solo? No se si me explico es decir, el player estático y el mapa es el que avanza hacia él. Saludos!

    ResponderEliminar
    Respuestas
    1. No se si lo que te refieras sea quizá algo como lo que vimos en "Un juego de naves", o quizá sea lo de desactivar el ajuste de la cámara al mapa (¿Que el personaje siempre esté en el centro y el resto se mueva?).

      Si no es ninguno de estos casos, ¿Podrías explicarme mejor lo que deseas?

      Eliminar
    2. Este comentario ha sido eliminado por el autor.

      Eliminar
  4. Exacto! Necesito que el personaje este inmóvil y el resto (el escenario) se mueva. Gracias por responder tan rápido y son geniales tus tutoriales!

    ResponderEliminar
    Respuestas
    1. Cambia la función focus de tu cámara a que solo tenga las dos primeras líneas, de esta forma:

      Camera.prototype.focus=function(x,y){
      this.x=x-canvas.width/2;
      this.y=y-canvas.height/2;
      }

      Eliminar
    2. Hola! Gracias por responder, pero el resultado no es el que esperaba mi idea es que el mapa corra solo, con un intervalo de tiempo o velocidad, es decir que vaya hacia el jugador al estilo de el de las naves quizás, perdona por explicarme tan mal. Saludos.

      Eliminar
    3. En tal caso, quizá sea más sencillo hacer lo mismo que se hace en el juego de naves, solo moviendo los objetos de forma horizontal en lugar de vertical. ¡Suerte!

      Eliminar
  5. buenas disculpa quizas no tengas tiempo pero igual queria preguntarte si podrias agregar un pequeño apartado o si podrias explicarlo en un comentario no se... sobre como crear una mochila para cargar items osea la logica que no la tengo muy clara, no se si me entendiste bueno espero que si y gracias de antemano!

    ResponderEliminar
    Respuestas
    1. La mochila es un arreglo con los objetos que tienes. Al pausar, por ejemplo, puedes mostrarles en pantalla, y al presionar el botón de acción, "consumir" el ítem cuyo identificador estés encima. Sí tienes aún dudas después de esta explicación, prepararé material para explicar a detalle dicha función.

      Eliminar
    2. gracias mas o menos entendi el concepto pero si no es molestia me gustaria si tienes tiempo que explicaras a detalle para salir de toda duda posible

      Eliminar
  6. bueno y otra cosa no he visto que nadie a preguntado pero si hago que el suelo tenga un sprite y agrego un enemigo por ejemplo en la ubicacion donde cree el enemigo se queda del color original del canvas como hago para resolverlo? a mi entender no sera mas facil con ... capas? a la hora de dibujarle las imagenes...

    ResponderEliminar
  7. buenas, buenos tus aportes xD queria decir ando tratando de hacer un rpg y hasta el momento solo tengo un inconveniente me di cuenta que dibujar el piso o mas bien los 0 del arreglo dan un bajon drasticos a los fps alguna ayuda con eso?

    ResponderEliminar
    Respuestas
    1. ¿Podría ver tu código? Lo que dices no debería ocurrir, pero una vez realicé algo similar, y me di cuenta más tarde arte el FPS bajaba drásticamente porque puse el dibujado de un elemento dentro del ciclo de todo el mapa, repitiéndole innecesariamente múltiples veces. Revisa si tu error no resulta ser algo similar.

      Eliminar