Aprende a crear juegos en HTML5 Canvas

domingo, 13 de julio de 2014

Proyección isométrica

Anteriormente vimos como dibujar un mapa convirtiendo un arreglo de datos en arreglos de objetos, y posteriormente dibujando estos arreglos en pantalla para crear el mapa. Existe una forma alterna de dibujar estos mapas directo del arreglo de datos, que es prácticamente una copia de la función "setMap", cambiando los empujes a los arreglos por el dibujado de los elementos del mapa:
    function fillMap(ctx, map, blockSize) {
        var col = 0,
            row = 0,
            columns = 0,
            rows = 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) {
                    ctx.drawImage(spritesheet, 32, 16, 16, 16, col * blockSize, row * blockSize, blockSize, blockSize);
                } else if (map[row][col] === 2) {
                    ctx.drawImage(spritesheet, 32, 32 + (~~(elapsed * 10) % 2) * 16, 16, 16, col * blockSize, row * blockSize, blockSize, blockSize);
                } else {
                    ctx.drawImage(spritesheet, 32, 0, 16, 16, col * blockSize, row * blockSize, blockSize, blockSize);
                }
            }
        }
    }
Posteriormente esta función es llamada desde la función "paint" de la siguiente forma:
        fillMap(ctx, maps[currentMap], 10);
Ahora, existen pocos casos en los que es necesario dibujar directo del arreglo de datos, en realidad solo es importante hacerlo de esta forma si el orden en que se dibujen los elementos del mapa es importante, y si se dibujarán todos los elementos del mapa, incluyendo los vacíos (el suelo). Sin embargo, se de un caso en que es importante que de cumplan estos dos casos, y es justamente el tema que veremos hoy: La proyección isométrica.

La proyección isométrica es una técnica en los videojuegos usada en sus orígenes para representar un escenario con perspectiva tridimensional en un juego bidimencional. Es usada principalmente en juegos de laberintos como el que aprendimos a hacer hace poco, así que, sin modificar ni una línea de su lógica de acciones, usaremos ese ejemplo para aprender como convertir nuestro juego de una proyección de vista superior, a una isométrica.

...De acuerdo, tuve que cambiar los tamaños de los bloques de 10 pixeles a 16, por que las medidas pequeñas no dejaban apreciar bien la técnica...

Comencemos cambiando nuestras imágenes por las nuevas en perspectiva isométrica:


Como podrás notar, son exactamente las mismas imágenes que antes en la misma posición, así que hacer la transición será bastante sencillo. El único cambio que se ha hecho, es que sacrificamos un bloque de lava a cambio de obtener uno de suelo.

Para poder dibujar en isométrico, debemos estar consientes que el sistema de coordenadas ha cambiado ligeramente. Dado que los bloques están "inclinados", ya no los dibujaremos directo en su posición x,y... Sin embargo, obtener sus nuevas coordenadas no es demasiado complicado, solo hay que seguir estas formulas a la hora de dibujar los objetos:
destinationX = posX / 2 - posY / 2;
destinationY = posX / 4 + posY / 4;
Estas formulas son todo lo que necesitamos para dibujar nuestro juego en proyección ortográfica. Ahora apliquemos esta información. Comencemos por lo sencillo, crearemos una copia de la función "Rectangle2D.drawImageArea" y la renombraremos como "Rectangle2D.drawIsoImageArea". La función será idéntica, aplicando tan solo las formulas anteriores, además de agregar el nuevo parámetro z, que indicará a que altura se dibujará el objeto sobre el nivel del suelo. Este valor se aplica restando z al dibujar el destino Y del dibujo, quedando la función de la siguiente forma:
        drawIsoImageArea: function (ctx, cam, img, z, sx, sy, sw, sh) {
            if (ctx !== undefined) {
                z = (z === undefined) ? 0 : z;
                if (cam !== undefined) {
                    if (img.width) {
                        ctx.drawImage(img, sx, sy, sw, sh, this.left / 2 - this.top / 2 - cam.x, this.left / 4 + this.top / 4 - z - cam.y, this.width, this.height);
                    } else {
                        ctx.strokeRect(this.left / 2 - this.top / 2 - cam.x, this.left / 4 + this.top / 4 - z - cam.y, this.width, this.height);
                    }
                } else {
                    if (img.width) {
                        ctx.drawImage(img, sx, sy, sw, sh, this.left / 2 - this.top / 2, this.left / 4 + this.top / 4 - z, this.width, this.height);
                    } else {
                        ctx.strokeRect(this.left / 2 - this.top / 2, this.left / 4 + this.top / 4 - z, this.width, this.height);
                    }
                }
            }
        }
La cámara también necesita enfocar el escenario de acuerdo a la posición isométrica de los objetos, por lo que crearemos una función de enfoque isométrico. Dado que de igual forma se verá siempre parte del exterior del escenario, no agregaremos las formulas para contener la cámara dentro de los límites del mundo, así que haremos una función simple que siempre centre la coordenada dada:
        isoFocus: function (x, y) {
            this.x = (x / 2) - (y / 2) - canvas.width / 2;
            this.y = (x / 4) + (y / 4) - canvas.height / 2;
        }
Finalmente, aplicaremos lo aprendido a la recientemente aprendida función "fillMap", dibujando los objetos con el nuevo sistema de coordenadas isométricas:
    function fillIsoMap(ctx, map, blockSize) {
        var col = 0,
            row = 0,
            columns = 0,
            rows = 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) {
                    // Draw wall
                    ctx.drawImage(spritesheet, 32, 16, 16, 16, (col * blockSize / 2) - (row * blockSize / 2) - cam.x, (row * blockSize / 4) + (col * blockSize / 4) - cam.y, blockSize, blockSize);
                } else if (map[row][col] === 2) {
                    // Draw lava
                    ctx.drawImage(spritesheet, 32, 32 + (~~(elapsed * 10) % 2) * 16, 16, 16, (col * blockSize / 2) - (row * blockSize / 2) - cam.x, (row * blockSize / 4) + (col * blockSize / 4) - cam.y, blockSize, blockSize);
                } else {
                    // Draw soil
                    ctx.drawImage(spritesheet, 32, 0, 16, 16, (col * blockSize / 2) - (row * blockSize / 2) - cam.x, (row * blockSize / 4) + (col * blockSize / 4) - cam.y, blockSize, blockSize);
                }
            }
        }
    }
Ahora sí, dibujamos todo esto en la función "paint". Comencemos por dibujar el mapa y el personaje principal:
        // Draw map
        fillIsoMap(ctx, maps[currentMap], 16);

        // Draw player
        player.drawIsoImageArea(ctx, cam, spritesheet, 0, (~~(elapsed * 10) % 2) * 16, player.dir * 16, 16, 16);
Si probamos el juego, veremos que todo se ve muy bien, pero las coordenadas del jugador parecen estar desfasadas. Esto es debido a que estamos dibujando al jugador desde el nivel inferior del bloque, y nosotros necesitamos ponerlo sobre su superficie. Este efecto se logra fácil aumentando su coordenada z a la mitad de la altura de los bloques, que vendría siendo 8. Finalmente, dibujamos a los enemigos, tomando en cuenta también este desface en z:
        // Draw enemies
        for (i = 0, l = enemies.length; i < l; i += 1) {
            enemies[i].drawIsoImageArea(ctx, cam, spritesheet, 8, 48 + (~~(elapsed * 10) % 2) * 16, enemies[i].dir * 16, 16, 16);
        }
¡Listo! Esto es todo lo que necesitamos para cambiar una proyección de vista superior a una proyección isométrica, y nuestro juego se verá como en el ejemplo más abajo.

"Pero eso no se ve tan tercera dimensión", podrás decirme.. Bueno, para ello es necesario agregar un desface en el nivel z de los objetos en el mapa. Por poner un ejemplo, se puede agregar una segunda capa de objetos tipo pared, agregando a ellos la media altura del bloque para el desface en z:
                ctx.drawImage(spritesheet, blockSize * 2, blockSize, blockSize, blockSize,  (col * blockSize / 2) - (row * blockSize / 2) - cam.x, (col * blockSize / 4) + (row * blockSize / 4) - blockSize / 2 - cam.y, blockSize, blockSize);
También he disminuido un cuarto de altura a la lava para que parezca que está en un agujero sobre el suelo:
                ctx.drawImage(spritesheet, 32, 32 + (~~(elapsed * 10) % 2) * 16, 16, 16,  (col * blockSize / 2) - (row * blockSize / 2) - cam.x, (col * blockSize / 4) + (row * blockSize / 4) + blockSize / 4 - cam.y, blockSize, blockSize);
El ejemplo de como queda este resultado, puedes encontrarlo en el siguiente enlace: http://canvas.ninja/?js=isometric1b. Sin embargo, pronto podrás notar que el personaje y los enemigos pueden caminar sobre las paredes. Para eliminar este defecto visual, necesitaremos aprender nuevas técnicas de dibujado más complejas, pero eso lo aprenderemos hasta la siguiente semana.

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;
            }
        },
        
        isoFocus: function (x, y) {
            this.x = (x / 2) - (y / 2) - canvas.width / 2;
            this.y = (x / 4) + (y / 4) - canvas.height / 2;
        }
    };
    
    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);
                    }
                }
            }
        },
        
        drawIsoImageArea: function (ctx, cam, img, z, sx, sy, sw, sh) {
            if (ctx !== undefined) {
                z = (z === undefined) ? 0 : z;
                if (cam !== undefined) {
                    if (img.width) {
                        ctx.drawImage(img, sx, sy, sw, sh, this.left / 2 - this.top / 2 - cam.x, this.left / 4 + this.top / 4 - z - cam.y, this.width, this.height);
                    } else {
                        ctx.strokeRect(this.left / 2 - this.top / 2 - cam.x, this.left / 4 + this.top / 4 - z - cam.y, this.width, this.height);
                    }
                } else {
                    if (img.width) {
                        ctx.drawImage(img, sx, sy, sw, sh, this.left / 2 - this.top / 2, this.left / 4 + this.top / 4 - z, this.width, this.height);
                    } else {
                        ctx.strokeRect(this.left / 2 - this.top / 2, this.left / 4 + this.top / 4 - z, 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 = 8;
                        enemy.dir = 1;
                    } else if (map[row][col] === 4) {
                        enemy.vy = 8;
                        enemy.dir = 2;
                    }
                    enemies.push(enemy);
                }
            }
        }
        worldWidth = columns * blockSize;
        worldHeight = rows * blockSize;
    }

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

    /*function fillMap(ctx, map, blockSize) {
        var col = 0,
            row = 0,
            columns = 0,
            rows = 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) {
                    ctx.drawImage(spritesheet, 32, 16, 16, 16, col * blockSize, row * blockSize, blockSize, blockSize);
                } else if (map[row][col] === 2) {
                    ctx.drawImage(spritesheet, 32, 32 + (~~(elapsed * 10) % 2) * 16, 16, 16, col * blockSize, row * blockSize, blockSize, blockSize);
                } else {
                    ctx.drawImage(spritesheet, 32, 0, 16, 16, col * blockSize, row * blockSize, blockSize, blockSize);
                }
            }
        }
    }*/

    function fillIsoMap(ctx, map, blockSize) {
        var col = 0,
            row = 0,
            columns = 0,
            rows = 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) {
                    // Draw wall
                    ctx.drawImage(spritesheet, 32, 16, 16, 16, (col * blockSize / 2) - (row * blockSize / 2) - cam.x, (row * blockSize / 4) + (col * blockSize / 4) - cam.y, blockSize, blockSize);
                } else if (map[row][col] === 2) {
                    // Draw lava
                    ctx.drawImage(spritesheet, 32, 32 + (~~(elapsed * 10) % 2) * 16, 16, 16, (col * blockSize / 2) - (row * blockSize / 2) - cam.x, (row * blockSize / 4) + (col * blockSize / 4) - cam.y, blockSize, blockSize);
                } else {
                    // Draw soil
                    ctx.drawImage(spritesheet, 32, 0, 16, 16, (col * blockSize / 2) - (row * blockSize / 2) - cam.x, (row * blockSize / 4) + (col * blockSize / 4) - cam.y, blockSize, blockSize);
                }
            }
        }
    }

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

        // Draw map
        fillIsoMap(ctx, maps[currentMap], 16);

        // Draw player
        player.drawIsoImageArea(ctx, cam, spritesheet, 8, (~~(elapsed * 10) % 2) * 16, player.dir * 16, 16, 16);

        // Draw enemies
        for (i = 0, l = enemies.length; i < l; i += 1) {
            enemies[i].drawIsoImageArea(ctx, cam, spritesheet, 8, 48 + (~~(elapsed * 10) % 2) * 16, enemies[i].dir * 16, 16, 16);
        }

        // 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', 120, 80);
            } else {
                ctx.fillText('PAUSE', 120, 80);
            }
            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 -= 8;
                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 += 8;
                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 += 8;
                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 -= 8;
                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], 16);
                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], 16);
                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);
            cam.isoFocus(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 = 240;
        canvas.height = 160;
        worldWidth = canvas.width;
        worldHeight = canvas.height;
        
        // Load assets
        spritesheet.src = 'assets/iso-sprites.png';
        
        // Create camera and player
        cam = new Camera();
        player = new Rectangle2D(64, 64, 16, 16, true);

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

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

15 comentarios:

  1. Excelente tuto ahora solo seria hacerlo en una mayor escala y con graficos vectoriales seria muy cool, tal como los famosos juegos isometricos de Facebook. Saludos:)

    ResponderEliminar
    Respuestas
    1. La base está lista, y si bien la mayoría de los navegadores soportan SVG como gráficos vectoriales, quizá sea mejor idea rasterizarlos en un tamaño grande para asegurar su compatibilidad. ¡Mucha suerte! Y gracias.

      Eliminar
  2. te quedo genial, porcierto ¿no has pensado en hacer un RPG por turnos?

    ResponderEliminar
    Respuestas
    1. ¿En hacerlo para un curso? Es algo tardado de crear el material, pero no es demasiado difícil de hacer. En realidad solo es detectar un evento al azar cada movimiento, y cambiar de escenario a este modo. La lógica de la batalla es bastante simple. Si quieres implementarlo y tienes dudas, con gusto podría guiarte en ello.

      Eliminar
    2. esque cuando vi tu juego en hisometrico, me hiso recordar, a Digimon DS, el cual es RPG por turnos, como tambien a Final Fantasy tactics, ese es el porqué, de preguntarte si podias hacer un curso de RPG clasico.

      Eliminar
    3. Creo que más que un RPG por turnos clásico, tenías más en mente un juego de tipo Tactics, que es un subgénero del mismo. Implementarlo es igual para un estilo top-view o isométrico, con la diferencia de dibujado que aquí aprendimos.

      Y como mencioné, implementarlo no es demasiado complejo, pero si un tanto largo de hacer y explicar, por lo que dudo mucho que aparezca entre los próximos tutoriales. Pero si deseas probar suerte en implementarlo, con gusto buscaré ayudarte a que lo consigas.

      Eliminar
    4. gracias, pero aun soy un mero novato en esto de javascript, asique cuando, tenga bien definidas las bases, te pedire ayuda, muchas gracias por estarme respondiando(apesar de aparecer en anonimo XD).

      Eliminar
    5. ¡Mucha suerte entonces! Y lo importante no es anonimato o no, lo importante es ayudar a quienes solicitan ayuda siempre :D... Aunque seria más lindo saber de vez en vez con quien entre todos se está hablando ;)

      Eliminar
  3. Hola querido karl me podrias indicar en el codigo como haces para trabajar tu sistema de depth sorting... ¿es la variable z? Tu codigo me confunde un poco pues ya se ha hecho un poco largo aunque todo esta muy claro,, en definitiva la pregunta es, ¿especificamente como haces para renderizar primero el norte-superior del mapa y luego ir bajando al sur?, yo se que lo explicas en esta entrada pero ya hay muchos sistemas juntos.... estare esperando ansioso tu respuesta...

    ResponderEliminar
    Respuestas
    1. En este ejemplo aún no se muestra nada relativo a como se ordena el dibujar de las imágenes. Sin embargo, todo lo que debes saber, es que las imágenes se dibujan en el mismo orden que indicas dentro de la función paint. Si deseas dibujar una imagen en orden diferente, solo debes cambiar de posición la línea donde dibujas dicha imagen.

      Eliminar
  4. Crítica constructiva:
    El proceso/algoritmo de "renderizado" de sprites/tiles de forma isométrica es un poco más complicado que lo explicado.
    Lo explicado sirve como base de trabajo y para escenarios en donde "ningún sprite" está encima/debajo de otro (los sprites solo están delante o detrás pero al mismo

    nivel/altura). Para este caso el algoritmo es claro: seguir el orden de "pintado" +(Z-X).
    ¿Pero que pasa cuando tengo elementos flotantes a un nivel/altura Y?

    a-) Si el/los sprite/s van exactamente encima de otros sprites sin solapar/intersectar con ningún adyacente de el/ellos (en el eje Y), se puede

    utilizar el orden de pintado +(Z-X)-Y.

    b-) Si el sprite "solapa/intersecta" a más de 1 sprite al mismo tiempo (en el eje Y) TENEMOS UN PROBLEMA: En qué orden los pintamos?. Esto ya necesita de un

    algoritmo de ordenación complejo para determinar el orden. Y no digamos cuando tengamos 30 sprites flotantes que hay que "integrar".

    Yo, después de un largo análisis y un mucho sufrimiento conseguí implementar un algoritmo. Nunca habría imajinado la complejidad (sinceramente pensé que era más

    sencillo) de este "asunto" y que fui descubiriendo a medida que empecé el desarrollo de mi primer juego 2.5D en cocos2D-x.
    Adelanto que un algoritmo de este tipo necesita, en primer instancia, de una "comparación" de "todos con todos" pero es posible hacer una serie de

    optimizaciones y reducir el proceso de cálculo muchísimo.

    Un ejemplo (demo) del resultado del algoritmo puede verse aquí:
    https://www.youtube.com/watch?v=tU1uJYdxUY4
    https://www.youtube.com/watch?v=tBwxX44c6eE

    ResponderEliminar
  5. para cuando IA?
    buen post

    ResponderEliminar
    Respuestas
    1. ¿Qué clase de Inteligencia Artificial estás buscando? Existen de todo tipo y complejidades para cada de juego...

      Eliminar
  6. Para qué sirve 'ElapsedTime' y 'deltatime'?

    ResponderEliminar
    Respuestas
    1. DeltaTime indica el tiempo que ha pasado desde el último ciclo hasta el actual. Elapsed es el tiempo acumulado desde el inicio del juego, y lo usamos para las animaciones.

      Eliminar