Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.
Carga de estructura y fondo
Definimos la estructura global del juego y cargamos la imagen de fondo.
Cargar prota con el JSON de Tiled
class Escena extends Phaser.Scene {
preload() {
...
this.load.spritesheet('player', '../img/player.png', {frameWidth: 180, frameHeight: 180});
this.load.tilemapTiledJSON('level1', '../img/map.json');
}
create() {
...
const map = this.add.tilemap('level1');
const playersFromTiled = this.findObjectsByType('player', map);
this.player = this.physics.add.sprite(playersFromTiled[0].x, playersFromTiled[0].y, 'player');
}
findObjectsByType(type, tilemap) {
const result = [];
tilemap.objects.forEach(function (element) {
if (element.name === 'objectsLayer') {
element.objects.forEach(function (element2) {
if (element2.type === type) {
element2.y -= tilemap.tileHeight;
result.push(element2);
}
});
}
});
return result;
}
}
Instanciar al protagonista utilizando clases en lugar de llamar al método add.sprite
this.player = new Player(this, playersFromTiled[0].x, playersFromTiled[0].y);
class Player extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y) {
super(scene, x, y, 'player');
scene.physics.systems.displayList.add(this);
scene.physics.systems.updateList.add(this);
scene.physics.world.enableBody(this, 0);
}
}
Cargar hierbaLayer
preload() {
...
this.load.image('gameTiles', '../img/tiles.png');
}
create(){
...
const tileset = map.addTilesetImage('tiles', 'gameTiles');
map.createStaticLayer('hierbaLayer', tileset).setDepth(100);
}
Cargar la backgroundLayer
map.createStaticLayer('backgroundLayer', tileset);
Cargar la collisionLayer
create(){
...
this.collisionLayer = map.createStaticLayer('collisionLayer', tileset);
// Hacemos que los tiles que pertenecen a la collisionLayer sean colisionables
this.collisionLayer.setCollisionByExclusion([-1]);
// Establecemos la colisión entre el jugador y la collisionLayer
this.physics.add.collider(this.player, this.collisionLayer);
}
Ajustando la boundingbox del player
this.setSize(90, 180, true);
El jugador se mueve
class Escena extends Phaser.Scene {
create(){
...
this.cursors = this.input.keyboard.createCursorKeys();
}
update() {
if (this.cursors.left.isDown) {
this.player.caminarALaIzquierda()
} else if (this.cursors.right.isDown) {
this.player.caminarALaDerecha();
} else {
this.player.reposo();
}
}
}
class Player extends Phaser.Physics.Arcade.Sprite {
...
caminarALaIzquierda(){
this.body.setVelocityX(-250);
}
caminarALaDerecha(){
this.body.setVelocityX(250);
}
reposo(){
this.body.setVelocityX(0);
}
}
Animación del jugador
Definimos la función que cargará las animaciones y que será llamada dentro del método create de la Escena:
class Escena extends Phaser.Scene {
create() {
...
this.animacionesDeLaEscena();
}
animacionesDeLaEscena() {
this.anims.create({
key: 'walk',
frames: this.anims.generateFrameNumbers('player', {start: 2, end: 5}),
frameRate: 10,
repeat: -1,
});
this.anims.create({
key: 'reposo',
frames: this.anims.generateFrameNumbers('player', {start: 0, end: 1}),
frameRate: 4,
repeat: -1,
});
}
}
caminarALaIzquierda(){
this.body.setVelocityX(-250);
this.flipX = true;
this.play('walk', true);
}
caminarALaDerecha(){
this.body.setVelocityX(250);
this.flipX = false;
this.play('walk', true);
}
reposo() {
this.body.setVelocityX(0);
this.play('reposo', true);
}
Saltar
Añadimos el método saltar a la clase Player. El jugador sólo debe poder saltar si está tocando el suelo:
class Player extends Phaser.Physics.Arcade.Sprite {
...
saltar(){
if(this.enElSuelo){
this.body.setVelocityY(-250);
}
}
update(){
this.enElSuelo = this.body.onFloor();
}
}
class Escena extends Phaser.Scene {
...
update() {
...
if (this.cursors.up.isDown) {
this.player.saltar();
}
this.player.update();
}
}
Caer
this.anims.create({
key: 'caer',
frames: this.anims.generateFrameNumbers('player', {start: 6, end: 8}),
frameRate: 7,
repeat: -1,
});
class Player extends Phaser.Physics.Arcade.Sprite {
...
saltar(){
if(this.enElSuelo){
this.body.setVelocityY(-250);
this.play('caer', true);
}
}
caminarALaIzquierda(){
...
if(this.enElSuelo)this.play('walk', true);
}
caminarALaDerecha(){
...
if(this.enElSuelo)this.play('walk', true);
}
reposo() {
...
if (this.enElSuelo)this.play('reposo', true);
}
}
Controles visuales
Habrá que llamar al siguiente método desde el create de la escena.
controlesVisuales() {
this.player.setData('direccionHorizontal', 0);
this.player.setData('estaSaltando', false);
const leftbtn = this.add.sprite(50, 560, 'flecha').setInteractive()
const rightbtn = this.add.sprite(140, 560, 'flecha').setInteractive();
rightbtn.flipX = true;
const upbtn = this.add.sprite(850, 560, 'flecha').setInteractive();
upbtn.rotation = Math.PI/2;
leftbtn.on('pointerdown', function() {
this.scene.player.setData('direccionHorizontal', Phaser.LEFT);
});
rightbtn.on('pointerdown', function() {
this.scene.player.setData('direccionHorizontal', Phaser.RIGHT);
});
upbtn.on('pointerdown', function() {
this.scene.player.setData('estaSaltando', Phaser.UP);
});
leftbtn.on('pointerup', function() {
this.scene.player.setData('direccionHorizontal', Phaser.NONE);
});
rightbtn.on('pointerup', function() {
this.scene.player.setData('direccionHorizontal', Phaser.NONE);
});
upbtn.on('pointerup', function() {
this.scene.player.setData('estaSaltando', Phaser.NONE);
});
}
update() {
if (this.cursors.left.isDown || this.player.getData('direccionHorizontal') === Phaser.LEFT) {
this.player.caminarALaIzquierda();
} else if (this.cursors.right.isDown || this.player.getData('direccionHorizontal') === Phaser.RIGHT) {
this.player.caminarALaDerecha();
} else {
this.player.reposo();
}
if (this.cursors.up.isDown || this.player.getData('estaSaltando') === Phaser.UP) {
this.player.saltar();
}
this.player.update();
}
La cámara sigue al prota
class Escena extends Phaser.Scene {
create() {
...
this.cameras.main.setSize(960, 640);
this.cameras.main.startFollow(this.player);
}
La cámara sigue al prota horizontalmente
Eliminamos la línea:
this.cameras.main.startFollow(this.player);
class Escena extends Phaser.Scene {
update(){
...
this.cameras.main.scrollX = this.player.x - 400;
this.cameras.main.scrollY = 0;
}
Los Controles visuales no se desplazan
rightbtn.setScrollFactor(0);
El fondo se repite
Sustituímos la línea que carga el fondothis.bg = this.add.tileSprite(480, 320, 960, 640, 'fondo').setScrollFactor(0);
En el método update, desplazamos el fondo acorde con la posición del jugador
class Escena extends Phaser.Scene {
update() {
this.bg.tilePositionX = this.player.x;
Método para la inserción de enemigos
Debemos sustituir el código que teníamos para la insercción de los enemigos por este:
class Escena extends Phaser.Scene {
create() {
...
const hormigasFromTiled = this.findObjectsByType('hormigaEnemy', map);
this.insertarMalos(hormigasFromTiled, HormigaEnemy, this);
}
insertarMalos(arrayDeMalos, type, scene) {
const enemies = scene.physics.add.group({classType: type, runChildUpdate: true, runChildCreate: true});
for (let i = 0; i < arrayDeMalos.length; i++) {
const malo = new type(arrayDeMalos[i].x, arrayDeMalos[i].y, scene);
enemies.add(malo);
}
return enemies;
}
class HormigaEnemy extends Phaser.Physics.Arcade.Sprite {
constructor(x, y, scene) {
super(scene, x, y, 'hormiga');
scene.physics.add.collider(this, scene.collisionLayer);
scene.add.existing(this);
}
}
Enemigo con animación
Las animaciones deben ser cargadas desde el create de la escena.
this.anims.create({
key: 'hormigaWalk',
frames: this.anims.generateFrameNumbers('hormiga', {start: 0, end: 3}),
frameRate: 7,
repeat: -1,
});
En el constructor del enemigothis.play('hormigaWalk');
Enemigo se mueve
constructor(x, y, scene) {
...
this.velocidad = 100;
this.direccion = -1;
}
update() {
this.body.setVelocityX(this.direccion * this.velocidad);
}
Enemigo inteligente
class HormigaEnemy extends Phaser.Physics.Arcade.Sprite {
update(){
...
this.body.setVelocityX(this.direccion * this.velocidad);
const nextX = Math.floor(this.x / 32) + this.direccion;
let nextY = this.y + this.height / 2;
nextY = Math.round(nextY / 32);
const nextTile = this.scene.collisionLayer.hasTileAt(nextX, nextY);
if (!nextTile && this.body.blocked.down) {
this.direccion *= -1;
}
if (this.direccion > 0) {
this.flipX = true;
} else {
this.flipX = false;
}
}
}
Enemigo muere
class Escena extends Phaser.Scene {
...
create() {
...
this.physics.add.overlap(this.player, hormigas, this.player.checkEnemy, null, this.player);
}
class Player extends Phaser.Physics.Arcade.Sprite {
checkEnemy(player, enemigo) {
// El jugador está cayendo?
if (this.body.velocity.y > 0) {
enemigo.morir();
} else {
this.morir();
}
}
morir() {
this.disableBody();
}
class HormigaEnemy extends Phaser.Physics.Arcade.Sprite {
morir() {
this.disableBody();
}
Enemigo explota
this.anims.create({
key: 'explosionAnim',
frames: this.anims.generateFrameNumbers('explosion', {start: 0, end: 4}),
frameRate: 7
});
class HormigaEnemy extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, sprite) {
...
this.on('animationcomplete', this.animationComplete, this);
}
morir() {
...
this.play('explosionAnim');
}
animationComplete(animation, frame, sprite) {
if (animation.key === 'explosionAnim') {
this.disableBody(true, true);
}
}
Prota explota
Utilizaremos la variable estaVivo para detener todas las demás animaciones cuando el Player es colisionado.
class Player extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y) {
...
this.estaVivo = true;
this.on('animationcomplete', this.animationComplete, this);
}
morir() {
this.estaVivo = false;
this.disableBody();
this.play('explosionAnim', true);
}
animationComplete(animation, frame, sprite) {
if (animation.key === 'explosionAnim') {
this.disableBody(true, true);
}
}
}
update() {
...
if (this.player.estaVivo) {
if (this.cursors.left.isDown || this.player.getData('direccionHorizontal') === Phaser.LEFT) {
this.player.caminarALaIzquierda();
} else if (this.cursors.right.isDown || this.player.getData('direccionHorizontal') === Phaser.RIGHT) {
this.player.caminarALaDerecha();
} else {
this.player.reposo();
}
if (this.cursors.up.isDown || this.player.getData('estaSaltando') === Phaser.UP) {
this.player.saltar();
}
this.player.update();
}
}
Enemigo con Herencia
Renombramos la clase HormigaEnemy como Enemy y retocamos ligeramente su constructor para que la clase HormigaEnemy herede de ella.
Aunque cambiemos la estructura del código interno, en este punto el juego debe seguir haciendo lo mismo.
class Enemy extends Phaser.Physics.Arcade.Sprite{
constructor(scene, x, y, sprite ) {
super(scene, x, y, sprite);
...
}
...
}
class HormigaEnemy extends Enemy {
constructor(x, y, scene) {
super(scene, x, y, 'hormiga');
this.play('hormigaWalk');
}
}
Insertar Oruga
class Escena extends Phaser.Scene {
preload() {
...
this.load.spritesheet('oruga', '../img/oruga.png', {
frameWidth: 96,
frameHeight: 192
});
}
create(){
...
const orugasFromTiled = this.findObjectsByType('orugaEnemy', map);
const orugas = this.insertarMalos(orugasFromTiled, OrugaEnemy, this);
this.physics.add.overlap(this.player, orugas, this.player.checkEnemy, null, this.player);
}
class OrugaEnemy extends Enemy {
constructor(x, y, scene) {
super(scene, x, y, 'oruga');
this.play('orugaWalk');
}
}
this.anims.create({
key: 'orugaWalk',
frames: this.anims.generateFrameNumbers('oruga', {start: 0, end: 3}),
frameRate: 7,
repeat: -1,
});
Insertar Avispa dando vueltas
Las animaciones deben cargarse en la escena, no en la clase a la que pertenecen.
this.load.spritesheet('avispa', '../img/avispa.png', {
frameWidth: 128,
frameHeight: 128
});
this.anims.create({
key: 'avispaFly',
frames: this.anims.generateFrameNumbers('avispa', {start: 0, end: 2}),
frameRate: 10,
repeat: -1,
});
const avispasFromTiled = this.findObjectsByType('avispaEnemy', map);
this.insertarMalos(avispasFromTiled, AvispaEnemy, this);
class AvispaEnemy extends Phaser.Physics.Arcade.Sprite {
constructor(x, y, scene) {
super(scene, x, y, 'avispa');
scene.add.existing(this);
this.play('avispaFly', true);
this.flyPath = new Phaser.Curves.Ellipse(x, y, 100, 100);
/*this.pathIndex es el grado de completitud de dicha trayetoria. 0 será el punto inicial de la trayectoria circular y 1 el punto final.*/
this.pathIndex = 0;
this.pathSpeed = 0.005;
this.pathVector = new Phaser.Math.Vector2();
/* • La función getPoint recibe dos parámetros:
◦ El primero es el grado de completitud de la trayectoria (el path).
◦ El segundo es la variable (this.pathVector) en la que vamos a almacenar las coordenadas correspondientes a ese grado de completitud de la trayectoria.
*/
this.flyPath.getPoint(0, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
/*this.path es variable que almacenará las diferentes trayectorias de la avispa (inicialmente dando vueltas, luego en línea recta hacia el player para atacarle y en línea recta hasta su posición original)*/
this.path = this.flyPath;
}
update() {
/*Incrementamos pathIndex, que es el coeficiente que indica el grado de completitud de la trayectoria.*/
this.pathIndex = Phaser.Math.Wrap(this.pathIndex + this.pathSpeed, 0, 1);
/*Alimentamos la variable pathVector, que estará en función del grado de completitud de la trayectoria.*/
this.flyPath.getPoint(this.pathIndex, this.pathVector);
/*Modificamos la posición de la avispa en función de las coordenadas x e y del vector.*/
this.setPosition(this.pathVector.x, this.pathVector.y);
}
}
Añadir el estado VOLANDO a la Avispa
Añadiremos los estados después de la clase Avispa.
class AvispaEnemy extends Phaser.Physics.Arcade.Sprite {
...
}
AvispaEnemy.VOLANDO = 0;
La avispa sólo volará en círculos en el estado volando, así que cambiamos un poco el código del update de la Avispa. De momento, el método checkPlayer se encargará de ejecutar este vuelo circular de la avispa:
update() {
if (this.state === AvispaEnemy.VOLANDO) {
this.checkPlayer();
}
}
checkPlayer (){
this.pathIndex = Phaser.Math.Wrap(this.pathIndex + this.pathSpeed, 0, 1);
this.flyPath.getPoint(this.pathIndex, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
}
Avispa detecta a Player
class AvispaEnemy extends Phaser.Physics.Arcade.Sprite {
create(){
...
this.patrolCircle = new Phaser.Geom.Circle(0, 0, 256);
}
checkPlayer (){
...
// Actualizamos la posición del círculo que patrulla la avispa para ver si el player se mete dentro
this.patrolCircle.x = this.x;
this.patrolCircle.y = this.y;
// El jugador ha entrado dentro del area de patrulla de la avispa?
const player = this.scene.player;
if (this.patrolCircle.contains(player.x, player.y)) {
alert('perseguir')
}
}
Avispa persigue a Player
class AvispaEnemy extends Phaser.Physics.Arcade.Sprite {
constructor(x, y, scene) {
...
this.attackPath = new Phaser.Curves.Line([0, 0, 0, 0]);
}
update(time, delta) {
if (this.state === AvispaEnemy.VOLANDO) {
this.checkPlayer();
} else if (this.state === AvispaEnemy.PERSIGUIENDO) {
this.persiguePlayer(delta);
}
}
checkPlayer (){
...
if (this.patrolCircle.contains(player.x, player.y)) {
this.attackPath.p0.set(this.x, this.y);
this.attackPath.p1.set(player.x, player.y);
this.path = this.attackPath;
this.pathIndex = 0;
this.attackTime = 0;
this.state = AvispaEnemy.PERSIGUIENDO;
}
}
persiguePlayer(delta) {
this.attackTime += delta;
var player = this.scene.player;
this.attackPath.p1.set(player.x, player.y);
this.pathIndex += this.pathSpeed * 2;
this.path.getPoint(this.pathIndex, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
if (this.scene.physics.overlap(this, player) && this.state === AvispaEnemy.PERSIGUIENDO) {
alert('ataca');
}
}
}
AvispaEnemy.VOLANDO = 0;
AvispaEnemy.PERSIGUIENDO = 1;
Avispa ataca a Player
Las animaciones se deben definir dentro de la escena, no de la entidad a la que pertenecen.
this.anims.create({
key: 'avispaAttack',
frames: this.anims.generateFrameNumbers('avispa', { frames: [ 3, 4, 5, 4, 3 ] }),
frameRate: 10
});
En vez de un alert, cuando la avispa colisione contra el Player, detonaremos la animación de ataque.
this.on('animationcomplete', this.animationComplete, this);
persiguePlayer(delta) {
...
if (this.scene.physics.overlap(this, player) && this.state === AvispaEnemy.PERSIGUIENDO) {
this.play('avispaAttack', true);
}
}
animationComplete(animation, frame, sprite) {
if (animation.key === 'avispaAttack') {
alert("termina ataque");
}
}
Avispa vuelve a casa
class AvispaEnemy extends Phaser.Physics.Arcade.Sprite {
constructor(x, y, scene) {
...
this.startPlace = new Phaser.Math.Vector2(this.pathVector.x, this.pathVector.y);
}
update(time, delta) {
...
}else if (this.state === AvispaEnemy.VOLVIENDO) {
this.pathIndex += this.pathSpeed * 2;
this.path.getPoint(this.pathIndex, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
if (this.pathIndex >= 1){
this.continuaVolando();
}
}
}
animationComplete(animation) {
if (animation.key === 'avispaAttack') {
this.returnHome();
}
}
returnHome() {
this.play('avispaFly', true);
this.attackPath.p0.set(this.x, this.y);
this.attackPath.p1.set(this.startPlace.x, this.startPlace.y);
this.pathIndex = 0;
this.path.getPoint(this.pathIndex, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
this.state = AvispaEnemy.VOLVIENDO;
}
continuaVolando () {
this.state = AvispaEnemy.VOLANDO;
this.path = this.flyPath;
this.pathIndex = 0;
}
}
AvispaEnemy.VOLANDO = 0;
AvispaEnemy.PERSIGUIENDO = 1;
AvispaEnemy.VOLVIENDO = 2;
La avispa siempre mira al protagonista
if(this.x < this.scene.player.x){
this.flipX = true;
}else{
this.flipX = false;
}
El heroe recibe daño
Dentro de la función persiguePlayerif (this.scene.physics.overlap(this, player) && this.state === AvispaEnemy.PERSIGUIENDO){
...
this.scene.playerGolpeadoPorAvispa();
}
playerGolpeadoPorAvispa(){
alert('golpeado')
}
El prota sale despedido por los aires
playerGolpeadoPorAvispa(){
this.player.estaAturdido = true;
this.player.body.setVelocity(-150,-150);
this.player.play('caer', true);
}
update() {
if(!this.player.estaAturdido){
//aquí dentro vendría todo el código que ya teníamos de la función update
}
}
Reactivar los controles del jugador
playerGolpeadoPorAvispa() {
this.estaAturdido = true;
this.body.setVelocity(-150, -150);
this.time.addEvent({delay: 1000, callback: this.terminoElAturdimiento, callbackScope: this});
}
terminoElAturdimiento() {
this.player.estaAturdido = false;
}
Añadir meta
preload(){
...
this.load.image('meta', '../img/meta.png');
}
create(){
...
const metaFromTiled = this.findObjectsByType('meta', map)[0];
this.meta = this.physics.add.sprite(metaFromTiled.x, metaFromTiled.y, 'meta');
this.meta.body.immovable = true;
this.meta.body.moves = false;
}
Fin del juego
this.physics.add.overlap(this.player, this.meta, this.playerAlcanzaMeta, null, this);
playerAlcanzaMeta(){
this.scene.start('finScene');
}
Reducir el area de colisión de la serpiente para poder meternos dentro de su boca
this.meta.setSize(160, 160);
Limitando la cámara
this.cameras.main.setBounds(0, 0, 3520, 640);
Escena de perder tras explotar
animationComplete(animation, frame, sprite) {
if (animation.key === 'explosionAnim') {
this.disableBody(true, true);
this.scene.scene.start('perderScene');
}
}
Escena de perder tras caer por precicipio
this.physics.world.setBoundsCollision(false, false, false, true);
this.physics.world.on('worldbounds', () => {
this.scene.start('perderScene');
});
this.setCollideWorldBounds(true);
this.body.onWorldBounds = true;