Introducción a PhaserJS

¿Quién soy?

Algo a lo que he jugado

¿Y ahora?

  • Me divierto con la WiiU
  • Juego a algún indie o juegos con menor presupuesto
  • Revivo algunos clásicos
  • Empiezo a crear mis juegos

"El único límite es tu imaginación"

Índice

  • ¿Qué es PhaserJS?
  • ¿Por dónde empiezo?
  • Conceptos básicos
  • Crear un juego
    • sprites
    • físicas
    • tilemaps
    • sonido

¿Qué es PhaserJS?

Phaser es un framework de código libre, rápido, gratuito y divertido para hacer juegos HTML5 para escritorio y navegadores móviles. Usa Pixi.js internamente para una aceleración 2D y renderizado de WebGL. 

Y Phaser no es...

Una herramienta para generar arte

Es posible jugar de diferentes formas

PhaserJS

PhaserJS

¿Por donde empiezo?

Vamos a usar el generador de Yeoman para Phaser. Así que es necesario instalar algunas cosas antes

  1. GIT
  2. NodeJS
  3. Bower
    •  
  4. Yeoman
    •   
npm install -g yo
npm install -g bower

Instalamos el generador

  • npm install -g yo generator-phaser
  • npm install -g gulp
npm install -g yo generator-phaser
npm install -g gulp

Con el generador instalado

Primero creamos una carpeta donde crearemos nuestro primer juego

Introducimos en un terminal

 

 

Nos pedirá el nombre de nuestro juego

Si algo falla durante la instalación de dependencias

 

yo phaser
npm install && bower install

Con el esqueleto generado

Ejecutamos en un terminal

gulp

 

Se arrancará el juego  y podemos verlo en nuestro navegador en la dirección http://localhost:9000

gulp

Canvas

  • El canvas es un tag de HTML

  • Lienzo donde se pinta lo que ocurre en el juego

  • Soporte

    • Chrome 4.0

    • Explorer 9

    • Firefox 2.0

    • Opera 9

    • Safari 3.1

<canvas id="myCanvas"></canvas>

¿Cómo funciona Phaser?

  • Preload

    • Carga los recursos

  • Create

    • Se crean los elementos del juego: el jugador, fondos, música, enemigos...

  • Update

    • Se ejecuta constantemente

  • Render

    • Sirve para debuggear

Revisemos el esqueleto del juego que acabamos de generar

index.html

<head>
  <meta charset="utf-8">
  <title>intro/title>

  <link rel="stylesheet" href="css/main.css"> 
</head>
<body>
  <div id="intro-game" class="game"></div>

  <script src="http://localhost:35729/livereload.js"></script>  

  <script src="bower_components/phaser-official/build/phaser.min.js"></script>

  <script src="js/boot.js"></script>
  <script src="js/preloader.js"></script>
  <script src="js/menu.js"></script>
  <script src="js/game.js"></script>
  <script src="js/main.js"></script>
</body>

main.css

* {
  -webkit-user-select: none;
  user-select: none;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
}

.game {
  width: 640px;
  height: 480px;
  margin: 20px auto;
}

main.js

  • Crea el lienzo y agrega estados/pantallas al juego
var game,
    ns = window['intro'];
game = new Phaser.Game(640, 480, Phaser.AUTO, 'intro-game');
game.state.add('boot', ns.Boot);
game.state.add('preloader', ns.Preloader);
game.state.add('menu', ns.Menu);
game.state.add('game', ns.Game);

game.state.start('boot');

Inicia un estado

que se ha agregado previamente

main.js

game = new Phaser.Game(
    640, /* ancho */
    480, /* alto */
    Phaser.AUTO, /* renderizador: Phaser.WEBGL, Phaser.CANVAS o Phaser.HEADLESS */
    'intro-game' /* elemento del DOM donde se inyecta el canvas */
);

En el objeto Phaser.Game ocurre la magia. Es el corazón del juego. Nos proporciona acceso a muchas funciones de Phaser y es capaz de iniciar el cambio entre pantallas

"El modo AUTO me ha dado problemas. Recomiendo poner CANVAS para los ejercicios de la presentación"

boot.js

  function Boot() {}

  Boot.prototype = {
    
    preload: function () {
      this.load.image('preloader', 'assets/preloader.gif');
    },

    create: function () {
      this.game.input.maxPointers = 1;

      if (this.game.device.desktop) {
        this.game.scale.pageAlignHorizontally = true;
      } else {
        this.game.scaleMode = Phaser.ScaleManager.SHOW_ALL;
        this.game.scale.minWidth =  480;
        this.game.scale.minHeight = 260;
        this.game.scale.maxWidth = 640;
        this.game.scale.maxHeight = 480;
        this.game.scale.forceLandscape = true;
        this.game.scale.pageAlignHorizontally = true;
        this.game.scale.setScreenSize(true);
      }
      this.game.state.start('preloader');
    }
  };

  window['intro'] = window['intro'] || {};
  window['intro'].Boot = Boot;

carga un .gif

lanza un estado que ha sido agregado en el main.js

registra una pantalla para este JS que será agregada al juego en el main.js

establece el número máximo de punteros y las dimensiones del juego

preloader.js

  function Preloader() {
    this.asset = null;
    this.ready = false;
  }
  Preloader.prototype = {

    preload: function () {
      this.asset = this.add.sprite(320, 240, 'preloader');
      this.asset.anchor.setTo(0.5, 0.5);

      this.load.onLoadComplete.addOnce(this.onLoadComplete, this);
      this.load.setPreloadSprite(this.asset);
      this.load.image('player', 'assets/player.png');
      this.load.bitmapFont('minecraftia', 'assets/minecraftia.png', 'assets/minecraftia.xml');
    },

    create: function () {
      this.asset.cropEnabled = false;
    },

    update: function () {
      if (!!this.ready) {
        this.game.state.start('menu');
      }
    },

    onLoadComplete: function () {
      this.ready = true;
    }
  };

  window['intro'] = window['intro'] || {};
  window['intro'].Preloader = Preloader;

carga recursos

cuando se han cargado todos los recursos se lanza el 'menu'

registra una pantalla para este JS que será agregada al juego en el main.js

asigna un recurso para que sea una barra de carga

menu.js

  function Menu() {
    this.titleTxt = null;
    this.startTxt = null;
  }

  Menu.prototype = {
    create: function () {
      var x = this.game.width / 2
        , y = this.game.height / 2;

      this.titleTxt = this.add.bitmapText(x, y, 'minecraftia', 'Example Game' );
      this.titleTxt.align = 'center';
      this.titleTxt.x = this.game.width / 2 - this.titleTxt.textWidth / 2;

      y = y + this.titleTxt.height + 5;
      this.startTxt = this.add.bitmapText(x, y, 'minecraftia', 'START');
      this.startTxt.align = 'center';
      this.startTxt.x = this.game.width / 2 - this.startTxt.textWidth / 2;

      this.input.onDown.add(this.onDown, this);
    },

    update: function () {},

    onDown: function () {
      this.game.state.start('game');
    }
  };

  window['intro'] = window['intro'] || {};
  window['intro'].Menu = Menu;

guarda en x e y el centro del juego

asigna un evento a una entrada. Si se hace click o se toca la pantalla ejecuta la función onDown

registra una pantalla para este JS que será agregada al juego en el main.js

Crea un texto

Crea un texto

esta función lanza el 'game'

game.js

  function Game() {
    this.player = null;
  }
  Game.prototype = {
    create: function () {
      var x = this.game.width / 2
        , y = this.game.height / 2;

      this.player = this.add.sprite(x, y, 'player');
      this.player.anchor.setTo(0.5, 0.5);


      this.input.onDown.add(this.onInputDown, this);
    },

    update: function () {
      /* ... */
    },

    onInputDown: function () {
      this.game.state.start('menu');
    }

  };

  window['intro'] = window['intro'] || {};
  window['intro'].Game = Game;

crea un jugador con una imagen cargada en el preloader.js

el contenido de update se ejecuta constantemente. detecta la posición del ratón y realiza las operaciones necesarias para que la imagen del jugador haga zoom y rote

registra una pantalla para este JS que será agregada al juego en el main.js

asigna un evento a una entrada. Si se hace click o se toca la pantalla ejecuta la función onDown

Resumen

  • main.js
    • Crear el juego
    • Agrega estados
  • boot.js
    • Configura dimensiones
  • preloader.js
    • Carga recursos
  • menu.js
  • game.js

¿Qué recursos puedo cargar en Phaser?

  • Imágenes
    • Sprites
  • Audio
  • Mapeados

Personaje

  • Movimiento
  • Animaciones
    • standby
    • movimiento
    • dispare
    • ...
  • Características
    • Salud
    • Masa
    • Munición
    • ...

Escenario

  • Fondo
  • Mapeado (tilemap)

Físicas

  • Arcade
  • Ninja
  • P2

 

 

 

 

 

Los diferentes sistemas pueden combinarse entre sí

Físicas Arcade

  • La más básica
    • Formas rectangulares AABB
  • Colisiones
  • Superposición
  • Métodos de movimiento
    • Aceleración hacia objeto
    • objetoA persiga a objetoB
    • ...

 

this.game.physics.startSystem(Phaser.Physics.ARCADE);

Físicas Ninja

  • Sistema creado para Flash por Metanet Software y portado a javascript por Richard Davey
  • Formas rectangulares y circulares
  • Colisiones
  • Superposiones
  • Mapeados cóncavos y convexos

 

this.game.physics.startSystem(Phaser.Physics.NINJA);

Físicas P2

  • Formas complejas
    • Rectángulos
    • Círculos
    • Polígonos
  • Materiales
    • Por ejemplo: Hielo
  • Colisiones mediante listeners
  • Mapeados complejos

 

this.game.physics.startSystem(Phaser.Physics.P2);

Velocidad de decodificado

Peso

node-webkit

ogg

wav

mp3

Rápida

Ligero

Rápida

Pesado

No

Muy lenta

Ligero

No

"El sonido y la música son el 50% del entretenimiento en una película": George Lucas

Sonido

¿Qué queremos hacer?

  • Tipo
    • Plataformas
    • Beat 'em up
    • Shoot 'Em Up
    • RPG
    • Mesa
    • ...
  • Historia
    • Ambientación
    • Enemigos
    • ...

 

Juego de plataformas

Crear JS

(function () {
    'use strict';
    function Demo() {}

    Demo.prototype = {

        create: function () {
            var x = this.game.width / 2,
                y = this.game.height / 2,
                textStyle = {
                    'align': 'center',
                    'fill': '#FABADA'
                }

            var miTexto = this.game.add.text(x, y, 'Hola juego', textStyle)
            miTexto.anchor.setTo(0.5, 0.5);
        },

        update: function () {}
    };

    window['intro'] = window['intro'] || {};
    window['intro'].Demo= Demo;
}());

ruta: src/js/ejemplos/demo.js

Enlazamos en el index.html

<script src="js/boot.js"></script>
<script src="js/preloader.js"></script>
<script src="js/menu.js"></script>
<script src="js/game.js"></script>

<script src="js/ejemplos/demo.js"></script>

<script src="js/main.js"></script>

Creamos estado en main.js

game.state.add('boot', ns.Boot);
game.state.add('preloader', ns.Preloader);
game.state.add('menu', ns.Menu);
game.state.add('game', ns.Game);

game.state.add('demo', ns.Demo);
  
game.state.start('boot');

Para ver nuestro nuevo "juego"

Modificamos el preloader.js para que lance a ejemplo1

Ejecutamos por consola a gulp si no está funcionando

update: function () {
  if (!!this.ready) {
    this.game.state.start('demo');
  }
},
gulp

Ya tenemos el js donde crearemos el juego

Podemos verlo en nuestro navegador en la dirección http://localhost:9000

Crear un escenario

Necesitamos una imagen de fondo

src/assets/scenario/sky.jpg

Crear un escenario

Necesitamos un sprite con todas tiles (baldosas)

src/assets/scenario/tiles.png

16px + 1px

16px

Crear un escenario

Necesitamos un mapa para el nivel del juego

  • Instalamos Tiled
    • Usaremos nuestro sprite de tiles
    • Podemos crear capas
    • Generaremos un .json con el mapeado del juego

Tiled

Primero cargaremos en Tiled el sprite de tiles

Mapa > Nuevo conjunto de patrones...

Tiled

Crearemos una capa llamada tiles que contendrá nuestro mapa

Tiled

Cuando tengamos el mapa listo lo guardamos como tilemap1.json en la carpeta src/assets/scenario

Cargamos en el preloader.js los recursos para el escenario

this.load.image('sky', 'assets/scenario/sky.jpg');
this.load.spritesheet('tiles', 'assets/scenario/tiles.png', 16, 16, 1, 0);
this.load.tilemap('tilemap1', 'assets/scenario/tilemap1.json', null, Phaser.Tilemap.TILED_JSON);

En demo.js hacemos uso del escenario

create: function () {

this.game.physics.startSystem(Phaser.Physics.ARCADE);
this.game.physics.arcade.gravity.y = 485;



this.bg = this.game.add.tileSprite(0, 0, 640, 480, 'sky');
this.bg.fixedToCamera = true;



// map
this.map = this.game.add.tilemap('tilemap1');
this.map.addTilesetImage('tiles');
this.map.setCollisionBetween(0, this.map.tiles.length);
this.layer = this.map.createLayer('Tiles');
this.layer.resizeWorld();

Crear un personaje

Necesitamos un sprite con todas las animaciones

src/assets/character/marco-rossi.png

55px

50px

Cargamos el sprite del personaje en el preloader.js

this.load.sprite('marco', 'assets/character/marco-rossi.png', 55, 50);
//this.load.sprite(clave, ruta, ancho, alto);

Usamos el sprite en demo.js

create: function () {
    //map
    /* ... */

    // player
    function setupPlayer(player) {
        player.animations.add('standby', [0, 1, 2, 1], 6, true);
        player.animations.add('move', [3, 4, 5, 6, 7, 8, 9, 10, 11], 18, true);
        player.animations.add('fire', [12, 13, 14, 15], 12, true);
        player.body.collideWorldBounds = true;
        player.body.setSize(18, 35, -6, 6);
        player.anchor.setTo(0.3, 0.5);
        player.fireTimer = 0;
        player.health = 1;
    }


    this.player = this.game.add.sprite(100, 20, 'marco');
    this.game.physics.enable(this.player, Phaser.Physics.ARCADE);
    setupPlayer(this.player);
    this.game.camera.follow(this.player);
},

tamaño colisión, ancla, escalado, vida

animaciones

creación del personaje,

físicas,

le decimos a la camara que le siga

Hacemos que el personaje colisione con el mapa

update: function () {
    this.game.physics.arcade.collide(this.player, this.layer);
},
render: function () {
    this.game.debug.body(this.player);
    this.game.debug.spriteInfo(this.player, 150, 30);
}

Phaser proporciona el método render para ayudarnos a debuggear

create: function () {
    // map ...
    // player ...
    // controls
    function createControls(gameContext) {
        gameContext.controls= {
            'left': gameContext.game.input.keyboard.addKey(65), //A
            'right': gameContext.game.input.keyboard.addKey(68), //D
            'down': gameContext.game.input.keyboard.addKey(83), //S
            'up': gameContext.game.input.keyboard.addKey(87), //W
            'fire': gameContext.game.input.keyboard.addKey(75), // K
            'jump': gameContext.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR)
        };
    }
    createControls(this);
},

Movimiento

update: function() {
    this.game.physics.arcade.collide(this.player, this.layer);

    this.player.body.velocity.x = 0;
    if (this.controls.left.isDown) {
        this.player.body.velocity.x = -150;
        this.player.animations.play('move');
        if (this.player.scale.x > 0) {
            this.player.scale.x = - 1;
            this.player.body.setSize(18, 35, 0, 6);
        }
    } else if (this.controls.right.isDown) {
        this.player.body.velocity.x = 150;
        this.player.animations.play('move');
        if (this.player.scale.x < 0) {
            this.player.scale.x = 1;
            this.player.body.setSize(18, 35, -6, 6);
        }
    } else {
        this.player.animations.play('standby');
    }
    if (this.controls.jump.isDown && this.player.body.onFloor()) {
        this.player.body.velocity.y = - 250;
    }
},

Crear enemigo

Necesitamos un sprite con todas las animaciones

src/assets/enemies/abul-abbas.png

41px

41px

Cargamos en el preloader.js el recurso

this.load.spritesheet('abul', 'assets/enemies/abul-abbas.png', 41, 41);

En demo.js creamos un grupo de enemigos

create: function() {
    // map, player, controls ...
    // enemies
    // abul-abbas
    function setupAbul(enemy) {
        this.game.physics.enable(enemy, Phaser.Physics.ARCADE);
        enemy.animations.add('default', [0, 1, 2, 3, 4, 5], 10, true);
        enemy.body.collideWorldBounds = true;
        enemy.body.setSize(20, 38);
        enemy.anchor.setTo(0.5, 0.5);
        enemy.health = 3;
        enemy.animations.play('default');
    }
    this.abuls = this.game.add.group();
    this.abuls.create(460, 390, 'abul');
    this.abuls.create(880, 530, 'abul');
    this.abuls.forEach(setupAbul, this);
},
update: function() {
    this.game.physics.arcade.collide(this.abuls, this.layer);
    //...

creamos un grupo de enemigos con características similares

colision con el mapa

El enemigo persigue a Marco

update: function() {
    this.game.physics.arcade.collide(this.player, this.layer);
    this.game.physics.arcade.collide(this.abuls, this.layer);
    this.abuls.forEach(function (abul) {
        if (this.game.physics.arcade.distanceBetween(abul, this.player) < 250) {
            this.game.physics.arcade.accelerateToObject(abul, this.player, 120, 120, 0)
            // accelerateToObject(objeto, destino, vel, xVelMax, yVelMax)
        } else {
            abul.body.velocity.x = 0;
        }
    }, this);

    //...

El enemigo daña a Marco

update: function() {
    //...
    this.game.physics.arcade.collide(this.abuls, this.player, this.playerIsDamaged, null, this);
    // collide(object1, object2, collideCallback, processCallback, callbackContext)
    //..
},
playerIsDamaged: function (player) {
    player.damage(1);
},

Munición para Marco

Necesitamos una imagen del proyectil

src/assets/character/bullet.png

Cargamos en el preloader.js el recurso

this.load.image('bullet', 'assets/character/bullet.png');

En demo.js usamos creamos las balas

create: function() {
    // map, player, enemies, ...     
    // bullets
    function setupBullet(bullet) {
        this.game.physics.enable(bullet, Phaser.Physics.ARCADE);
        bullet.body.collideWorldBounds = false;
        bullet.body.setSize(13, 13);
        bullet.anchor.setTo(0.5, 0.5);
        bullet.lifespan = 1000;
        bullet.outOfBoundsKill = true;
        bullet.checkWorldBounds = true;
        bullet.body.allowGravity = false;
    }
    this.bullets = this.game.add.group();
    this.bullets.createMultiple(20, 'bullet');
    this.bullets.forEach(setupBullet, this);
},

¡Marco dispara!

update: function() {
    //...    
    } else if (this.controls.fire.isDown && this.player.alive) {
        this.player.animations.play('fire');
        this.fireBullet(this);
    } else {
        this.player.animations.play('standby');
    }
    //...
},
fireBullet: function (context) {
    if (context.game.time.now > context.player.fireTimer) {
        var bullet = context.bullets.getFirstExists(false);
        if (bullet) {
            bullet.reset(context.player.x, context.player.y - 6);
            bullet.body.velocity.x = 400 * context.player.scale.x;
            context.player.fireTimer = context.game.time.now + 200;
        }
    }
},

Que el enemigo caiga

update: function() {
    //...
    this.game.physics.arcade.collide(this.abuls, this.bullets, this.enemyIsDamaged, null, this);
    this.game.physics.arcade.collide(this.layer, this.bullets, this.destroyBullet, null, this);
    //...
},
enemyIsDamaged: function (enemy, bullet) {
    enemy.damage(1);
    bullet.kill();
},
destroyBullet: function (bullet) {
    bullet.kill();
},

Audio

Pongamos música y sonido al disparar

src/assets/audio/ryu-remix.ogg

src/assets/audio/shoot.wav

this.load.audio('music', 'assets/audio/ryu-remix.ogg');
this.load.audio('shootSound', 'assets/audio/shoot.wav');

Cargamos en el preloader.js el recurso

En demo.js usamos el audio

create: function() {
    // map, player, enemies, bullets, ...
    // audio
    this.game.sound.stopAll();
    this.music = this.game.add.audio('music');
    this.shootSound = this.game.add.audio('shootSound');
    this.music.play('', 0, 1, true);
},        
update: function() {/*...*/},
fireBullet: function (context) {
    if (context.game.time.now > context.player.fireTimer) {
        var bullet = context.bullets.getFirstExists(false);
        if (bullet) {
            context.shootSound.play('', 0, 0.4, false);
            bullet.reset(context.player.x, context.player.y - 6);
            bullet.body.velocity.x = 400 * context.player.scale.x;
            context.player.fireTimer = context.game.time.now + 200;
        }
    }
},

creamos los audios

la música se empieza a reproducir

en cada disparo se reproduce un sonido

Resultado

  • Juego de plataformas
  • Físicas arcade
  • Enemigos
    • persiguen al jugador desde el suelo
  • El jugador puede
    • moverse
    • disparar
    • saltar

¿Cómo podríamos mejorar?

  • Más paisajes
  • Más armas
  • Munición limitada
  • Partículas al disparar y/o cuando un personaje es herido
  • Explosiones
  • Diferentes enemigos
  • Premios
  • Jugador con más vida
  • Multijugador
  • Interfaz con el estado del jugador
  • Elementos destructibles
  • Tienda donde canjear monedas
  • Integración con redes sociales
  • Control con móvil
  • Control con gamepad
  • ...

Juego de plataformas old-school

con pequeños toques RPG

Enlaces de interés

GAME OVER

Gracias

¡Registro a próximos MeetUps ya disponible!

Introducción a PhaserJS

By Alejandro Arroyo Duque

Introducción a PhaserJS

Charla para el meetup @videogamearmy

  • 3,165