Plataformas con Phaser

Contactar con el profesor

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;
← Tiled
Carreras con arcade y tiles →

Aviso Legal | Política de privacidad