Plataformas con Phaser

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 el mapa

class Platform extends Phaser.Scene {
  preload() {
    this.load.tilemapTiledJSON('jsonLevel', jsonLevel);
    this.load.image('tilesetPNG', tilesetPNG);
  }
  create(){
    const map = this.make.tilemap({ key: 'jsonLevel' });
    const tileset = map.addTilesetImage('nombreDelTilesetEnTiled', 'tilesetPNG');
    this.collisionLayer = map.createLayer('collisionLayer', tileset);

    // Esta línea hace un zoom para alejar, para que se vea el mapa al completo, una vez hayamos centrado el mapa en la posición correcta, podemos comentarla
    this.cameras.main.setZoom(0.5);
  }

Ubicar el punto de origen de la cámara en pantalla.

Para ello podremos usar la siguiente clase:

export class DebugMapCamera {
  scene;
  controls;
  debugCoords;

  constructor(scene) {
    this.scene = scene;
    this.debugCoords = scene.add.text(0, 0, '').setScrollFactor(0);
    const cursors = scene.input.keyboard.createCursorKeys();
    this.controls = new Phaser.Cameras.Controls.FixedKeyControl({
      camera: scene.cameras.main,
      left: cursors.left,
      right: cursors.right,
      up: cursors.up,
      down: cursors.down,
      speed: 0.5
    });
   }

   update(time, delta) {
     this.controls.update(delta);
     this.debugCoords.setText([
       `scrollX: ${this.scene.cameras.main.scrollX}`,
       `scrollY: ${this.scene.cameras.main.scrollY}`
     ]);
   }
}

export default DebugMapCamera;

Y luego, dentro de la escena donde queremos fijar la cámara, usaremos:

create() {
  ...
  this.debugCamera = new DebugMapCamera(this);
}    

update(time, delta) {
  this.debugCamera.update(time, delta);
}

Con esto podremos obtener las coordenadas en las que ubicaremos la cámara:

this.cameras.main.setScroll(440, -304);

Cargar Player con el JSON de Tiled

Primero definiremos la clase del Player en un fichero externo, para que todo quede más ordenado.

class Player extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    super(scene, x, y, 'player');
    scene.add.existing(this);
    scene.physics.add.existing(this);
    this.body.gravity.y = 500;
  }

  static loadAssets(scene) {
    scene.load.spritesheet('player', player, { frameWidth: 180, frameHeight: 180 });
  }
}

export default Player;

Hemos creado el método estático loadAssets para poder cargar los assets del player dentro de la propia clase del player. Sin embargo, para poder hacer esto, habrá que llamar ese método desde el preload de la escena:

preload(){
  ...
  Player.loadAssets(this);
}

Necesitaremos el siguiente método en la escena:

findObjectsByClassInObjectsLayer(classParam, tilemap) {
  const result = [];
  tilemap.objects.forEach(function (element) {
    if (element.name === 'objectsLayer') {
      element.objects.forEach(function (element2) {
        if (element2.type === classParam) {
          element2.y -= tilemap.tileHeight;
          result.push(element2);
        }
      });
    }
  });
  return result;
}

Finalmente, cargamos el player en la escena.

const playersFromTiled = this.findObjectsByClassInObjectsLayer('player', map);
this.player = new Player(this, playersFromTiled[0].x, playersFromTiled[0].y);

Hacer la capa collisionLayer colisionable con el player

// 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);

Cargar el resto de capas no colisionables (grassLayer y backgroundLayer)

create(){
  ...
  map.createLayer('grassLayer', tileset).setDepth(100);
  map.createLayer('backgroundLayer', tileset);
}

Ajustando la boundingbox del player

this.setSize(90, 180, true);

El jugador se mueve

class Player extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    ...
    this.cursors = this.scene.input.keyboard.createCursorKeys();
  }

  update() {       
    if (this.cursors.left.isDown) {
      this.leftWalk()
    } else if (this.cursors.right.isDown) {
       this.rightWalk();
    }
  }

  leftWalk() {
    this.body.setVelocityX(-250);
  }

  rightWalk() {
    this.body.setVelocityX(250);
  }
}
class Platform extends Phaser.Scene {
  ...
  update() {
    this.player.update();
  }
}

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 Player extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    ...
    this.createAnimations();
  }

  createAnimations() {
    this.anims.create({
      key: 'walk',
      frames: this.anims.generateFrameNumbers('player', {start: 2, end: 5}),
      frameRate: 10,
      repeat: -1,
    });
  }

  leftWalk(){
    this.body.setVelocityX(-250);
    this.flipX = true;
    this.play('walk', true);
  }

  rightWalk(){
    this.body.setVelocityX(250);
    this.flipX = false;
    this.play('walk', true);
  }
}

Reposo

class Player extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    ...
    this.play('idle', true);
  }

  createAnimations() {
    ...
    this.anims.create({
      key: 'idle',
      frames: this.anims.generateFrameNumbers('player', { start: 0, end: 1 }),
      frameRate: 4,
      repeat: -1,
      });
  }

  update(){
    ...
    }else {
      this.idle();
    }
  }

  idle() {
    this.body.setVelocityX(0);
    this.play('idle', 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 {
  constructor(scene, x, y) {
    this.onGround= false;
  }
	
  update(){
    this.onGround= this.body.onFloor();
    ...
    if (this.cursors.up.isDown) {
      this.jump();
    }
  }

  jump(){
    if(this.onGround){
      this.body.setVelocityY(-500);
    }
  }
}

Añadir nuevos controles para un segundo jugador (paso opcional)

this.cursors2 = scene.input.keyboard.addKeys({ up: 'W', left: 'A', down: 'S', right: 'D' });

Además, debemos tener en cuenta que los nombres de los recursos y de las animaciones no pueden estar repetidos.

static loadAssets(scene) {
  scene.load.spritesheet('run2', playerImgRun, { frameWidth: 52, frameHeight: 34 });
  scene.load.spritesheet('idle2', playerImgIdle, { frameWidth: 52, frameHeight: 34 });
}

createAnimations() {
  this.anims.create({
    key: 'walk2',
    frames: this.anims.generateFrameNumbers('run2', { start: 0, end: 13 }),
    frameRate: 30,
    repeat: -1,
  });
  this.anims.create({
    key: 'idle2',
    frames: this.anims.generateFrameNumbers('idle2', { start: 0, end: 17 }),
    frameRate: 30,
    repeat: -1,
  });
}

Animación de Caer

class Player extends Phaser.Physics.Arcade.Sprite {
  ...
  createAnimations() {
    ...
    this.anims.create({
      key: 'fall',
      frames: this.anims.generateFrameNumbers('player', {start: 6, end: 7}),
      frameRate: 7,
      repeat: -1,
    });
  }

  jump(){
    if(this.onGround){
      this.body.setVelocityY(-500);
      this.play('fall', true);
    }
  }

  leftWalk(){
    ...
    if(this.onGround)this.play('walk', true);
  }

  rightWalk(){
    ...
    if(this.onGround)this.play('walk', true);
  }

  idle() {
    ...
    if (this.onGround)this.play('idle', true);
  }
}

Controles visuales – poniendo los botones en pantalla

static loadAssets(scene) {
  ...
  scene.load.image('arrow', arrow);
}

visualControls(scene) {
  const leftbtn = scene.add.sprite(50, gameHeight - 30, 'arrow').setInteractive();
  leftbtn.setDepth(200);
  leftbtn.setScrollFactor(0, 0); // este método hará que cuando en los siguientes pasos hagamos que la cámara siga al jugador, el botón de desplazamiento no se desplace. Si no utilizásemos este método, cuando hagamos el pointerup, como este no se hace en la misma posición en la que hicimos el pointerdown, no funcionaría
  const rightbtn = scene.add.sprite(140, gameHeight - 30, 'arrow').setInteractive();
  rightbtn.flipX = true;
  rightbtn.setDepth(200);
  rightbtn.setScrollFactor(0, 0);
  const upbtn = scene.add.sprite(850, gameHeight - 30, 'arrow').setInteractive();
  upbtn.rotation = Math.PI/2;
  upbtn.setDepth(200);
  upbtn.setScrollFactor(0, 0);
}

Controles visuales – dando funcionalidad

Habrá que llamar al siguiente método desde el create del player.

visualControls(scene) {
  ...
  this.setData('horizontalDirection', 0);
  this.setData('isJumping', false);

  leftbtn.on('pointerdown', () => {
    this.setData('horizontalDirection', Phaser.LEFT);
  });

  rightbtn.on('pointerdown', () => {
    this.setData('horizontalDirection', Phaser.RIGHT);
  });

  upbtn.on('pointerdown', () => {
    this.setData('isJumping', Phaser.UP);
  });

  leftbtn.on('pointerup', () => {
    this.setData('horizontalDirection', Phaser.NONE);
  });

  rightbtn.on('pointerup', () => {
    this.setData('horizontalDirection', Phaser.NONE);
  });

  upbtn.on('pointerup', () => {
    this.setData('isJumping', Phaser.NONE);
  });
}

update() {
  this.onGround = this.body.onFloor();
  if (this.cursors.left.isDown || this.getData('horizontalDirection') === Phaser.LEFT) {
    this.leftWalk();
  } else if (this.cursors.right.isDown || this.getData('horizontalDirection') === Phaser.RIGHT) {
    this.rightWalk();
  } else {
    this.idle();
  }
  if (this.cursors.up.isDown || this.getData('isJumping') === Phaser.UP) {
    this.jump();
  }
}

La cámara sigue al prota

class Platform extends Phaser.Scene {
  create() {
    ...
    this.cameras.main.setSize(gameWidth, gameHeight);
    this.cameras.main.startFollow(this.player);
  }

La cámara sigue al prota horizontalmente

Eliminamos la línea:

// this.cameras.main.startFollow(this.player);
class Platform extends Phaser.Scene {
  create(){
    this.cameras.main.scrollY = 0;
  }

  update(){
    ...
    this.cameras.main.scrollX = this.player.x - 400;
  }

El fondo se repite

Sustituímos la línea que carga el fondo
this.bg = this.add.tileSprite(gameWidth/2, gameHeight/2, gameWidth, gameHeight, 'back').setScrollFactor(0);

En el método update, desplazamos el fondo acorde con la posición del jugador

class Platform extends Phaser.Scene {
  update() {
    this.bg.tilePositionX = this.player.x;

Método para la inserción de enemigos

class Platform extends Phaser.Scene {
  create() {
    ...
    const antsFromTiled = this.findObjectsByClassInObjectsLayer('antEnemy', map);
    const ants = this.insertBadGuys(this, antsFromTiled, AntEnemy);
  }

  insertBadGuys(scene, arrayDeMalos, type) {
    const enemies = scene.physics.add.group({classType: type, runChildUpdate: true, runChildCreate: true, gravityY:100}); // Cuando instanciamos un grupo, los parámetros de gravedad los ponemos en la creación del grupo en lugar de en el personaje individual
    for (let i = 0; i < arrayDeMalos.length; i++) {
      const malo = new type(scene, arrayDeMalos[i].x, arrayDeMalos[i].y);
      enemies.add(malo);
    }
    return enemies;
  }
}
class AntEnemy extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    super(scene, x, y, 'ant');
    scene.physics.add.collider(this, scene.collisionLayer);
    scene.add.existing(this);
  }

  static loadAssets(scene) {
    scene.load.spritesheet('ant', ant, { frameWidth: 192, frameHeight: 96 });
  }
}
export default AntEnemy;

Enemigo con animación

Las animaciones deben ser cargadas desde el create de la escena.

class AntEnemy extends Phaser.Physics.Arcade.Sprite {
  constructor(x, y, scene) {
  ...
  this.anims.create({
    key: 'antWalk',
    frames: this.anims.generateFrameNumbers('ant', { start: 0, end: 3 }),
    frameRate: 7,
    repeat: -1,
  });
    this.play('antWalk');
  } 
}

export default AntEnemy;

Enemigo se mueve

constructor(x, y, scene) {
  ...
  this.speed = 100;
  this.direction = 1;
}

update() {
  this.body.setVelocityX(this.direction* this.speed);
}

Enemigo inteligente

class AntEnemy extends Phaser.Physics.Arcade.Sprite {

  update(){
    ...
    const nextX = Math.floor(this.x / 32) + this.direction;
    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.direction *= -1;
    }
    if (this.direction > 0) {
      this.flipX = false;
    } else {
      this.flipX = true;
    }
  }
}

Player muere, Enemigo muere

class Platform extends Phaser.Scene {
  ...
  create() {
    ...
    this.physics.add.overlap(this.player, ants, this.player.checkEnemy, null, this.player);
  }
class Player extends Phaser.Physics.Arcade.Sprite {
  checkEnemy(player, enemy) {
    //  El jugador está cayendo?
    if (this.body.velocity.y > 0) {
      enemy.die();
    } else {
      this.die();
    }
  }
  die() {
    this.disableBody();
  }
class AntEnemy extends Phaser.Physics.Arcade.Sprite {
  die() {
    this.disableBody();
  }

Enemigo explota

class AntEnemy extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y, sprite) {
    ...
    this.anims.create({
      key: 'crashAnim',
      frames: this.anims.generateFrameNumbers('crash', {start: 0, end: 4}),
      frameRate: 7
    });
    this.on('animationcomplete', this.animationComplete, this);
  }
	
  static loadAssets(scene) {
    ...
    scene.load.spritesheet('crash', crashImg, { frameWidth: 199, frameHeight: 200 });
  }
		
  die() {
    this.disableBody();
    this.play('crashAnim');
  }

  animationComplete(animation, frame, sprite) {
    if (animation.key === 'crashAnim') {
      this.destroy();
    }
  }
}

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.isAlive = true;
    this.on('animationcomplete', this.animationComplete, this);
    this.anims.create({
      key: 'crashAnim',
      frames: this.anims.generateFrameNumbers('crash', { start: 0, end: 4 }),
      frameRate: 7
    });
  }

  die() {
    this.isAlive= false;
    this.disableBody();
    //Para que esta animación se ejecute es necesario haber puesto el código que evalúa this.estaVivo en el update, para que no se sigan ejecutando el resto de animaciones y comportamientos
    this.play('crashAnim');
  }

  animationComplete(animation, frame, sprite) {
    if (animation.key === 'crashAnim') {
      this.destroy();
    }
  }

  update() {
    if (this.isAlive) {
      this.onGround = this.body.onFloor();
      if (this.cursors.left.isDown || this.getData('horizontalDirection') === Phaser.LEFT) {
        this.leftWalk();
      } else if (this.cursors.right.isDown || this.getData('horizontalDirection') === Phaser.RIGHT) {
        this.rightWalk();
      } else {
        this.idle();
      }

      if (this.cursors.up.isDown || this.getData('isJumping') === Phaser.UP) {
        this.jump();
      }
    }
  }
}

Enemigo con Herencia

Creamos una nueva clase llamada Enemy de tal forma que AntEnemy herede de Enemy. Debemos las partes del código de AntEnemy que son comunes a todos los enemigos a la clase Enemy.

Aunque cambiemos la estructura del código interno, en este punto el juego debe seguir haciendo lo mismo.

class Enemy extends Phaser.Phantysics.Arcade.Sprite{
  constructor(scene, x, y, sprite ) {
    super(scene, x, y, sprite);
    scene.physics.add.collider(this, scene.collisionLayer);
    scene.add.existing(this);
    this.speed = 100;
    this.direction = 1;

    this.anims.create({
      key: 'crashAnim',
      frames: this.anims.generateFrameNumbers('crash', { start: 0, end: 4 }),
      frameRate: 7
    });
    this.on('animationcomplete', this.animationComplete, this);
  }

  animationComplete(animation, frame, sprite) {
    if (animation.key === 'crashAnim') {
      this.destroy();
    }
  }

  static loadAssets(scene) {
    scene.load.spritesheet('crash', crash, { frameWidth: 199, frameHeight: 200 });
  }

  update() {
    this.body.setVelocityX(this.direction * this.speed);

    const nextX = Math.floor(this.x / 32) + this.direction;
    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.direction *= -1;
    }
    if (this.direction > 0) {
      this.flipX = false;
    } else {
      this.flipX = true;
    }
  }

  die() {
    this.disableBody();
    this.play('crashAnim');
  }
}

export default Enemy;
class AntEnemy extends Enemy {
  constructor(scene, x, y) {
    super(scene, x, y, 'ant');

    this.anims.create({
      key: 'antWalk',
      frames: this.anims.generateFrameNumbers('hormiga', { start: 0, end: 3 }),
      frameRate: 7,
      repeat: -1,
    });
    this.play('antWalk');
  }

  static loadAssets(scene) {
    scene.load.spritesheet('ant', ant, { frameWidth: 192, frameHeight: 96 });
    scene.load.spritesheet('crash', crash, { frameWidth: 199, frameHeight: 200 });
  }
}

export default AntEnemy;

Insertar Oruga

class CaterpillarEnemy extends Enemy {
  constructor(scene, x, y) {
    super(scene, x, y, 'caterpillar');
    this.anims.create({
      key: 'caterpillarWalk',
      frames: this.anims.generateFrameNumbers('caterpillar', { start: 0, end: 3 }),
      frameRate: 7,
      repeat: -1,
    });
    this.play('caterpillarWalk');
  }

  static loadAssets(scene) {
    scene.load.spritesheet('caterpillar', caterpillar, { frameWidth: 96, frameHeight: 192 });
  }
}

export default CaterpillarEnemy;
class Platform extends Phaser.Scene {
  preload() {
    ...
      CaterpillarEnemy.loadAssets(this);
  }

  create(){
    ...
    const caterpillarsFromTiled = this.findObjectsByClassInObjectsLayer('caterpillarEnemy', map);
    const caterpillars = this.insertBadGuys(this, caterpillarsFromTiled, CaterpillarEnemy);
    this.physics.add.overlap(this.player, caterpillars, this.player.checkEnemy, null, this.player);
  }

Insertar Avispa

class BeeEnemy extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    super(scene, x, y, 'bee');
    scene.add.existing(this);
    this.anims.create({
      key: 'beeFly',
      frames: this.anims.generateFrameNumbers('bee', { start: 0, end: 2 }),
      frameRate: 10,
      repeat: -1,
    });
    this.play('beeFly');
  }

  static loadAssets(scene) {
    scene.load.spritesheet('bee', bee, { frameWidth: 128, frameHeight: 128 });
  }
}

export default BeeEnemy;

Hacer que la avispa de vueltas

class BeeEnemy extends Phaser.Physics.Arcade.Sprite {
  constructor(x, y, scene) {
    ...
    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.001;
    this.currentPosition = new Phaser.Math.Vector2();
		
    // La función getPoint aplicada sobre la trayectoria de vuelo (flyPath), modificara el valor de currentPosition para darle unas coordenadas en funcion del grado de completitud de la trayectoria (primer parametro) recibe dos parámetros
    this.flyPath.getPoint(this.pathIndex, this.currentPosition);
    this.setPosition(this.currentPosition.x, this.currentPosition.y);
  }

  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 currentPosition, que estará en función del grado de completitud de la trayectoria.
    this.flyPath.getPoint(this.pathIndex, this.currentPosition);
    // Modificamos la posición de la avispa en función de las coordenadas x e y del vector.
    this.setPosition(this.currentPosition.x, this.currentPosition.y);
  }
}

Refactorizar el código para utilizar el estado FLYING y la función checkPlayer

Añadiremos los estados después de la clase Avispa.

class BeeEnemy extends Phaser.Physics.Arcade.Sprite {
// 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 === BeeEnemy.FLYING) {
      this.checkPlayer();
    }
  }

  checkPlayer(){
    this.pathIndex = Phaser.Math.Wrap(this.pathIndex + this.pathSpeed, 0, 1);
    this.flyPath.getPoint(this.pathIndex, this.currentPosition);
    this.setPosition(this.currentPosition.x, this.currentPosition.y);
  }
}
BeeEnemy.FLYING = 0;

Avispa detecta a Player

class BeeEnemy 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 BeeEnemy extends Phaser.Physics.Arcade.Sprite {

  update(time) {
    if (this.state === BeeEnemy.FLYING) {
      this.checkPlayer();
    } else if (this.state === BeeEnemy.CHASING) {
      this.chasePlayer();
    }
  }

  checkPlayer(){
    ...
    const player = this.scene.player;
    if (this.patrolCircle.contains(player.x, player.y)) {
      this.pathIndex = 0;
      this.state = BeeEnemy.CHASING;
    }
  }

  chasePlayer() {
    const player = this.scene.player;
    this.scene.physics.moveTo(this, player.x, player.y, this.speed);
  }
}

BeeEnemy.FLYING = 0;
BeeEnemy.CHASING = 1;

Avispa ataca a Player

class BeeEnemy extends Phaser.Physics.Arcade.Sprite {
  constructor(scene, x, y) {
    ...
    this.anims.create({
      key: 'beeAttack',
      frames: this.anims.generateFrameNumbers('bee', { frames: [ 3, 4, 5, 4, 3 ] }),
      frameRate: 10
    });
    this.on('animationcomplete', this.animationComplete, this);
  }

  chasePlayer() {
    ...
    if (this.scene.physics.overlap(this, player)) {
      this.play('beeAttack', true);
    }
  }

  animationComplete(animation, frame, sprite) {
    if (animation.key === 'beeAttack') {
      alert("termina ataque");
    }
  } 
}

En vez de un alert, cuando la avispa colisione contra el Player, detonaremos la animación de ataque.

Avispa vuelve a casa

class BeeEnemy extends Phaser.Physics.Arcade.Sprite {
  constructor(x, y, scene) {
    ...
    this.startPlace = new Phaser.Math.Vector2(this.currentPosition.x, this.currentPosition.y);
  }

  update(time) {
    ...
    }else if (this.state === BeeEnemy.RETURNING) {
      returnHome();
    }
  }

  animationComplete(animation, frame, sprite) {
    if (animation.key === 'beeAttack') {
      this.state = BeeEnemy.RETURNING;
    }
  }

  chasePlayer(time){
    ...
    if (!this.patrolCircle.contains(player.x, player.y) && time - this.timeFromStartingToChase > 4000) {
      this.returnHome();
    }
  }

  returnHome() {
    this.play('beeFly', true);
    this.scene.physics.moveTo(
      this,
      this.startPlace.x,
      this.startPlace.y,
      this.speed
    );
    
    if (
      Math.round(this.x) === this.startPlace.x &&
      Math(this.y) === this.startPlace.y
    ) {
      this.flyPath.getPoint(this.pathIndex, this.currentPosition);
      this.pathIndex = 0;
      this.state = BeeEnemy.FLYING;
    }
  }
}

BeeEnemy.FLYING= 0;
BeeEnemy.CHASING= 1;
BeeEnemy.RETURNING = 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

chasePlayer() {
  ...
  if (this.scene.physics.overlap(this, player)){
    ...
    player.playerHittedByBee();
  }
}
playerHittedByBee(){
  alert('golpeado');
}

El Player sale despedido por los aires

playerHittedByBee(){
  this.isDazing = true;
  this.body.setVelocity(-150,-150);
  this.play('fall', true);
}

update() {
  if(this.isAlive){
    if(!this.isDazing){
    //aquí dentro vendría todo el código que ya teníamos de la función update
    }
  }	
}

Reactivar los controles del jugador

playerHittedByBee() {
  ...
  this.scene.time.addEvent({delay: 1000, callback: this.dazeFinished, callbackScope: this});
}

dazeFinished() {
  this.isDazing= false;
}

Añadir meta

preload(){
  ...
  this.load.image('goal', goal);
}
create(){
  ...
  const goalFromTiled = this.findObjectsByClassInObjectsLayer('goal', map)[0];
  this.putCheckPoint(goalFromTiled.x, goalFromTiled.y, 'goal');
}

putCheckPoint(x, y, sprite) {
  const goal = this.physics.add.sprite(x, y, sprite);
  goal.body.immovable = true;
  goal.body.moves = false;
  goal.setSize(160, 160);
}

Fin del juego

putCheckPoint(x, y, sprite) {
  ...
  this.physics.add.overlap(this.player, goal, this.playerReachGoal, null, this);
}
playerReachGoal(){
  this.scene.start('Win');
}

Limitando la cámara

this.cameras.main.setBounds(0, 0, 3520, 640);

Escena de perder tras explotar

animationComplete(animation, frame, sprite) {
  if (animation.key === 'crashAnim') {
    this.scene.scene.start('Lose');
  }
}

Escena de perder tras caer por precicipio

./src/scenes/Platform/index.js
this.physics.world.setBoundsCollision(false, false, false, true);
this.physics.world.on('worldbounds', () => {
    this.scene.start('Lose');
});
./src/characters/Player/index.js
this.setCollideWorldBounds(true);
this.body.onWorldBounds = true;

Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.