`

Como crear tu videojuego desde 0 en la web con HTML5 y JavaScript - Capítulo 4

Buenas buenas!

Como siempre, vamos a empezar resolviendo los 2 challenge que nos dejó el capítulo 3.

El primer challenge nos pedía agregar la animación de antorcha en cualquier parte del terreno, como primer paso, tenemos que agregarla en las urls.

this.urls = {
    ...
    torch: "./images/torch.png",
};

Y seguido pasamos a agregarla en el city.json con el nombre que declaramos.

{
	...
	"animation": "torch"
}

Y el challenge 2, ya un poco más complicado, nos pedía agregar una animación en el personaje, para esto lo que hice fue agregar al objeto del usuario 2 parámetros más.

this.user = {	
    pos: {	
        x: 3,	
        y: 5	
    },	
    fx: "healing",	
    frameFxCounter: 0	
};

fx: Es el nombre de la animación que va a tener el personaje.

frameFxCounter: Nos va a servir como contador de frames, como hacemos con las animaciones del mapa.

Agregamos el sprite a las urls

this.urls = {
    ...
    healing: "./images/healing.png"	
};

Como la animación va a estar en el personaje, vamos a estar trabajando en el método renderCharacter.

renderCharacter() {
    this.ctx.foreground.drawImage(
        this.images.character,
        this.user.pos.x * this.sizeTile,
        this.user.pos.y * this.sizeTile - 32
    );

    if (this.user.fx) {
        const animation = this.animations[this.user.fx];

        if (typeof this.user.frameFxCounter === "undefined") {
            this.user.frameFxCounter = 0;
        }

        this.user.frameFxCounter += this.delta / animation.speed;

        let frameFxCounter = Math.floor(this.user.frameFxCounter);

        if (frameFxCounter >= animation.frames.length) {
            this.user.frameFxCounter = 0;
            frameFxCounter = 0;
        }

        const frame = animation.frames[frameFxCounter];

        this.ctx.foreground.drawImage(
            this.images[this.user.fx],
            frame.sX,
            frame.sY,
            frame.width,
            frame.height,
            this.user.pos.x * this.sizeTile + animation.offset.x,
            this.user.pos.y * this.sizeTile + animation.offset.y,
            frame.width,
            frame.height
        );
    }
}

Si se fijan es la misma lógica que explicamos en el Capítulo 3 poniendo las animaciones en el mapa, pero en ve de usar la posición del mapa usamos la posición del usuario para renderizar la animación.

this.ctx.foreground.drawImage(
    this.images[this.user.fx],
    frame.sX,
    frame.sY,
    frame.width,
    frame.height,
    this.user.pos.x * this.sizeTile + animation.offset.x,
    this.user.pos.y * this.sizeTile + animation.offset.y,
    frame.width,
    frame.height
);

Si se fijan, agregué 2 parametros más que es offset.x e offset.y

"healing": {
    "speed": 50,
    "offset": {
        "x": -60,
        "y": -110
    },
    "frames": [
        ...
    ]
}

Este offset lo vamos a usar para centrar la animación en el personaje, piensen que cada animación tiene una altura, ancho y tamaños diferentes, así que si queremos centrar perfectamente donde va a aparecer la animación lo vamos a ir haciendo con el parámetro offset que está en la configuración de cada sprite.

Challenges del capítulo 3 finalizados!

Antes de empezar y como ya saben, todo contenido va a ser 100% gratuito, con lo único que me ayudarían es dándome un follow en Twitter: @DamianCatanzaro  y compartiendo el contenido para que más gente lo pueda usar 😁.

Y empecemos con el capítulo 4!

En este capítulo vamos a estar trabajando en el traspaso de mapas y en las animaciones del personaje al caminar.

Lo primero que vamos a hacer antes de empezar con los temas fuertes es usar un método de las Promises para cargar todos los recursos juntos, antes en el método initialize cargamos los recursos con await, lo que provoca esto es que necesitamos esperar a que carguen las imagenes para cargar los mapas, para después cargar las animaciones y así esperando a la carga anterior, pero tranquilamente podemos cargar imágenes, mapas y animaciones en paralelo ya que no se necesitan entre si para funcionar.

async initialize() {
    const loadImages = this.loadImages();
    const loadMaps = this.loadMap();
    const loadAnimations = this.loadAnimations();

    await Promise.all([loadImages, loadMaps, loadAnimations]);
    await this.renderMap();

    ...
}

con await Promise.all esperamos que carguen todos los recursos que vamos a declarar dentro del array, los mismos se van a descargar en paralelo y va a provocar que el juego esté funcional en menor tiempo.

Traspasos de mapas!

Como en todo juego, no nos vamos a quedar con un único mapa, nuestro mundo tiene que tener múltiples mapas, poder ir cambiando el entorno del mismo, pueden ser ciudades, bosques, montañas, etc.

Como verán, se ven puntos rojos y en donde están los traspasos tiles de "exit", esto lo vamos a poner para que nos sean más fácil identificar los bloqueos y los tiles de traspaso de mapa al estar desarrollando.

En el constructor del Engine vamos a tener una variable de debug, que en cuanto esté activa nos va a mostrar estos puntos así es mucho más fácil entender el entorno que estamos creando.

(Como aclaración los "..." es para no escribir todo el código anterior que tenemos en el constructor, sino lo que estamos agregando nuevo).

constructor(context) {
    ...
    this.debug = true;
}

Agregamos los 2 tiles nuevos al proyecto.

constructor(context) {
    ...

    this.urls = {
        ...
        exit: "./images/exit.png",
        blocked: "./images/blocked.png"
    };
}

Y en el renderMap vamos a agregar después de dibujar el tile que dibuje tanto el punto de bloqueo y el tile de exit (este último tile vamos a verlo más adelante, es lo que vamos a usar para que pase de un mapa a otro).

if (this.debug) {
    if (tile.blocked) {
        this.ctx.background.drawImage(
            this.images.blocked,
            x * this.sizeTile,
            y * this.sizeTile
        );
    }

    if (tile.tileExit) {
        this.ctx.background.drawImage(
            this.images.exit,
            x * this.sizeTile,
            y * this.sizeTile
        );
    }
}

Una vez que lo agreguemos vamos a ver los tiles bloqueados de la siguiente manera, si cambiamos el this.debug a false ya no los vamos a ver más y tener el juego listo para producción.

Ahora si, vamos a pasar a crear un nuevo mapa que es al que vamos a trasladarnos, voy a crear un mapa igual al que tenemos pero sin arboles ni carteles, solo con el terreno y agua, lo voy a llamar forest.json y voy a renombrar nuestro city.json a home.json

[
    [
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        }
    ],
    [
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        }
    ],
    [
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        }
    ],
    [
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        }
    ],
    [
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        }
    ],
    [
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        }
    ],
    [
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        }
    ],
    [
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        }
    ],
    [
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        }
    ],
    [
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        },
        {
            "background": "water",
            "blocked": true
        }
    ]
]

Como ahora tenemos 2 mapas, vamos a tener que especificarle al usuario en que mapa se encuentra y dibujar el mismo segun ese parametro, así que a nuestro objeto de usuario le vamos a agregar la propiedad map.

this.user = {
    pos: {
        x: 3,
        y: 5
    },
    //fx: "healing",
    frameFxCounter: 0,
    map: "home"
};

El nombre va a ser el nombre del json que creemos en nuestra carpeta de mapas, actualmente tenemos home.json y forest.json que es el ultimo que creamos.

Como ahora tenemos múltiples mapas, ya nuestro loadMap que cargaba solo el city.json no nos sirve, vamos a tener que adaptarlo para que cargue más de un mapa y lo guarde en memoria, así que primero vamos a crear 2 variables, un array que contenga los mapas a descargar y cambiar la variable de map a un objeto, porque vamos a ir cargando ahí dentro los mapas.

constructor(context) {
    ...

    this.mapsToLoad = ["home", "forest"];
    this.maps = {};
}

Así que acto seguido vamos a modificar el metodo loadMap para que tome los mapas que le pasemos por parametros y vamos a crear otro metodo que sea loadMaps en donde va a recorrer this.mapsToLoad y pasarselos a loadMap para descargarlos.

async loadMap(map) {
    const response = await fetch(`/maps/${map}.json`);
    const result = await response.json();

    return result;
}

async loadMaps() {
    this.mapsToLoad.map(async map => {
        this.maps[map] = await this.loadMap(map);
    });
}

Y en el initialize vamos a cambiar el this.loadMap por this.loadMaps que es el que los va a cargar todos.

async initialize() {
    ...
    const loadMaps = this.loadMaps();
}

Con esto modificado lo que va a ser nuestro engine es ir descargando los mapas e ir guardandolos en this.maps de la siguiente manera:

this.maps['home'] = [...];
this.maps['forest'] = [...];

Ya tenemos el mapa actual en el que está el usuario en this.user.map y los mapas guardados en this.maps, así que vamos a tener que modificar el renderMap para que dibuje el actual en el que está parado el usuario.

async renderMap() {
        this.clearCanvasBackground();

        const userMap = this.user.map;

        for (let y = 0; y <= this.mapSize.y - 1; y++) {
            for (let x = 0; x <= this.mapSize.x - 1; x++) {
                const tile = this.maps[userMap][y][x];

                ...
            }
        }
    }

Agarramos el mapa del usuario y lo guardamos en userMap.

const userMap = this.user.map;

Y obtenemos el tile del mapa pasando el actual del usuario.

const tile = this.maps[userMap][y][x];

Y como verán, arriba de todo tenemos un nuevo método que es clearCanvasBackground, es el mismo método de clearCanvas pero en ve de limpiar el canvas de foreground vamos a limpiar el de background solo cuando queramos actualizarlo, que va a ser al momento de cambiar de mapa.

clearCanvasBackground() {
    this.ctx.background.clearRect(
        0,
        0,
        this.mapSize.x * this.sizeTile,
        this.mapSize.y * this.sizeTile
    );
}

Ya tenemos gran parte de la lógica lista y ahora nos falta agregar los traspasos en nuestra configuración del mapa y va a tener la siguiente estructura:

"tileExit": {
    "x": 1,
    "y": 0,
    "map": "forest"
}

tileExit lo vamos a agregar donde queramos el traslado en el mapa y dentro vamos a poner a donde nos vamos a trasladar.

[
    [
        {
            "background": "grass",
            "animation": "torch"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass"
        },
        {
            "background": "grass",
            "tileExit": {
                "x": 1,
                "y": 0,
                "map": "forest"
            }
        }
    ],
    ...
]

Si vemos el ejemplo que dejé arriba, lo que hice es poner en el home.json todos los traslados del lado derecho para pasar al mapa de forest.json, y en forest.json todos los traslados del lado izquierdo para pasar al home.json

Si se fijan, los traslados los ponemos para que no caigamos exactamente en el mismo tile del traslado sino uno más, esto es para que no tengamos que volver para atrás y adelante y podamos pasar todo el tiempo de mapa.

Lo ultimo que nos falta es la logica, esta logica como es parte del movimiento del usuario la vamos a hacer en el metodo initializeKeys.

initializeKeys() {
    document.addEventListener("keydown", e => {
        ...

        this.checkTileExit();
    });
}

Vamos a agregar un método nuevo que después de cada paso del usuario va a checkear si está parado sobre un tileExit y si es así moverlo hacia su destino.

checkTileExit() {
    const { map, pos } = this.user;

    const tile = this.maps[map][pos.y][pos.x];

    if (tile.tileExit) {
        this.user.map = tile.tileExit.map;
        this.user.pos.x = tile.tileExit.x;
        this.user.pos.y = tile.tileExit.y;

        this.renderMap();
    }
}

Como dijimos cada vez que el usuario se mueve comprobamos si hay algun tileExit en nuestro mapa, de ser así asignamos el mapa de destino y sus coordenadas al usuario y pedimos renderizar el mapa nuevamente con renderMap.

Que aprendimos en este capitulo? Un poco de optimización de la carga de recursos, para que sea en paralela y que cargue más rapido nuestro juego y poder agregarle multiples mapas y traslados la mismo.

Los challenges de este capitulo van a ser los siguientes:

Primer challenge: Crear un mapa extra, con el contenido que ustedes quieran y agregarlo a la derecha de forest, que quede de la siguiente manera: Home -> Forest -> Mapa nuevo. Con sus respectivos traslados.

Segundo challenge: Vamos a tener que transportar al usuario desde Home al mapa nuevo usando un portal como animación en el mapa.

Sprite:

Configuración de sprite:

"portal": {
    "speed": 90,
        "frames": [
            {
                "sX": 2,
                "sY": 0,
                "width": 41,
                "height": 64
            },
            {
                "sX": 44,
                "sY": 0,
                "width": 41,
                "height": 64
            },
            {
                "sX": 87,
                "sY": 0,
                "width": 41,
                "height": 64
            },
            {
                "sX": 129,
                "sY": 0,
                "width": 41,
                "height": 64
            },
            {
                "sX": 171,
                "sY": 0,
                "width": 41,
                "height": 64
            },
            {
                "sX": 214,
                "sY": 0,
                "width": 41,
                "height": 64
            },
            {
                "sX": 257,
                "sY": 0,
                "width": 41,
                "height": 64
            },
            {
                "sX": 299,
                "sY": 0,
                "width": 41,
                "height": 64
            }
        ]
}

Y eso es todo por este capítulo!

Muchas gracias por el aguante y apoyo que le están dando al blog! Si pueden resolver los challenges no duden en compartirlos, los voy a estar leyendo en mi Twitter @DamianCatanzaro

Tambien se pueden suscribir al blog o seguirme en Twitter para recibir todas las notificaciones de cuando suba nuevos capítulos!

Nos vemos en el siguiente capítulo!

Follow @DamianCatanzaro