Phaser

  1. ¿Qué es Phaser?
  2. Acierta Imagen
    1. Estructura HTML
    2. Estructura Javascript
    3. Cargar fondo y caras
    4. Detecta click/pulsación sobre la cara
    5. Cara random
    6. Evalúa solución
    7. Caras aleatorias
    8. Refrescar escena
    9. Marcador
    10. Temporizador
  3. Haciendo el juego responsive y publicación
  4. Elije tu propia Aventura
  5. Fall down
    1. Gravedad
    2. Un caramelo por segundo
    3. Detecta pulsación sobre caramelo
    4. Caramelo random
    5. El caramelo cae desde diferentes posiciones
    6. Perder el juego
  6. Flappy Bird
    1. Detectar teclas.
    2. Colisiones
    3. Crear tuberías
    4. Colisión
  7. Coches
    1. Instalación del Map Editor
    2. Creamos un documento json utilizando el map editor
    3. Objects Layer
    4. Mover coche hacia delante
    5. Rotar coche
  8. Plataformas

¿Qué es phaser?

Es una librería de Javascript que permite desarrollar videojuegos en este lenguaje de forma muy rápida.

Algunos enlaces de interés

Estructura HTML

Nuestro código Javascript estará dentro de un documento HTML similar al siguiente:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>El título del juego</title>
	<meta name="viewport" content="user-scalable=no,  initial-scale=1,  maximum-scale=1,  width=device-width" />
	<style>
		html, body {
			margin: 0 auto;
			padding: 0;
			background: black;
		}
	</style>

	<script src="phaser.js"></script>
	<script src="javascript.js"></script>
</head>
<body>

</body>
</html>
Estructura básica

Estructura Javascript

class Escena extends Phaser.Scene {

  preload() {
    console.log('preload');
  }

  create() {
    console.log('create');
  }

  update() {
    console.log('update');
  }
}

const config = {
  type: Phaser.AUTO,
  width: 960,
  height: 640,
  scene: Escena,
};

new Phaser.Game(config);

Método resize

function resize(){
	const canvas = document.querySelector("canvas");
	const windowWidth = window.innerWidth;
	const windowHeight = window.innerHeight;

	canvas.style.width = `${windowWidth}px`;
	canvas.style.height = `${windowHeight}px`;

/*  const windowRatio = windowWidth / windowHeight;
	const gameRatio = config.width / config.height;
	if(windowRatio < gameRatio){
		canvas.style.width = `${windowWidth}px`;
		canvas.style.height = `${windowWidth / gameRatio}px`;
	}else{
	canvas.style.width = `${windowHeight * gameRatio}px`;
	canvas.style.height = `${windowHeight}px`;
}
*/
}
  preload() {
	resize();
	window.addEventListener('resize', resize);
	console.log('preload');
};

Acierta imagen

Acierta imágen Descargar recursos

Carga fondo

preload() {
	resize();
	this.load.image('fondo', '/curso/assets/phaser/acierta-imagen/fondo.jpg');
}

create() {
	this.add.sprite(480, 320, 'fondo');
}
Carga fondo

Carga resto de imagenes

preload() {
	this.load.image('fondo', '/curso/assets/phaser/acierta-imagen/fondo.jpg');
	this.load.image('caraIMG0', '/curso/assets/phaser/acierta-imagen/cara0.png');
	this.load.image('caraIMG1', '/curso/assets/phaser/acierta-imagen/cara1.png');
	this.load.image('caraIMG2', '/curso/assets/phaser/acierta-imagen/cara2.png');
}
  create() {
	this.add.sprite(480, 320, 'fondo');
	const cara1 = this.add.sprite(225, 425, 'caraIMG0');
	const cara2 = this.add.sprite(480, 460, 'caraIMG1');
	const cara3 = this.add.sprite(740, 425, 'caraIMG2');
	cara1.setScale(0.5, 0.5);
	cara2.setScale(0.5, 0.5);
	cara3.setScale(0.5, 0.5);
}
Carga imágenes

Cara pulsada

Dentro del método createcara0.on('pointerdown', this.caraPulsada);
cara1.on('pointerdown', this.caraPulsada);
cara2.on('pointerdown', this.caraPulsada);
caraPulsada() {
	alert(this.texture.key);
}
Cara pulsada

Imagen soĺución random

const random = Math.floor(Math.random() * 3);
this.add.sprite(400, 100, `caraIMG${random}`);
Imágen solución

Evaĺúa solución

create() {
	//Aquí va el resto de la función create
	const random = Math.floor(Math.random() * 3);
	this.spriteSolucion = this.add.sprite(400, 100, `caraIMG${random}`);
}
caraPulsada(cara) {
	if (cara.texture.key === this.spriteSolucion.texture.key) {
		alert('exito');
	} else {
		alert('fracaso');
	}
}
Evalúa solución

Caras aleatorias

En el método createconst numeros = [0, 1, 2, 3];
const caras = [];

for (let i = 0; i < 3; i++) {
	const posicion = Math.floor(Math.random() * numeros.length);
	const valor = numeros[posicion];
	let x;
	let y;
	if (i === 0) {
		x = 225;
		y = 425;
	} else if (i === 1) {
		x = 480;
		y = 460;
	} else if (i === 2) {
		x = 740;
		y = 425;
	}

	caras[i] = this.add.sprite(x, y, `caraIMG${valor}`).setInteractive();
	caras[i].setScale(0.5, 0.5);
	caras[i].on('pointerdown', () => this.caraPulsada(caras[i]));
	numeros.splice(posicion, 1);
}
Acierta imágen

Refrescar imagenes

cargarImagenes() {
	this.add.sprite(480, 320, 'fondo');

	const numeros = [0, 1, 2, 3];
	const caras = [];

	for (let i = 0; i < 3; i++) {
		const posicion = Math.floor(Math.random() * numeros.length);
		const valor = numeros[posicion];
		let x;
		let y;
		if (i === 0) {
			x = 225;
			y = 425;
		} else if (i === 1) {
			x = 480;
			y = 460;
		} else if (i === 2) {
			x = 740;
			y = 425;
		}
		caras[i] = this.add.sprite(x, y, `caraIMG${valor}`).setInteractive();
		caras[i].setScale(0.5, 0.5);
		caras[i].on('pointerdown', () => this.caraPulsada(caras[i]));
		numeros.splice(posicion, 1);
	}

	const random = Math.floor(Math.random() * 3);
	this.spriteSolucion = this.add.sprite(480, 190, caras[random].texture.key);
}
Refrescar imágenes

Marcador

Para cargar una tipografía externa en nuestro proyecto:

<style>
    @font-face {
        font-family: font1;
        src: url('/curso/assets/phaser/coches/fuentes/redkost-comic.otf');
        font-weight:400;
        font-weight:normal;
    }
</style>
<div style="font-family:font1;position: absolute;left: -1000px;visibility: hidden;">.</div>

Debemos cargar dos textos para el marcador, uno dinámico que aumenta cuando el usuario acierta:

create() {
this.marcador = 0;
this.marcadorTXT = this.add.text(90, 120, this.marcador, {
    fontFamily: 'font1',
    fontSize: 60,
    color: '#00ff00',
    align: 'right'
});
this.marcadorTXT.setOrigin(1, 0);

  this.cargarImagenes();
}
++this.marcador;
this.marcadorTXT.setText(this.marcador);

Y otro que tendrá la palabra pts:

this.add.text(105, 150, 'pts', {
    fontFamily: 'font1',
    fontSize: 24,
    color: '#00ff00'
});
Refrescar imágenes

Temporizador

this.topeDeTiempo = 10;
this.tiempo = this.topeDeTiempo;
this.tiempoTXT = this.add.text(835, 130, this.tiempo, {
    fontFamily: 'font1',
    fontSize: 64,
    color: '#00ff00',
});
this.tiempoTXT.rotation = 20*Math.PI/180;
this.temporizador();
temporizador() {
    --this.tiempo;
    this.tiempoTXT.setText(this.tiempo)
    if (this.tiempo === 0) {
        alert('Se acabó el tiempo')
    } else {
        setTimeout(() => this.temporizador(), 1000);
    }
}
Refrescar imágenes

Escena de perder

class PerderEscena extends Phaser.Scene {
    constructor() {
        super('perderScene');
    }
    preload() {
        this.load.image('fin', '/curso/assets/phaser/perder-juego.jpg');
    }
    create() {
        this.add.image(480, 320, 'fin');
        this.input.on('pointerdown', () => this.volverAJugar())
    }
    volverAJugar() {
        this.scene.start('Escena');
    }
}
this.input.on('pointerdown', () => this.volverAJugar())
Refrescar imágenes

Haciendo el juego responsive y publicación

Para lograr un óptimo resultado, utilizaremos la siguiente metaetiquta y el siguiente código CSS:

<meta name="viewport" content="user-scalable=no,  initial-scale=1,  maximum-scale=1,  width=device-width" />
<style>
        body{
            background: brown

        }
        canvas{
            display:block;
            margin: 0;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
	}
</style>

Tras haber añadido estos códigos, integraremos el siguiente código en nuestra aplización:

window.addEventListener("resize", resize, false);

function preload (){
	resize();
	...
}
function resize (width, height){
	var canvas = document.querySelector("canvas");
	var windowWidth = window.innerWidth;
	var windowHeight = window.innerHeight;

	canvas.style.width = windowWidth + "px";
	canvas.style.height = windowHeight + "px";


	/* var windowRatio = windowWidth / windowHeight;
	var gameRatio = game.config.width / game.config.height;
	if(windowRatio < gameRatio){
		canvas.style.width = windowWidth + "px";
		canvas.style.height = (windowWidth / gameRatio) + "px";
	}else{
		canvas.style.width = (windowHeight * gameRatio) + "px";
		canvas.style.height = windowHeight + "px";
	}*/
}

Apache cordova

A la hora de compilar la aplicación con Apache Cordova o Phonegap Build es altamente recomendable incluir en la raíz de nuestro código (a la misma altura que el index.html), un fichero llamado config.xml con el siguiente código por defecto:

<?xml version="1.0" encoding="UTF-8" ?>
	<widget xmlns   = "http://www.w3.org/ns/widgets"
		xmlns:gap   = "http://phonegap.com/ns/1.0"
		id          = "com.phonegap.example"
		versionCode = "1"
		version     = "1.0.0" >
	<name>PhoneGap Example App</name>
	<description>
		An example for phonegap build app which wont show up in the playstore.
	</description>
	<author href="https://pablomonteserin.com/" email="pablomonteserin@pablomonteserin.com">
		Pablo Monteserín
	</author>
	<preference name="Orientation" value="landscape" />
	<preference name="Fullscreen" value="true" />
</widget>

En este fichero, podemos:

Poner la aplicación a pantalla completa: <preference name="Fullscreen" value="true" /> 
Forzar la pantalla a horizontal o vertical<preference name="Orientation" value="landscape" />
<preference name="Orientation" value="portrait" />

Ejercicio Elije tu propia aventura

Acierta imagen con Phaser.js Descargar recursos

Partimos de una sala con dos puertas. Una lleva a una muerte segura y la otra a un tesoro.

Cargamos el fondo

Area de la puerta pulsada

const opcionNave = this.add.zone(140, 10, 450, 410);
opcionNave.setName('nave');
opcionNave.setOrigin(0);
opcionNave.setInteractive();
opcionNave.once('pointerdown', () => this.opcionPulsada(opcionNave));
this.add.graphics().lineStyle(2, 0xff0000).strokeRectShape(opcionNave);

Evalúa opción

opcionPulsada(opcion) {
if (opcion.name === 'nave') {
	alert('nave');
	} else {
		alert('tierra');
	}
}

Con cambio de escena

opcionPulsada(opcion) {
	if (opcion.name === 'nave') {
		this.scene.start('naveScene');
	} else {
		this.scene.start('homeScene');
	}
}
class EscenaNave extends Phaser.Scene {

	constructor() {
		super({key: 'naveScene'});
	}
	...
}
Puerta random

Cargar escena

var sceneConfigA = {
	key: 'sceneA',
	preload: preload,
	create: create
};

var sceneConfigB = {
	key: 'sceneB',
	preload: preloadB,
	create: createB
};

var gameConfig = {
	type: Phaser.CANVAS,
	parent: 'phaser-example',
	width: 800,
	height: 600,
	scene: [ sceneConfigA, sceneConfigB ]
};

var game = new Phaser.Game(gameConfig);
...
juego = this
...
juego.scene.switch('sceneB');
Cambio de escena

Fall down

Descargar recursos

Cargar fondo

Físicas

create() {
	this.add.sprite(320, 480, 'fondo');
	this.physics.add.image(50, 100, 'misil0');
}

const config = {
	type: Phaser.AUTO,
	width: 640,
	height: 960,
	scene: Escena,
	physics: {
		default: 'arcade',
		arcade: {
			debug: true,
			gravity: {
				y: 300,
			},
		},
	},
};

Múltiples elementos

En el create()this.time.delayedCall(1000, this.lanzarMisil(), [], this);
lanzarMisil() {
	const misil = this.physics.add.sprite(50, 100, 'misil0');
	misil.setVelocity(0, 200);
	this.time.delayedCall(1000, this.lanzarMisil, [], this);
}

Detectar pulsación

misil.on('pointerdown', () => this.misilPulsado(misil));
misilPulsado(m) {
	m.destroy();
}

Misiles variados

const aleatorio = Math.floor(Math.random() * 2);
const misil = this.physics.add.sprite(50, 100, `misil${aleatorio}`).setInteractive();

Cargar imágenes

Ver ejemplo de carga de imágenes

El caramelo cae por gravedad

En phaser Hay varios tipos de físicas:

  • ARCADE: las más básicas, fáciles de gestionar y que ofrecen un mejor rendimiento. Nos permiten gestionar la colisión entre dos rectángulos (dos bounding box rectangulares).
    Phaser arcade collider
  • NINJA: permiten crear rampas. Esto con las anteriores no era posible, ya que las areas de colisión no podían ser triangulares.
    Phaser ninja collider
  • P2: son las físicas más complejas y que consumen más recursos. Pueden no ser muy apropiadas para dispositivos móviles.
var config = {
	...
	physics: {
        default: 'arcade',
        arcade: {
            debug: true,
            gravity: { y: 200 }
        }
    }
};
...
function create(){
	...
	var block = this.physics.add.image(50, 100, 'c0');
	block.setVelocity(0, 200);
}
Ver ejercicio de un caramelo que cae por gravedad

Un caramelo por segundo

function create(){
	this.time.delayedCall(1000, onEvent, [], this);
}

function onEvent(){
	var block = this.physics.add.image(50, 100, 'c0');
	block.setVelocity(0, 200);
	this.time.delayedCall(1000, onEvent, [], this);
}
Aplicar físicas

Detecta pulsación sobre caramelo

function clickCandy(){
	this.destroy();
}
Detectar click

Caramelo random

var aleatorio = Math.floor(Math.random()*4);
var candy = this.add.sprite(50, 50, 'c'+aleatorio);
Caramelo aleatorio

El caramelo cae desde diferentes posiciones

Que el caramelo caiga en diferentes posiciones horizontales

Ver ejemplo de los caramelos cayendo en distintas posiciones

Perder el juego

function create(){
	...
	this.physics.world.setBoundsCollision(true, true, true, true);
	this.physics.world.on("worldbounds", function (body) {
		alert("fin del juego")
	})

function onEvent(){
	...
	block.setCollideWorldBounds(true);
	block.body.onWorldBounds = true;
Ver ejercicio de perder la partida

Flappy Bird

Juego coches con Phaser.js Descargar recursos

Pájaro con físicas y animación

class Escena extends Phaser.Scene {
preload() {
	resize();
	window.addEventListener('resize', resize, false);
	this.load.spritesheet('heroe', '/curso/assets/phaser/flappy-bird/heroe.png', {frameWidth: 150, frameHeight: 59});
}

create() {
	const pajaro = this.physics.add.sprite(50, 100, 'heroe');

	this.anims.create({
		key: 'volar',
		frames: this.anims.generateFrameNumbers('heroe', {start: 0, end: 2}),
		frameRate: 10,
		repeat: -1,
	});
	pajaro.play('volar', true);
	}
}

Saltar con space

this.input.keyboard.on('keydown', function (event) {
	if (event.keyCode === 32) {
		this.scene.saltar();
	}
});

saltar() {
	this.player.setVelocityY(-200);
}

Saltar con click en la pantalla

const pantalla = this.add.zone(0, 0, 960, 640);
pantalla.setInteractive();
pantalla.setOrigin(0);
pantalla.on('pointerdown', () => this.saltar());

Animación al saltar

saltar() {
	this.player.setVelocityY(-200);
	this.player.play('saltar');
}

Seguir volando

En el métdodo create():this.player.on('animationcomplete', this.animationComplete, this);
animationComplete(animation, frame, sprite) {
	if (animation.key === 'saltar') {
		this.player.play('volar');
	}
}

Las tuberías

nuevaColumna() {
	//Una columna es un grupo de cubos
	const columna = this.physics.add.group();
	//Cada columna tendrá un hueco (zona en la que no hay cubos) por dónde pasará el super heroe
	const hueco = Math.floor(Math.random() * 5) + 1;
	for (let i = 0; i < 8; i++) {
    	//El hueco estará compuesto por dos posiciones en las que no hay cubos, por eso ponemos hueco +1
		if (i !== hueco) {
			const cubo = columna.create(960, i * 60 + 10, 'pipe');
			cubo.body.allowGravity = false;
		}
	}
	columna.setVelocityX(-200);
	//Detectaremos cuando las columnas salen de la pantalla...
	columna.checkWorldBounds = true;
	//... y con la siguiente línea las eliminaremos
	columna.outOfBoundsKill = true;
	//Cada 1000 milisegundos llamaremos de nuevo a esta función para que genere una nueva columna
	this.time.delayedCall(1000, this.nuevaColumna, [], this);
}

Aumentando el espacio entre tuberías

if (i !== hueco && i !== hueco + 1 && i !== hueco - 1) {

Colisión con tuberías

Última línea del método nueva columna:this.physics.add.overlap(this.player, columna, this.hitColumna, null, this);
hitColumna() {
	alert('game over');
}

Poner tapas a las tuberías

if (i == hueco - 3) {
	cubo = columna.create(960, i * 44, 'pipeArriba');
} else if (i == hueco + 3) {
	cubo = columna.create(960, i * 44, 'pipeAbajo');
} else {
	cubo = columna.create(960, i * 44, 'pipe');
}

Imagen de tuberías aleatoria

const aleatorio = Math.floor(Math.random() * 2);
for (let i = 0; i < 8; i++) {
	//El agujero son cuatro casillas
	if (i !== hueco && i !== hueco + 1 && i !== hueco - 1) {
		let cubo;
		if (i == hueco - 2) {
			cubo = columna.create(960, i * 100, `pipeArriba${aleatorio}`);
		} else if (i == hueco + 2) {
			cubo = columna.create(960, i * 100, `pipeAbajo${aleatorio}`);
		} else {
			cubo = columna.create(960, i * 100, `pipe${aleatorio}`);
		}
		cubo.body.allowGravity = false;
	}
}

Pájaro en escena con físicas

Documentación caída por gravedad

Las medidas del sprite son 66x48

Pájaro en pantalla

Saltando al pulsar espacio

function create(){
	...
	this.input.keyboard.on('keydown', function (event) {
		if(event.keyCode== 32){
			pajaro.setVelocityY(-200);
		}
	});
}
El pájaro salta al pulsar espacio

Crear tuberías

function create(){
	...
	timedEvent = this.time.delayedCall(1000, nuevaColumna, [], this);
}

function nuevaColumna() {
	var columna = this.physics.add.group();

	var hueco = Math.floor(Math.random() * 5) + 1;
	for (var i = 0; i < 8; i++) {
		//El agujero son dos casillas, por eso ponemos hole +1
		if (i != hueco && i != hueco + 1) {
			var cubo = columna.create(800, i * 60 + 10, 'pipe');
			cubo.body.allowGravity = false;
		}
	}
	columna.setVelocityX(-200);
	columna.checkWorldBounds = true;
	columna.outOfBoundsKill = true;
	timedEvent = this.time.delayedCall(1000, nuevaColumna, [], this);
}
Ponemos las tuberías en pantalla

Otra aproximación para resolver este ejercicio sería crear un juego con gravedad 0 (configurado en el objeto config), y luego asignarle gravedad al pájaro:

pajaro.setGravity(000);

Colisión con tuberías

this.physics.add.overlap(pajaro, columna, hitColumna, null, this);

function hitColumna(){
	alert("game over")
}
Detectamos la colisión del pájaron con las tuberías

Coches

Juego coches con Phaser.js Descargar recursos

Carga background

Ver ejemplo

Sprite animado

Ver ejemplo
Dentro del método create()this.player1 = this.add.sprite(400, 500, 'player1');

this.anims.create({
	key: 'volar',
	frames: this.anims.generateFrameNumbers('player1', {start: 0, end: 1}),
	frameRate: 10,
	repeat: -1,
});
this.player1.play('volar', true);

Carga coches con función

create() {
	this.add.sprite(480, 320, 'fondo');
	this.coordenadasPlayers = [
		{x: 280, y: 500},
		{x: 360, y: 500},
		{x: 300, y: 550},
		{x: 380, y: 550},
	];
	this.players = [];
	this.n_jugadores = 4;
	this.creaPlayers();
}

creaPlayers() {
	for (let i = 0; i < this.n_jugadores; i++) {
		//Creamos el jugador. Lo almacenamos en un array de jugadores porque nos será más fácil en adelante aplicar funcionalidades a todos los jugadores
		this.players[i] = this.add.sprite(this.coordenadasPlayers[i].x, this.coordenadasPlayers[i].y, `player${i}`);
		//Creamos la animación del jugador
		this.anims.create({
			key: `volar${i}`,
			frames: this.anims.generateFrameNumbers(`player${i}`, {start: 0, end: 1}),
			frameRate: 10,
			repeat: -1,
		});
		//Vinculamos la animación al jugador creado y comenzamos su reproducción.
		//El primer parámetro de la función volar es el nombre de la animación, y el segundo, si la animación se va a ejecutar indefinidamente
		this.players[i].play(`volar${i}`, true);
	}
}
Ver ejemplo

Mover coche hacia delante

update() {
	for (let i = 0; i < this.n_jugadores; i++) {
		this.players[i].thrust(-0.001);
	}
}

const config = {
	type: Phaser.AUTO,
	width: 960,
	height: 640,
	scene: Escena,
	physics: {
		default: 'matter',
		matter: {
			gravity: {
				y: 0,
			},
		},
	},
};
Ver ejemplo

Rotar

update() {
for (let i = 0; i < this.n_jugadores; i++) {
	this.players[i].thrust(-0.003);
	if (this.cursors.left.isDown)
		this.players[i].setAngularVelocity(-0.065);
	else if (this.cursors.right.isDown)
		this.players[i].setAngularVelocity(0.065);
	}
}
Ver ejemplo

Controles visuales

creaPlayers() {
for (let i = 0; i < this.n_jugadores; i++) {
		//Código del bucle que no pongo porque ocupa mucho
		this.controlesVisuales(i);
	}
}

controlesVisuales(n) {
	this.players[n].setData('direccionHorizontal', 0);

	const leftbtn = this.add.sprite(this.coordenadasPlayers[n].leftbtn.x, this.coordenadasPlayers[n].leftbtn.y, 'leftbtn').setInteractive();
	const rightbtn = this.add.sprite(this.coordenadasPlayers[n].rightbtn.x, this.coordenadasPlayers[n].rightbtn.y, 'leftbtn').setInteractive();

	rightbtn.flipX = true;

	rightbtn.on('pointerdown', function() {
		this.scene.players[n].setData('direccionHorizontal', 1);
	});

	leftbtn.on('pointerdown', function() {
		this.scene.players[n].setData('direccionHorizontal', -1);
	});

	rightbtn.on('pointerup', function() {
		this.scene.players[n].setData('direccionHorizontal',  0);
	});

	leftbtn.on('pointerup', function() {
		this.scene.players[n].setData('direccionHorizontal', 0);
	});
}

update() {
	for (let i = 0; i < this.n_jugadores; i++) {
    	this.players[i].thrust(-0.003);
		if (this.cursors.left.isDown || this.players[i].getData('direccionHorizontal') === -1)
			this.players[i].setAngularVelocity(-0.065);
		else if (this.cursors.right.isDown || this.players[i].getData('direccionHorizontal') === 1)
			this.players[i].setAngularVelocity(0.065);
	}
}
Ver ejemplo

Physics Editor

En el método preload()this.load.image('ladoizquierdo', '/apuntes/assets/phaser/coches/fuentesTexturePacker/ladoizquierdo.png');
this.load.image('ladoderecho', '/apuntes/assets/phaser/coches/fuentesTexturePacker/ladoderecho.png');
this.load.image('centro', '/apuntes/assets/phaser/coches/fuentesTexturePacker/centro.png');
this.load.image('luna', '/apuntes/assets/phaser/coches/fuentesTexturePacker/luna.png');
this.load.image('saturno', '/apuntes/assets/phaser/coches/fuentesTexturePacker/saturno.png');
this.load.image('astronauta', '/apuntes/assets/phaser/coches/fuentesTexturePacker/astronauta.png');
this.load.json('muros', '/apuntes/assets/phaser/coches/muros.json');
En el método create()const ladoizquierda = this.matter.add.sprite(0, 0, 'ladoizquierdo', null, {shape: muros.ladoizquierdo});
const ladoderecha = this.matter.add.sprite(0, 0, 'ladoderecho', null, {shape: muros.ladoderecho});
const centro = this.matter.add.sprite(0, 0, 'centro', null, {shape: muros.centro});
const luna = this.matter.add.sprite(0, 0, 'luna', null, {shape: muros.luna});
const astronauta = this.matter.add.sprite(0, 0, 'astronauta', null, {shape: muros.astronauta});
//  const estrella = this.matter.add.sprite(0, 0, 'estrella', null, {shape: muros.estrella});
const saturno = this.matter.add.sprite(0, 0, 'saturno', null, {shape: muros.saturno});

luna.setBounce(1.5);

const coordenadasLadoIzquierda = this.getCoordenadasSinTenerEnCuentaLaMasa(ladoizquierda, 0, 0);
const coordenadasLadoDerecha = this.getCoordenadasSinTenerEnCuentaLaMasa(ladoderecha, 479, 0);
const coordenadasCentro = this.getCoordenadasSinTenerEnCuentaLaMasa(centro, 210, 155);
const coordenadasLuna = this.getCoordenadasSinTenerEnCuentaLaMasa(luna, 100, 100);
const coordenadasAstronauta = this.getCoordenadasSinTenerEnCuentaLaMasa(astronauta, 685, 210);
const coordenadasSaturno = this.getCoordenadasSinTenerEnCuentaLaMasa(saturno, 665, 110);

ladoizquierda.setPosition(coordenadasLadoIzquierda.x, coordenadasLadoIzquierda.y);
ladoderecha.setPosition(coordenadasLadoDerecha.x, coordenadasLadoDerecha.y);
centro.setPosition(coordenadasCentro.x, coordenadasCentro.y);
luna.setPosition(coordenadasLuna.x, coordenadasLuna.y);
astronauta.setPosition(coordenadasAstronauta.x, coordenadasAstronauta.y);
saturno.setPosition(coordenadasSaturno.x, coordenadasSaturno.y);
Ver ejemplo

La luna rebota

luna.setBounce(1.5);
Ver ejemplo

Detectar vueltas

Método create()const objeto = this.matter.add.rectangle(175, 500, 135, 30, {
	isSensor: true,
	marker: 1,
	angle: -0.25 * Math.PI
	});

	//collisionstart detecta la colisión entre cualquier forma y cualquier forma
	this.matter.world.on('collisionstart', function (event) {
	//  La variable pairs almacena todos los objetos que están colisionando
	var pairs = event.pairs;

	for (var i = 0; i < pairs.length; i++) {
		var bodyA = pairs[i].bodyA;
		var bodyB = pairs[i].bodyB;

		//  Estamos detectando la colisión entre objetos de cualquier tipo. Por tanto, también estamos detectando la colisión entre jugadores y objetos de diversa índole que no interesan. El siguiente condicional filtra para sólo tener en cuenta las colisiones donde uno de los elementos colisionados tiene la propiedad isSensor con valor True
		if (pairs[i].isSensor) {
			alert('colision');
		}
	}
});
Ver ejemplo

Poner marcador

pintarMarcador() {
	this.add.text(400, 275, 'P1:', {fontFamily: 'font1', fontSize: 24, color: '#00ff00'});
	this.add.text(480, 275, 'P2:', {fontFamily: 'font1', fontSize: 24, color: '#00ff00'});
	this.add.text(400, 315, 'P3:', {fontFamily: 'font1', fontSize: 24, color: '#00ff00'});
	this.add.text(480, 315, 'P4:', {fontFamily: 'font1', fontSize: 24, color: '#00ff00'});

	this.marcadorCoche0 = this.add.text(440, 275, '0', {fontFamily: 'font1', fontSize: 24, color: '#ffffff'});
	this.marcadorCoche1 = this.add.text(520, 275, '0', {fontFamily: 'font1', fontSize: 24, color: '#ffffff'});
	this.marcadorCoche2 = this.add.text(440, 315, '0', {fontFamily: 'font1', fontSize: 24, color: '#ffffff'});
	this.marcadorCoche3 = this.add.text(520, 315, '0', {fontFamily: 'font1', fontSize: 24, color: '#ffffff'});
}
Ver ejemplo

Aumentar marcador

for (var i = 0; i < pairs.length; i++) {
	// En las dos siguientes líneas estamos haciendo una deconstrucción de una propiedad en Javascript. Este en lugar de este código podíamos haber hecho también: const bodyA = pairs[i].bodyA
	const {bodyA} = pairs[i];
	const {bodyB} = pairs[i];

	if (pairs[i].isSensor) {
		// Cuando se produce la colisión que nos interesa detectar, uno de los elementos que colisionan será el area que colocamos en el paso anterior y el otro será el jugador. El jugador no tiene la propiedad isSensor a true, por tanto, utilizaremos esa propiedad para evaluar quien es quien. De esta forma, la variable zone almacenará  el area, mientras que la variable playerBody almacenará el jugador.

		let zone;
		let playerBody;
		if (bodyA.isSensor) {
			zone = bodyA;
			playerBody = bodyB;
		} else if (bodyB.isSensor) {
			zone = bodyB;
			playerBody = bodyA;
		}
		const playerSprite = playerBody.gameObject;
		const playerKey = playerSprite.texture.key;
		const playerIndex = playerKey.substr(-1);
		playerSprite.data.values.marcador++;
		this[`marcadorCoche${playerIndex}`].text = `${playerSprite.getData('marcador')}`;
	}
}
Ver ejemplo

Evitar trampas

this.matter.add.rectangle(450, 150, 30, 60, {isSensor: true, marker: 2});
this.matter.add.rectangle(760, 400, 90, 30, {isSensor: true, marker: 3});
this.matter.add.rectangle(500, 505, 30, 70, {isSensor: true, marker: 4});

...

if (playerSprite.getData('marker') === zone.marker) {
	if (zone.marker === 4) {
		playerSprite.setData('marker', 1);
	} else {
	if (zone.marker === 1) {
		playerSprite.data.values.marcador++;
		this[`marcadorCoche${playerIndex}`].text = `${playerSprite.getData('marcador')}`;
	}
	playerSprite.data.values.marker++;
	}
}
Ver ejemplo

Alguien gana el juego

if (playerSprite.getData('marcador') === 4) {
	alert("Ganó " + playerKey)
}else{
	this[`marcadorCoche${playerIndex}`].text = `P${playerIndex}: ${playerSprite.data.values.marcador}`;
}
Ver ejemplo

Con portada

class EscenaPortada extends Phaser.Scene {
	constructor() {
		super({key: 'EscenaPortada'});
	}
	preload() {
		resize();
		window.addEventListener('resize', resize);
		this.load.image('portada', '/apuntes/assets/phaser/coches/portada/portada.jpg');
		this.load.image('2jugadores', '/apuntes/assets/phaser/coches/portada/2.png');
		this.load.image('3jugadores', '/apuntes/assets/phaser/coches/portada/3.png');
		this.load.image('4jugadores', '/apuntes/assets/phaser/coches/portada/4.png');
	}
	create() {
		this.add.sprite(480, 320, 'portada');
		const opcion2Jugadores = this.add.sprite(280, 320, '2jugadores').setInteractive();
		const opcion3Jugadores = this.add.sprite(480, 320, '3jugadores').setInteractive();
		const opcion4Jugadores = this.add.sprite(680, 320, '4jugadores').setInteractive();
		opcion2Jugadores.on('pointerdown', () => this.opcionJugador(2));
		opcion3Jugadores.on('pointerdown', () => this.opcionJugador(3));
		opcion4Jugadores.on('pointerdown', () => this.opcionJugador(4));
	}
	opcionJugador(nJugadores) {
		this.scene.start('EscenaJuego', {nJugadores});
	}
}
class Escena extends Phaser.Scene {
	constructor() {
		super({key: 'EscenaJuego'});
	}
	init(data){
		this.n_jugadores = data.nJugadores;
	}
Ver ejemplo

Cargar música

this.load.audio('musica', '/curso/assets/phaser/coches/Loyalty_Freak_Music_-_13_-_Ghost_Surf_Rock.mp3');
const musica = this.sound.add('musica');
musica.play({
  volume: 1
})
Ver ejemplo

Con portada

Ver ejemplo

Escena final

Ver ejemplo

Cambio controles con triángulo

Ver ejemplo

Plataformas

Juego plataformas con phaser.js Descargar recursos

Carga de estructura y fondo

Ver ejemplo

Cargar prota con el JSON de Tiled

class Escena extends Phaser.Scene {
  preload() {
    resize()
    this.load.image('jungla', '/curso/assets/phaser/plataformas/background.jpg');
    this.load.spritesheet('dude', '/curso/assets/phaser/plataformas/dude.png', {frameWidth: 128, frameHeight: 128});
    this.load.tilemapTiledJSON('level1', '/curso/assets/phaser/plataformas/map.json');
  }
  create() {
    this.add.sprite(480, 320, 'jungla');
    const map = this.add.tilemap('level1');
    const playersFromTiled = this.findObjectsByType('player', map);
    this.player = this.physics.add.sprite(playersFromTiled[0].x, playersFromTiled[0].y, 'dude');
  }
  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;
  }
}
Ver ejemplo

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, 'dude');
        scene.physics.systems.displayList.add(this);
        scene.physics.systems.updateList.add(this);
        scene.physics.world.enableBody(this, 0);
    }
}
Ver ejemplo

Cargar la backgroundLayer

preload() {
    resize()
    this.load.image('gameTiles', '/curso/assets/phaser/plataformas/tiles.png');
    this.load.tilemapTiledJSON('level1', '/curso/assets/phaser/plataformas/map.json');
     ...
Ver ejemplo

Cargar la collisionLayer

this.collisionLayer = map.createStaticLayer('collisionLayer', tileset);
this.collisionLayer.setCollisionByExclusion([-1]);
Ver ejemplo

Cargar hierbaLayer

map.createStaticLayer('hierbaLayer', tileset).setDepth(100);
Ver ejemplo

Ajustando la boundingbox del player

this.setSize(90, 180, true);
Ver ejemplo

El jugador se mueve

Dentro del método createthis.cursors = this.input.keyboard.createCursorKeys();
Dentro de la clase PlayercaminarALaIzquierda(){
        this.body.setVelocityX(-250)
    }

    caminarALaDerecha(){
        this.body.setVelocityX(250)
    }

    reposo(){
        this.body.setVelocityX(0)
    }
Ver ejemplo

Animación del jugador

Definimos la función que cargará las animaciones y que será llamada dentro del método create de la Escena:

animacionesDeLaEscena() {
    this.anims.create({
        key: 'protaLeft',
        frames: this.anims.generateFrameNumbers('dude', {start: 2, end: 5}),
        frameRate: 10,
        repeat: -1,
    });
    this.anims.create({
        key: 'reposo',
        frames: this.anims.generateFrameNumbers('dude', {start: 0, end: 1}),
        frameRate: 4,
        repeat: -1,
    });
}
caminarALaIzquierda(){
	this.body.setVelocityX(-250);
	this.flipX = true;
	this.play('protaLeft', true);
}

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

reposo() {
	this.body.setVelocityX(0);
	if (this.enElSuelo)this.play('reposo', true);
}
Ver ejemplo

Saltar

Añadimos el método saltar a la clase Player. El jugador sólo debe poder saltar si está tocando el suelo:

saltar(){
	if(this.enElSuelo){
		this.body.setVelocityY(-250);
	}
}

En el método update del player alimentamos la variable enElSuelo:

update(){
	this.enElSuelo = this.body.onFloor();
}

El método update del player no se llama automáticamente, debemos llamarlo desde el método update de la escena:

update() {
	...
	this.player.update();
}
Ver ejemplo

Caer

Completamos la función de saltar:saltar(){
    if(this.enElSuelo){
        this.body.setVelocityY(-250);
        this.play('caer', true);
    }
}
Completamos la función animacionesDeLaEscenacaminarALaIzquierda(){
    this.body.setVelocityX(-250);
    this.flipX = true;
    if(this.enElSuelo)this.play('protaLeft', true);
}
caminarALaDerecha(){
    this.body.setVelocityX(250);
    this.flipX = false;
    if(this.enElSuelo)this.play('protaLeft', true);
}
reposo() {
    this.body.setVelocityX(0);
    if (this.enElSuelo)this.play('reposo', true);
}
Ver ejemplo

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

Habrá que actualizar el código del método update

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();
}
Ver ejemplo

La cámara sigue al prota

Dentro del método create:this.cameras.main.setSize(960, 640);
this.cameras.main.startFollow(this.player);
Ver ejemplo

La cámara sigue al prota horizontalmente

Dentro del método createthis.cameras.main.setSize(960, 640);
Dentro del método update de la Escenathis.cameras.main.scrollX = this.player.x - 400;
this.cameras.main.scrollY = 0;
Ver ejemplo

El fondo se repite

Sustituímos la línea que carga el fondothis.bg = this.add.tileSprite(480, 320, 960, 640, 'jungla').setScrollFactor(0);

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

this.add.sprite(480, 320, 'jungla');
this.bg.tilePositionX = this.player.x;
Ver ejemplo

Añadir un enemigo

En el método update:const hormigasFromTiled = this.findObjectsByType('hormigaEnemy', map);

for (let i = 0; i < hormigasFromTiled.length; i++) {
  const malo = this.physics.add.sprite(hormigasFromTiled[i].x, hormigasFromTiled[i].y, 'hormiga');
  this.physics.add.collider(malo, this.collisionLayer);
}
Ver ejemplo

Enemigo con clase

Dentro del método create:const hormigasFromTiled = this.physics.add.group({classType: 'hormiga', runChildUpdate: true});
for (let i = 0; i < hormigasFromTiled.length; i++) {
	const malo = new HormigaEnemy(hormigasFromTiled[i].x, hormigasFromTiled[i].y, this);
	enemies.add(malo);
}
class OrugaEnemy extends Phaser.Physics.Arcade.Sprite {
  constructor(x, y, scene) {
    super(scene, x, y, 'oruga', 3);
    scene.physics.add.collider(this, scene.collisionLayer);
    scene.add.existing(this);
  }
}
Ver ejemplo

Enemigo con animación

La animación que utilizaremosscene.anims.create({
  key: 'hormigaLeft',
  frames: scene.anims.generateFrameNumbers('oruga', {start: 0, end: 3}),
  frameRate: 10,
  repeat: -1,
});
En el constructor del enemigothis.play('hormigaLeft');
Ver ejemplo

Enemigo se mueve

constructor(x, y, scene) {
    ...
    this.velocidad = 100;
    this.direccion = 1;
}

update() {
  this.body.setVelocityX(this.direccion * this.velocidad);
}
Ver ejemplo

Enemigo inteligente

class HormigaEnemy extends Phaser.Physics.Arcade.Sprite {

    constructor(x, y, scene) {
        ...

        this.velocidad = 100;
        this.direccion = 1;
    }

    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;
        }
    }
}
Ver ejemplo

Enemigo muere

Método en el constructor de la escena:this.physics.add.overlap(this.player, hormigas, this.player.checkEnemy, null, this.player);
Método de la clase Player:checkEnemy(player, enemigo) {
	//  El jugador está cayendo?
	if (this.body.velocity.y > 0) {
		enemigo.morir();
	} else {
		this.die();
	}
}
Método del enemigomorir() {
	this.disableBody(true, true);
}
Ver ejemplo

Enemigo explota

this.anims.create({
	key: 'explosionAnim',
	frames: this.anims.generateFrameNumbers('explosion', {start: 0, end: 4}),
	frameRate: 7
});
morir() {
		this.disableBody();
		this.play('explosionAnim');
	}

	animationComplete(animation, frame, sprite) {
		if (animation.key === 'explosionAnim') {
		this.disableBody(true, true);
	}
}
Ver ejemplo

Prota explota

Utilizaremos la variable estaVivo para detener todas las demás animaciones cuando el Player es colisionado.

morir() {
		this.estaVivo = false;
		this.disableBody();
		this.play('explosionAnim', true);
	}

	animationComplete(animation, frame, sprite) {
	if (animation.key === 'explosionAnim') {
		this.disableBody(true, true);
	}
}
update() {
	this.bg.tilePositionX = this.player.x;

	this.cameras.main.scrollX = this.player.x - 400;
	this.cameras.main.scrollY = 0;

	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();
	}
}
Ver ejemplo

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:

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;
}
Ver ejemplo

Para llamar a este método, usaremos:

this.insertarMalos(malos, HormigaEnemy, this);

Enemigo con Herencia

class Enemy extends Phaser.Physics.Arcade.Sprite{
    constructor(scene, x, y, sprite ) {
        super(scene, x, y, sprite);
        scene.physics.add.collider(this, scene.collisionLayer);
        scene.add.existing(this);
        this.velocidad = 100;
        this.direccion = 1;
    }
    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;
        }
    }
    morir() {
        this.disableBody();
        this.play('explosionAnim');
    }
    animationComplete(animation, frame, sprite) {
        if (animation.key === 'explosionAnim') {
            this.disableBody(true, true);
        }
    }
}
Ver ejemplo

Insertar Oruga

class OrugaEnemy extends Enemy {

    constructor(x, y, scene) {
        super(scene, x, y, 'oruga');
        scene.add.existing(this);
        this.play('orugaLeft');
    }
}
Ver ejemplo

Insertar Avispa dando vueltas

class AvispaEnemy extends Phaser.Physics.Arcade.Sprite {
	constructor(x, y, scene) {
		super(scene, x, y, 'avispa');
		scene.physics.add.collider(this, this.scene.collisionLayer);
		scene.add.existing(this);
		scene.anims.create({
			key: 'avispaLeft',
			frames: scene.anims.generateFrameNumbers('avispa', {start: 0, end: 2}),
			frameRate: 10,
			repeat: -1,
		});
		this.play('avispaLeft', 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);
	}
}
Ver ejemplo

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);
}
Ver ejemplo

Avispa detecta a Player

Dentro del método 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')
}
Ver ejemplo

Avispa persigue a Player

Actualizamos el método 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;
}
update(time, delta) {
	if (this.state === AvispaEnemy.VOLANDO) {
		this.checkPlayer();
	} else if (this.state === AvispaEnemy.PERSIGUIENDO) {
		this.persiguePlayer(delta);
	}
}
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;
Ver ejemplo

Avispa ataca a Player

this.anims.create({
  key: 'avispaAttack',
  frames: scene.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

if (this.scene.physics.overlap(this, player) && this.state === AvispaEnemy.PERSIGUIENDO) {
	this.state = AvispaEnemy.ATACANDO;
	this.play('avispaAttack', true);
}

Ahora mismo, debajo de la clase Avispa tendremos tres constantes:

AvispaEnemy.VOLANDO = 0;
AvispaEnemy.PERSIGUIENDO = 1;
AvispaEnemy.ATACANDO = 2;
Ver ejemplo

Avispa vuelve a casa

this.startPlace = new Phaser.Math.Vector2(this.pathVector.x, this.pathVector.y);
this.on('animationcomplete', this.attackComplete, this);
attackComplete(animation) {
  if (this.state === AvispaEnemy.ATACANDO && animation.key === 'avispaAttack') {
    this.returnHome();
  }
}
returnHome() {
	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;
	this.play('avispaLeft', true);
}
Ver ejemplo

Avispa continua volando

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();
  }
}
continuaVolando () {
  this.state = AvispaEnemy.VOLANDO;
  this.path = this.flyPath;
  this.pathIndex = 0;
}
AvispaEnemy.VOLANDO = 0;
AvispaEnemy.PERSIGUIENDO = 1;
AvispaEnemy.ATACANDO = 2;
AvispaEnemy.VOLVIENDO = 3;
Ver ejemplo

La avispa siempre mira al protagonista

if(this.x < this.scene.player.x){
	this.flipX = true;
}else{
	this.flipX = false;
}
Ver ejemplo

El heroe recibe daño

Dentro de la función persiguePlayerif (this.scene.physics.overlap(this, player) && this.state === AvispaEnemy.PERSIGUIENDO){
	this.state = AvispaEnemy.ATACANDO;
	this.play('avispaAttack', true);
	this.scene.playerGolpeadoPorAvispa();
}
playerGolpeadoPorAvispa(){
    alert('golpeado')
}
Ver ejemplo

El prota sale despedido por los aires

  playerGolpeado(){
	this.player.estaAturdido = true;
	this.player.body.setVelocity(-150,-150);
}
update() {
	if(!this.player.estaSiendoGolpeado){
		//aquí dentro vendría todo el código que ya teníamos de la función update
	}
}
Ver ejemplo

Reactivar los controles del jugador

playerGolpeadoPorAvispa() {
	this.estaAturdido = true;
	this.body.setVelocity(-150, -150);
	this.scene.time.addEvent({delay: 1000, callback: this.terminoElAturdimiento, callbackScope: this});
}
terminoElAturdimiento() {
	this.estaAturdido = false;
}
Ver ejemplo

Añadir meta

En el preload()this.load.image('meta', '/curso/assets/phaser/plataformas/meta.png');
En el create()this.meta = this.physics.add.sprite(metaFromTiled.x, metaFromTiled.y, 'meta');
this.meta.body.immovable = true;
this.meta.body.moves = false;
Ver ejemplo

Fin del juego

this.physics.add.overlap(this.player, this.meta, this.playerAlcanzaMeta, null, this);
playerAlcanzaMeta(){
    this.scene.start('finScene');
}
Ver ejemplo

Limitando la cámara

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

Escena de perder tras explotar

class PerderEscena extends Phaser.Scene {
    constructor() {
        super({key: 'perderScene'});
    }
    preload() {
        this.load.image('fin', '/curso/assets/phaser/fin-de-juego.jpg');
    }
    create() {
        this.add.image(480, 320, 'fin');
    }
}
scene: [Escena, FinEscena, PerderEscena],
animationComplete(animation, frame, sprite) {
    if (animation.key === 'explosionAnim') {
        this.disableBody(true, true);
        this.scene.scene.start('perderScene');
    }
}
Ver ejemplo

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;
Ver ejemplo
icono de mandar un mail¡Contacta conmigo!
contacta conmigoPablo Monteserín

¡Hola! ¿En qué puedo ayudarte?