Crear videojuegos con JavaScript y Phaser

Aprender a crear videojuegos 2D con HTML5 JavaScript y la librería Phaser. Al final de este artículo sabrás como crear un videojuego de plataformas online.

https://phaser.io/ Phaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.

En la segunda parte de ésta publicación he creado una guía práctica donde te enseño a implementar la librería. A continuación puedes ver el juego una vez completado. Haz click aquí para abrirlo en una ventana nueva (ten en cuenta que está pensado para jugar con teclado)

*Este juego está pensado para jugar con teclado

¿Quieres aprender a hacer este ejercicio? Saltar directamente al tutorial

Tarde o temprano, todo aficionado a los videojuegos acaba jugando a algún juego que le marcará de por vida. Contribuyendo a definir sus gustos y modelando su imaginario.

Por consiguiente, entre desarrolladores es común sentir interés por crear videojuegos con lenguajes como JavaScript y librerías como Phaser.

Crear historias en forma de videojuegos con JavaScript y Phaser

Por su naturaleza, los videojuegos, permiten contar historias, y explorar nuevas formas de interactividad que conectan con los usuarios de formas únicas.

Gracias a la reciente evolución que han vivido las tecnologías front end, es posible crear videojuegos con HTML5. A través de APIs como “Canvas”, “RequestAnimationFrame”, “Web Audio API” y “WebGL”, se pueden desarrollar juegos realmente asombrosos

Sin embargo, controlar de forma óptima todos estos recursos, se puede volver complejo rápidamente. Es por ese motivo, que aparecen librerías como Phaser.

Phaser actúa como framework base para la creación de videojuegos 2D. Resolviendo, de antemano, muchos de los aspectos técnicos intrínsecos en cualquier proyecto de estas características.

Inicialmente fue creado por Richard Davey, y se encuentra en la versión 3.55.2. Con más de 31.1K estrellas en Github.

Para usar Phaser, se tiene que descargar previamente la librería mediante el comando “npm install phaser” o bien a través de un enlace CDN. Una vez disponible en nuestro entorno de desarrollo, se puede empezar a utilizar su API.

¿Cómo familiarizarse con la API de Phaser?

La mejor forma de familiarizarse con Phaser, es siguiendo el tutorial oficial que se encuentra en el apartado “learn” de la web. Tras realizar ese ejercicio, seremos capaces de implementar los primeros pasos de su API.

La clase Game, permite crear instancias que encapsulan las lógicas de cada juego. Dicha clase se puede exportar directamente de la librería Phaser, y admite un objeto JavaScript que define sus características.

Entre los parámetros de configuración de este objeto, se deciden aspectos como el alto y ancho del contenedor, o el tipo de motor de físicas por defecto.

Adicionalmente, también se pueden definir el conjunto de escenas del juego. La mejor forma de definir una escena, es extendiendo la clase “Scene” de Phaser. Mediante este sistema, se genera una clase nueva que hereda los métodos y propiedades necesarias.

Los métodos preload, create y update

Toda escena de Phaser, admite una serie de métodos que se ejecutan en determinados momentos. Algunos de estos métodos principales son preload(), create() y update().

Tal y como su nombre sugiere, preload() permite cargar los recursos que posteriormente serán utilizados. Recursos como imágenes, “sprites” o ficheros de audio.

El método create() y update() funcionan de forma similar a la que ya vimos en la librería P5.js. El primero se lanza solo una vez al inicio para preparar esos elementos del juego que no van a variar, como por ejemplo, una imagen de fondo.

Update actúa de “main loop”. Se trata de una función que se ejecuta indefinidamente para actualizar en todo momento el estado del juego y de sus elementos.

Una vez preparado lo que podríamos llamar el mundo del juego, se puede empezar a llenar de objetos o “game objects”. Uno de los objetos más característicos son los “Sprites”, imágenes que representan distintos estados de un elemento del juego.

Gracias al sistema de animaciones de Phaser, se pueden integrar y animar sprites de forma cómoda. La API de ese sistema se encuentra en la propiedad “anims” de la escena.

Otro elemento clave de muchos juegos es, por supuesto, el sistema de físicas. Esta librería ofrece la posibilidad de cargar un motor llamado “arcade”. Gracias a esto, se pueden controlar colisiones y fuerzas de gravedad entre objetos del juego, sin tener que reinventar la rueda.

Activar este tipo de físicas, es tan sencillo como agregar un argumento adicional al instanciar la escena.

Finalmente, no se puede hablar de videojuego si no se ofrece al usuario cierta capacidad de control. Por supuesto, Phaser tiene ese aspecto resuelto. A través del método de la escena “this.input.keyboard.createCursorKeys()”, se activa un sistema de escucha los eventos de teclado, y asociar un comportamiento.

A continuación, vamos a crear nuestro el primero de muchos videojuegos gracias a la API de Phaser y JavaScript.

Crea tu primer videojuego 2D con Phaser

Para entender mejor la librería y familiarizarse con los conceptos que la rodean, lo mejor es empezar un proyecto. En esta ocasión vamos a crear un juego de plataformas en perspectiva isométrica.

Para este ejercicio, extraeremos los recursos gráficos de la web de Kenney. Kenney es un artista que ofrece material gráfico de gran calidad para videojuegos e interfaces de todo tipo. Y de forma completamente gratuita.

Por defecto Phaser no maneja escenarios con perspectiva isométrica. Sin embargo, existe un plugin llamado “phaser3-plugin-isometric” que consigue simular este comportamiento. A pesar de que no está activamente actualizado, nos va a servir para esta demo. Lo instalaremos a través del comando “npm i phaser3-plugin-isometric –save”.

Antes de empezar a programar, crearemos un archivo JSON que describa una escena, al que llamaremos “level”. En este archivo especificaremos qué elementos van a aparecer, y qué acciones van a realizar. 

{
  "configuration": {
    "backgroundColor": "#d7f3f6",
    "cameraFollow": true,
    "soundTrack": "soundTrackDuck"
  },
  "player": {
    "pos": [-1, 4, 2]
  },
  "items": [
    [],
    [
      {
        "type": "key",
        "pos": [0, 8],
        "onGet": [
          {
            "action": "play_sound",
            "sound": "keySound"
          },
          {
            "action": "delete",
            "target": "holeKeyYellow1"
          },
          {
            "action": "delete",
            "target": "self"
          }
        ]
      }
    ],
    [
      {
        "type": "coin",
        "pos": [2, 5],
        "onGet": [
          {
            "action": "add_coin"
          },
          {
            "action": "play_sound",
            "sound": "coinSound2"
          },
          {
            "action": "delete",
            "target": "self"
          }
        ]
      },
      {
        "type": "coin",
        "pos": [-2, 5],
        "onGet": [
          {
            "action": "add_coin"
          },
          {
            "action": "play_sound",
            "sound": "coinSound2"
          },
          {
            "action": "delete",
            "target": "self"
          }
        ]
      }
    ],
    [],
    [
      {
        "type": "coin",
        "pos": [0, 0],
        "onGet": [
          {
            "action": "add_coin"
          },
          {
            "action": "play_sound",
            "sound": "coinSound2"
          },
          {
            "action": "delete",
            "target": "self"
          }
        ]
      }
    ],
    [
      {
        "type": "star",
        "pos": [-1, 4],
        "onGet": [
          {
            "action": "play_sound",
            "sound": "keySound"
          },
          {
            "action": "delete",
            "target": "self"
          }
        ]
      }
    ]
  ],
  "tiles": [
    [
      {
        "comment": "Base para la llave amarilla",
        "type": "baseTile",
        "pos": [0, 8]
      },
      {
        "type": "baseTile",
        "pos": [-2, 5]
      },
      {
        "type": "baseTile",
        "pos": [-2, 4]
      },
      {
        "type": "baseTile",
        "pos": [-2, 3]
      },
      {
        "type": "baseTile",
        "pos": [-1, 3]
      },
      {
        "type": "baseTile",
        "pos": [0, 3]
      },
      {
        "type": "baseTile",
        "pos": [-1, 5]
      },
      {
        "comment": "Base para el jugador",
        "type": "baseTile",
        "pos": [-1, 4]
      },
      {
        "type": "baseTile",
        "pos": [0, 4]
      },
      {
        "type": "baseTile",
        "pos": [0, 5]
      },
      {
        "type": "baseTile",
        "pos": [2, 5]
      },
      {
        "type": "baseTile",
        "pos": [4, 5]
      },
      {
        "type": "baseTile",
        "pos": [4, 2]
      },
      {
        "type": "baseTile",
        "pos": [4, 0]
      },
      {
        "type": "baseTile",
        "pos": [3, 0]
      }
    ],
    [
      {
        "type": "baseTile",
        "pos": [1, 0]
      },
      {
        "type": "baseTile",
        "pos": [0, 0]
      }
    ],
    [],
    [],
    [
      {
        "comment": "Base para la estrella",
        "type": "keyHoleCube",
        "pos": [-1, 4],
        "offset": [1, 0, 0],
        "id": "holeKeyYellow1"
      }
    ]
  ]
}

De este modo, en un futuro podremos crear otras escenas a partir de nuevos archivos JSON.

El primer código que vamos a preparar es de index.js. En este, instanciamos la clase Game, pasándole un objeto de configuración. Entre los parámetros de configuración definiremos el tipo de físicas, el ancho y alto de la vista, y un par de escenas que detallaremos a continuación.

import "./SCSS/index.scss";
import Phaser, { Game, AUTO } from "phaser";
Phaser.GROUP = "Group";
import IsoScene from "./IsoScene";
import UILayer from "./UILayer";
const config = {
  type: AUTO,
  width: window.innerWidth,
  height: window.innerHeight,
  scene: [IsoScene, UILayer],
  physics: { default: "arcade" },
};
const game = new Game(config);

IsoScene va a extender la clase Scene. Ésta va a ser nuestra principal escena, donde va a ocurrir toda la acción. Al declarar esta clase, es preciso llamar a la función super() en el constructor. Allí le pasaremos un objeto de configuración que va importar el plugin instalado.

También en el constructor aprovechamos para declarar una serie de elementos del juego como propiedades de la escena. La propiedad player, por ejemplo, será una referencia al personaje que controlará el usuario.

constructor() {
    const sceneConfig = {
      key: "IsoScene",
      mapAdd: { isoPlugin: "iso", isoPhysics: "isoPhysics" },
    };
    super(sceneConfig);
    this.soundsCtrl = new SoundsCtrl(this);
    this.loader = new Loader(this);
    this.player = undefined;
    this.gridSize = 40;
  }

Tal y como hemos visto, la escena requiere de al menos tres métodos imprescindibles, preload(), create() y update(). Veremos brevemente cada uno de estos métodos.

En preload() cargamos los recursos de imágenes y audios.

preload() {
    const { configuration } = level;
    this.loader.loadAssets();
    this.soundsCtrl.loadSounds();
    if (configuration.soundTrack) {
      this.soundsCtrl.loadSoundTrack(configuration.soundTrack);
    }
  }

Para mantener el código organizado, delegamos las tareas de carga y control de audio e imágenes en clases específicas para ello. Loader y SoundCtrl.

import IsoPlugin, { IsoPhysics } from "phaser3-plugin-isometric";
class Loader {
  constructor(scene) {
    this.scene = scene;
  }
  loadAssets() {
    this.scene.load.image("baseTile", "imgs/tiles/tile_base.png");
    this.scene.load.image("underBaseTile", "imgs/tiles/tile_under_base.png");
    this.scene.load.image("keyHoleCube", "imgs/tiles/key_hole_cube.png");
    this.scene.load.image("shadow", "imgs/characters/shadow.png");
    this.scene.load.image("key", "imgs/items/key.png");
    this.scene.load.image("coin", "imgs/items/coin.png");
    this.scene.load.image("star", "imgs/items/star.png");
    this.scene.load.spritesheet(
      "character",
      "imgs/characters/character_femaleAdventurer_sheet_resized.png",
      { frameWidth: 66, frameHeight: 88 }
    );
    this.scene.load.scenePlugin({
      key: "IsoPlugin",
      url: IsoPlugin,
      sceneKey: "iso",
    });
    this.scene.load.scenePlugin({
      key: "IsoPhysics",
      url: IsoPhysics,
      sceneKey: "isoPhysics",
    });
  }
}
export default Loader;
class SoundsCtrl {
  constructor(scene) {
    this.scene = scene;
    this.soundTracks = [
      { key: "soundTrackDuck", url: "audios/Fluffing-a-Duck.mp3" },
    ];
    this.soundsData = [
      { key: "coinSound", url: "audios/coin.wav" },
      { key: "coinSound2", url: "audios/coin2.wav" },
      { key: "keySound", url: "audios/key.wav" },
    ];
    this.sounds = {};
    this.soundTrackKey = undefined;
  }
  loadSoundTrack(soundTrackKey) {
    const soundTrack = this.soundTracks.filter((soundTrackData) => {
      return soundTrackKey === soundTrackData.key;
    })[0];
    if (soundTrack) {
      this.scene.load.audio(soundTrack.key, soundTrack.url);
      this.soundTrackKey = soundTrack.key;
    }
  }
  loadSounds() {
    for (let i = 0, j = this.soundsData.length; i < j; i++) {
      const soundData = this.soundsData[i];
      this.scene.load.audio(soundData.key, soundData.url);
    }
  }
  addSounds() {
    for (let i = 0, j = this.soundsData.length; i < j; i++) {
      const soundData = this.soundsData[i];
      this.sounds[soundData.key] = this.scene.sound.add(soundData.key);
    }
    if (this.soundTrackKey) {
      this.sounds[this.soundTrackKey] = this.scene.sound.add(
        this.soundTrackKey
      );
      this.sounds[this.soundTrackKey].loop = true;
    }
  }
  play(key) {
    if (!this.sounds[key]) {
      return;
    }
    this.sounds[key].play();
  }
  playSoundTrack() {
    if (!this.soundTrackKey) {
      return;
    }
    this.play(this.soundTrackKey);
  }
  stopSoundTrack() {
    if (!this.soundTrackKey) {
      return;
    }
    this.sounds[this.soundTrackKey].stop();
  }
}
export default SoundsCtrl;

En el método create() de la escena, preparamos varios elementos del juego, en función del objeto JSON level.

En las instrucciones de este método, añadimos los sonidos y música del juego. Definimos la fuerza de gravedad y la perspectiva del plugin de isometría. Seteamos el color de fondo, y determinamos el comportamiento de la cámara.

  create() {
    const { configuration, player, items, tiles } = level;
    this.soundsCtrl.addSounds();
    this.tilesGroup = this.add.group();
    this.itemsGroup = this.add.group();
    this.isoPhysics.world.gravity.setTo(0, 0, -1000);
    this.isoPhysics.projector.origin.setTo(0.5, 0.5);
    this.iso.projector.origin.setTo(0.5, 0.5);
    this.createTiles(tiles);
    this.createItems(items);
    this.createAnimations();
    this.createPlayer(
      player.pos[0] * this.gridSize,
      player.pos[1] * this.gridSize,
      player.pos[2] * this.gridSize
    );
    if (configuration.cameraFollow) {
      this.cameras.main.startFollow(this.player);
    }
    if (configuration.soundTrack) {
      this.soundsCtrl.playSoundTrack(configuration.soundTrack);
    }
    this.cameras.main.setBackgroundColor(
      configuration.backgroundColor || "#d7f3f6"
    );
  }

También crearemos dos grupos de objetos mediante el método “group()”. Posteriormente rellenaremos cada grupo con las losetas del escenario, y los ítems coleccionables. Para ello definimos los métodos “createTiles()” y “createItems()”.

  createTiles(tiles) {
    for (let i = 0, j = tiles.length; i < j; i++) {
      const tilesRow = tiles[i];
      for (let n = 0, m = tilesRow.length; n < m; n++) {
        const tileData = tilesRow[n];
        this.add.baseTile(
          tileData.pos[0] * this.gridSize +
            (tileData.offset ? tileData.offset[0] : 0),
          tileData.pos[1] * this.gridSize +
            (tileData.offset ? tileData.offset[1] : 0),
          i * this.gridSize + (tileData.offset ? tileData.offset[2] : 0),
          tileData.type,
          this.tilesGroup,
          tileData
        );
      }
    }
  }
  createItems(items) {
    for (let i = 0, j = items.length; i < j; i++) {
      const itemsRow = items[i];
      for (let n = 0, m = itemsRow.length; n < m; n++) {
        const itemData = itemsRow[n];
        this.add.item(
          itemData.pos[0] * this.gridSize,
          itemData.pos[1] * this.gridSize,
          i * this.gridSize + 10,
          itemData.type,
          this.itemsGroup,
          itemData
        );
      }
    }
  }

Por último,mediante el método “createPlayer()”, añadimos un personaje “player” controlado por el usuario. Y creamos las animaciones de su sprite con “createAnimations()”.

  createPlayer(x, y, z = 500) {
    this.player = this.add.player(x, y, z, "character");
  }
  createAnimations() {
    this.anims.create({
      key: "walk",
      frames: this.anims.generateFrameNumbers("character", {
        start: 36,
        end: 43,
      }),
      frameRate: 10,
      repeat: -1,
    });
    this.anims.create({
      key: "idle",
      frames: [{ key: "character", frame: 0 }],
      frameRate: 20,
    });
    this.anims.create({
      key: "jump",
      frames: [{ key: "character", frame: 8 }],
      frameRate: 20,
    });
  }

Para crear los items, las losetas y el player, llamamos a las instrucciones “this.add.player()”, “this.add.baseTile()” y “this.add.item()” dentro de los métodos mencionados.

Para que Phaser reconozca estas instrucciones, creamos una serie de clases nuevas. “Player”, “BaseTile, e “Item” son clases extendidas de IsoSprite, y describen el comportamiento de cada tipo de elemento. Cabe mencionar que IsoSprite, es una clase del plugin “phaser3-plugin-isometric” que se extiende de Sprite.

Las clases adicionales basadas en Sprite

import Phaser from "phaser";
import IsoSprite from "phaser3-plugin-isometric/src/IsoSprite";
class Player extends IsoSprite {
  constructor(scene, x, y, z, key, frame) {
    super(scene, x, y, z, key, frame);
    this.jumping = false;
    this.applyPhysics();
    this.addEvents();
  }
  applyPhysics() {
    this.scene.isoPhysics.world.enable(this);
    this.body.onWorldBounds = true;
    this.body.bounce.set(0, 0, 0);
    this.body.mass = 1;
    this.accelerationFactor = 1300;
    this.fricctionFactor = 0.08;
    this.maxVelocity = 500;
  }
  update() {
    this.applyVelocityLimit();
    this.applyFriction();
    if (this.isoZ < -200) {
      this.scene.gameOver();
    }
  }
  postUpdate() {
    this.applyFriction();
    if (this.jumping && this.body.touching.up) {
      this.jumping = false;
      this.anims.play("idle", true);
    }
  }
  respawn() {
    this.body.velocity.setTo(0, 0, 0);
    this.body.position.setTo(0, this.scene.cubeSize * 5, 500);
    this.body.acceleration.setTo(0, 0, 0);
  }
  applyVelocityLimit() {
    if (!this.body) {
      return;
    }
    if (Math.abs(this.body.velocity.x) > this.maxVelocity) {
      this.body.velocity.x = Math.round(
        Math.sign(this.body.velocity.x) * this.maxVelocity
      );
    }
    if (Math.abs(this.body.velocity.y) > this.maxVelocity) {
      this.body.velocity.y = Math.round(
        Math.sign(this.body.velocity.y) * this.maxVelocity
      );
    }
  }
  applyFriction() {
    if (!this.body) {
      return;
    }
    if (this.body.velocity.x === 0 && this.body.velocity.y === 0) {
      return;
    }
    let velX =
      this.body.velocity.x - this.body.velocity.x * this.fricctionFactor;
    let velY =
      this.body.velocity.y - this.body.velocity.y * this.fricctionFactor;
    velX = Math.abs(velX) < 0.5 ? 0 : velX;
    velY = Math.abs(velY) < 0.5 ? 0 : velY;
    this.body.velocity.setTo(velX, velY, this.body.velocity.z);
  }
  addEvents() {
    this.scene.events.on("update", () => {
      this.update();
    });
    this.scene.events.on("postupdate", () => {
      this.postUpdate();
    });
    const spaceBar = this.scene.input.keyboard.addKey(
      Phaser.Input.Keyboard.KeyCodes.SPACE
    );
    const leftKey = this.scene.input.keyboard.addKey(
      Phaser.Input.Keyboard.KeyCodes.LEFT
    );
    const rightKey = this.scene.input.keyboard.addKey(
      Phaser.Input.Keyboard.KeyCodes.RIGHT
    );
    const upKey = this.scene.input.keyboard.addKey(
      Phaser.Input.Keyboard.KeyCodes.UP
    );
    const downKey = this.scene.input.keyboard.addKey(
      Phaser.Input.Keyboard.KeyCodes.DOWN
    );
    spaceBar.on("down", (key) => {
      if (!this.body.touching.up) {
        return;
      }
      this.jumping = true;
      this.body.velocity.setTo(this.body.velocity.x, this.body.velocity.y, 300);
      this.anims.play("jump", true);
    });
    leftKey.on("down", (key) => {
      if (Math.abs(this.body.velocity.x) > this.maxVelocity) {
        return;
      }
      this.flipX = true;
      this.body.acceleration.setTo(
        this.body.acceleration.x,
        this.accelerationFactor,
        this.body.acceleration.z
      );
      this.anims.play("walk", true);
    });
    leftKey.on("up", (key) => {
      this.body.acceleration.setTo(
        this.body.acceleration.x,
        0,
        this.body.acceleration.z
      );
      this.anims.play("idle", true);
    });
    rightKey.on("down", (key) => {
      this.flipX = false;
      this.body.acceleration.setTo(
        this.body.acceleration.x,
        -this.accelerationFactor,
        this.body.acceleration.z
      );
      this.anims.play("walk", true);
    });
    rightKey.on("up", (key) => {
      this.body.acceleration.setTo(
        this.body.acceleration.x,
        0,
        this.body.acceleration.z
      );
      this.anims.play("idle", true);
    });
    upKey.on("down", (key) => {
      this.flipX = false;
      this.body.acceleration.setTo(
        this.accelerationFactor,
        this.body.acceleration.y,
        this.body.acceleration.z
      );
      this.anims.play("walk", true);
    });
    upKey.on("up", (key) => {
      this.body.acceleration.setTo(
        0,
        this.body.acceleration.y,
        this.body.acceleration.z
      );
      this.anims.play("idle", true);
    });
    downKey.on("down", (key) => {
      this.flipX = true;
      this.body.acceleration.setTo(
        -this.accelerationFactor,
        this.body.acceleration.y,
        this.body.acceleration.z
      );
      this.anims.play("walk", true);
    });
    downKey.on("up", (key) => {
      this.body.acceleration.setTo(
        0,
        this.body.acceleration.y,
        this.body.acceleration.z
      );
      this.anims.play("idle", true);
    });
  }
  removeEvents() {
    this.scene.events.off("update");
    this.scene.events.off("postupdate");
  }
  delete() {
    this.removeEvents();
    this.destroy();
  }
}
Phaser.GameObjects.GameObjectFactory.register(
  "player",
  function (x, y, z, key, group, frame = 0) {
    const sprite = new Player(this.scene, x, y, z, key, frame);
    if (typeof group === "undefined") {
      this.displayList.add(sprite);
      this.updateList.add(sprite);
    } else {
      group.add(sprite, true);
    }
    return sprite;
  }
);
export default Player;
import Phaser from "phaser";
import IsoSprite from "phaser3-plugin-isometric/src/IsoSprite";
class BaseTile extends IsoSprite {
  constructor(scene, x, y, z, key, data) {
    super(scene, x, y, z, key, 0);
    this.data = {
      ...data,
      destroy: () => {},
    };
    this.addEvents();
    this.applyPhysics();
    if (data.id) {
      this.id = data.id;
    }
    if (data.tint) {
      this.setTint(data.tint);
    }
    if (typeof data.alpha === "number") {
      this.setAlpha(data.alpha);
    }
  }
  applyPhysics() {
    this.scene.isoPhysics.world.enable(this);
    this.body.immovable = true;
    this.body.allowGravity = false;
  }
  enablePhysics(reset) {
    this.body.enable = true;
  }
  disablePhysics() {
    this.body.enable = false;
  }
  update() {
    if (this.marked) {
    }
  }
  addEvents() {
    this.scene.events.on("update", () => {
      this.update();
    });
  }
  delete() {
    this.disablePhysics();
    this.scene.tilesGroup.remove(this, true, true);
  }
}
Phaser.GameObjects.GameObjectFactory.register(
  "baseTile",
  function (x, y, z, key, group, data) {
    const sprite = new BaseTile(this.scene, x, y, z, key, data);
    if (typeof group === "undefined") {
      this.displayList.add(sprite);
      this.updateList.add(sprite);
    } else {
      group.add(sprite, true);
    }
    return sprite;
  }
);
export default BaseTile;
import Phaser from "phaser";
import IsoSprite from "phaser3-plugin-isometric/src/IsoSprite";
class Item extends IsoSprite {
  constructor(scene, x, y, z, key, data) {
    super(scene, x, y, z, key, 0);
    this.data = { ...data, destroy: () => {} };
    scene.isoPhysics.world.enable(this);
    if (data.id) {
      this.id = data.id;
    }
    if (data.tint) {
      this.setTint(data.tint);
    }
    if (typeof data.alpha === "number") {
      this.setAlpha(data.alpha);
    }
  }
  onGet() {
    if (!this.data.onGet) {
      return;
    }
    for (let i = 0, j = this.data.onGet.length; i < j; i++) {
      const action = this.data.onGet[i];
      this.handleOnGetAction(action);
    }
  }
  handleOnGetAction(action) {
    switch (action.action) {
      case "delete":
        const target = action.target;
        if (target === "self") {
          this.delete();
          return;
        }
        this.scene.children.getChildren().forEach((child) => {
          if (child.data && child.data.id === target) {
            child.delete();
          }
        });
        return;
      case "add_coin":
        this.scene.events.emit("addCoin");
        return;
      case "play_sound":
        this.scene.soundsCtrl.play(action.sound);
        return;
      default:
        return;
    }
  }
  delete() {
    this.scene.isoPhysics.world.bodies.entries =
      this.scene.isoPhysics.world.bodies.entries.filter((body) => {
        return body !== this.body;
      });
    this.scene.isoPhysics.world.bodies.length =
      this.scene.isoPhysics.world.bodies.entries.length;
    this.scene.itemsGroup.remove(this, true, true);
  }
}
Phaser.GameObjects.GameObjectFactory.register(
  "item",
  function (x, y, z, key, group, data) {
    const sprite = new Item(this.scene, x, y, z, key, data);
    if (typeof group === "undefined") {
      this.displayList.add(sprite);
      this.updateList.add(sprite);
    } else {
      group.add(sprite, true);
    }
    return sprite;
  }
);
export default Item;

En el método “update()” comprobaremos las colisiones de los elementos del juego. Estas colisiones se pueden dar entre “player” y el grupo de losetas. Entre el grupo de ítems y las losetas. Y entre el “player” y los algun item.

  update() {
    if (!this.player) {
      return;
    }
    this.isoPhysics.world.collide(this.player, this.tilesGroup, (player) => {});
    this.isoPhysics.world.collide(this.itemsGroup, this.tilesGroup);
    this.isoPhysics.world.collide(
      this.player,
      this.itemsGroup,
      (player, item) => {
        item.onGet();
      }
    );
  }

Finalmente, creamos un método gameOver para resetear la escena si el jugador pierde.

  gameOver() {
    this.soundsCtrl.stopSoundTrack();
    this.player.delete();
    this.events.emit("gameOver");
    this.scene.restart();
  }

Definiremos los elementos de interfaz gráfica en una escena independiente. Vamos a llamar esta escena UILayer.

import { Scene } from "phaser";
class UILayer extends Scene {
  constructor() {
    const sceneConfig = {
      key: "UIScene",
      active: true,
    };
    super(sceneConfig);
    this.coins = 0;
    this.infoCoins = undefined;
  }
  preload() {
    this.load.image("uiCoin", "imgs/items/coin.png");
    this.load.image("uiStar", "imgs/items/star.png");
  }
  updateText(newText) {
    if (!this.infoCoins) {
      return;
    }
    this.infoCoins.setText(newText);
  }
  create() {
    this.infoCoins = this.add.text(75, 37, "x0", {
      font: "30px Helvetica",
      fill: "#41737a",
    });
    this.add.image(50, 50, "uiCoin");
    const game = this.scene.get("IsoScene");
    game.events.on("addCoin", () => {
      this.coins += 1;
      this.updateText(`x${this.coins}`);
    });
    game.events.on("gameOver", () => {
      this.coins = 0;
      this.updateText(`x0`);
    });
  }
}
export default UILayer;

Con JavaScript y Phaser, puedes crear el siguiente videojuego revolucionario

Siempre me ha seducido la idea de crear mi propio videojuego. Ya de bien pequeño, cuando aún desconocía el mundo de la programación. 

Recuerdo dedicar incontables horas a crear aventuras “point & click” con el Power Point. Completamente influenciado por los juegos de aventuras gráficas como “Monkey Island”.

Gracias a librerías como Phaser, hoy podemos crear juegos directamente en el navegador y jugarlos desde cualquier parte del mundo. Y quien sabe, hasta incluso venderlos por cifras millonarias.

Este ha sido un pequeño vistazo a la librería Phaser. A continuación os dejo enlaces a más recursos, así como el ejercicio subido al repositorio de Github de “Librerías Js”:

Antes de despedirme, me gustaría lanzarte un pequeño reto, recientemente publiqué un post analizando la librería ToneJs para la creación de música en el navegador, te animo a que la estudies, y la uses para sonorizar tu juego. Aquí tienes el enlace a la publicación

Crear música con JavaScript y ToneJs

Puedes dejar en los comentarios qué videojuego has creado con estos recursos.

Nos vemos pronto, un abrazo!

Deja un comentario