Cada actualización que hago al juego, me trae nuevas ideas, y puedo investigar más para que el proyecto tenga mejor forma. Como programador en otras tecnologías, cuando veo que un archivo de un proyecto esta quedando demasiado grande, comienzo a pensar en como mejorar la estructura para que sea modular y mejorar el mantenimiento de la aplicación a futuro.

En la parte 1 del tutorial creamos lo básico para movernos por la pantalla, en la parte 2 agregamos los enemigos.

En el post de hoy traigo varias cosas, primero: nuevos "assets", ya que me tomé más tiempo para crear los personajes pixel art y otros items que agregaremos al juego más adelante. Puedes descargarte aquí un archivo zip con todos los assets actualizados y los que usaremos en el futuro (el enlace dirige a Drobox).


La Nueva Estructura


Para esta versión del juego vamos a crear escenas, una al principio donde tenemos un botón que diga "JUGAR" y algún título para nuestro juego. Cuando le das al botón, tiene que comenzar oficialmente el juego.

mi-juego/ 
  ├── assets/ 
  │   ├── reddie.png          # Sprite del enemigo 
  │   ├── greenie.png         # Sprite del jugador 
  │   └── door.png            # Sprite de la puerta  objetivo 
  ├── index.html              # HTML principal que  carga Phaser y scripts 
  ├── js/ 
  │   ├── config.js           # Configuración del  juego (640x480, física) 
  │   ├── main.js             # Variables globales e  inicialización 
  │   ├── entidades/ 
  │   │   ├── enemigo.js      # Clase Enemigo con  movimiento automático 
  │   │   └── jugador.js      # Clase Player con  controles WASD/flechas 
  │   └── escenas/ 
  │       ├── GameScene.js    # Escena principal del juego 
  │       └── MenuScene.js    # Escena del menú con  botón JUGAR
lang-txt
 
También agregué una carpeta donde manejaremos las entidades, es decir, nuestro jugador, los enemigos. Lo que busca este cambio es que la lógica del juego se empiece a distribuir en archivos más pequeños para mantener,

Lo más importante que he cambiado es que ahora tenemos las Escenas, y moví la lógica del Jugador y los Enemigos a un archivo independiente.

Ahora veremos como quedan los archivos del juego, más pequeños y más manejables. Dejé toda la explicación como comentarios por cada línea, pero si queda alguna duda, recuerda que puedes dejarme un comentario al final del post.


Main.js

/**
 * VARIABLES GLOBALES
 * 
 * Estas variables almacenan los objetos principales del juego
 * para poder acceder a ellos desde cualquier función
 */
let player;     // El sprite del jugador
let teclas;     // Objeto que controla las teclas del teclado
let puerta;     // El sprite de la puerta (objetivo del juego)
let enemigos;   // Grupo que contiene todos los enemigos

// Inicializar el juego con la configuración definida en config.js
const game = new Phaser.Game({
    ...gameConfig,
    scene: [MenuScene, GameScene] 
});
lang-javascript


Config.js

/**
 * CONFIGURACIÓN DEL JUEGO
 * 
 * Aquí definimos todas las configuraciones básicas de nuestro juego Phaser
 */
const gameConfig = {
    type: Phaser.AUTO,          // Phaser elegirá automáticamente WebGL o Canvas
    width: 640,                 // Ancho del canvas en píxeles
    height: 480,                // Alto del canvas en píxeles
    backgroundColor: '#1d1d1d', // Color de fondo oscuro
    pixelArt: true,             // Habilitar modo pixel art para sprites nítidos
    physics: {
        default: 'arcade',      // Usar el sistema de física Arcade (simple y rápido)
        arcade: {
            gravity: { y: 0 },  // Sin gravedad (juego vista desde arriba)
            debug: false        // Cambiar a true para ver los cuerpos de colisión
        }
    }
};
lang-javascript


MenuScene.js

class MenuScene extends Phaser.Scene {
 
    constructor() {
        super({ key: 'MenuScene' });
    }

    preload() {
      // Por acá vamos a cargar un Logo del juego más adelante
    }

    create() {
        // Usamos el mismo fondo que ya tenemos en el config.js

        // Fondo del Menú
        this.add.rectangle(
          gameConfig.width / 2,    // x: centro
          gameConfig.height / 2,   // y: centro
          gameConfig.width,
          gameConfig.height,
          0x1d1d1d
        );

        // Titulo del Juego
        this.add.text(
          gameConfig.width / 2,   // x: centro horizontal
          gameConfig.height / 3,  // y: tercio superior (160)
          'THE ESCAPE GAME',
          {
            // Configuración de la fuente
            fontSize: '48px',
            fill: '#ffffff',
            fontFamily: 'Arial',
            stroke: '#000000',
            strokeThickness: 4
          }
        ).setOrigin(0.5); // Centrar el texto

        const botonJugar = this.add.text(
          gameConfig.width / 2,   // x: centro horizontal
          gameConfig.height / 2,  // y: centro vertical
          'JUGAR',
          {
            // Configuración de la fuente
            fontSize: '32px',
            fill: '#00ff00',
            fontFamily: 'Arial',
            stroke: '#333333',
            padding: { x: 20, y: 10 } // Espaciado con respecto al borde
          }
        ).setOrigin(0.5); 

        // Boton interactivo
        botonJugar.setInteractive();

        // Efecto hover, el botón cambia de color cuando pasa el mouse
        botonJugar.on('pointerover', () => {
          botonJugar.setStyle({ fill: '#ffffff' });
        });

        // Cuando quitas el mouse, regresa al color original
        botonJugar.on('pointerout', () => {
          botonJugar.setStyle({ fill: '#00ff00' });
        });

        // Cuando haces click, cambia a la scena principal
        botonJugar.on('pointerdown', () => {
          this.scene.start('GameScene');
        });       
    }
}
lang-javascript


GameScene.js

class GameScene extends Phaser.Scene {
    constructor() {
        super({ key: 'GameScene' });
    }
        
    /**
     * FUNCIÓN PRELOAD
     * 
     * Se ejecuta una sola vez al inicio del juego.
     * Aquí cargamos todos los recursos (imágenes, sonidos, etc.)
     * que necesitaremos en el juego.
     */
    preload() {
        // Cargar las imágenes desde la carpeta assets
        this.load.image('jugador', 'assets/greenie.png');  // Sprite del jugador
        this.load.image('enemigo', 'assets/reddie.png');   // Sprite de los enemigos
        this.load.image('puerta', 'assets/door.png');      // Sprite de la puerta
    }

    /**
     * FUNCIÓN CREATE
     * 
     * Se ejecuta una sola vez después de preload.
     * Aquí creamos todos los objetos del juego, configuramos la física
     * y establecemos las colisiones entre objetos.
     */
    create() {
        // === CREAR EL JUGADOR ===
        // Crear el jugador en la posición (50, 450) 
        player = new Player(this, 50, 450);
        
        // === CREAR ENEMIGOS ===
        // Crear un grupo para almacenar todos los enemigos
        enemigos = this.physics.add.group();

        // Definir las posiciones y direcciones de cada enemigo
        const posicionesEnemigos = [
            { x: 100, y: 200, dir: 'horizontal' },  // Enemigo que se mueve izquierda-derecha
            { x: 300, y: 150, dir: 'vertical' },    // Enemigo que se mueve arriba-abajo
            { x: 200, y: 400, dir: 'horizontal' }   // Otro enemigo horizontal
        ];

        // Crear cada enemigo y añadirlo al grupo
        posicionesEnemigos.forEach(({ x, y, dir }) => {
            const enemigo = new Enemigo(this, x, y, dir);
            enemigos.add(enemigo);
        });

        // === CREAR LA PUERTA (OBJETIVO) ===
        // La puerta es un objeto estático (no se mueve) en la esquina superior derecha
        puerta = this.physics.add.staticImage(600, 50, 'puerta').setScale(4);

        // === CONFIGURAR COLISIONES ===
        // Si el jugador toca un enemigo -> función perder()
        this.physics.add.overlap(player, enemigos, this.perder, null, this);
        // Si el jugador toca la puerta -> función ganar()
        this.physics.add.overlap(player, puerta, this.ganar, null, this);
    }

    /**
     * FUNCIÓN UPDATE
     * 
     * Se ejecuta continuamente, aproximadamente 60 veces por segundo.
     * Aquí manejamos el movimiento del jugador y actualizaciones del juego.
     */
    update() {
        player.update();
    }

    perder() {
        alert("¡Has perdido! Toca un enemigo.");
        this.scene.restart();
    }

    ganar() {
        alert("¡Felicidades! Has llegado a la puerta.");
        this.scene.restart();
    }
}
lang-javascript


MenuScene.js

class MenuScene extends Phaser.Scene {
 
    constructor() {
        super({ key: 'MenuScene' });
    }

    preload() {
      // Por acá vamos a cargar un Logo del juego más adelante
    }

    create() {
        // Usamos el mismo fondo que ya tenemos en el config.js

        // Fondo del Menú
        this.add.rectangle(
          gameConfig.width / 2,    // x: centro
          gameConfig.height / 2,   // y: centro
          gameConfig.width,
          gameConfig.height,
          0x1d1d1d
        );

        // Titulo del Juego
        this.add.text(
          gameConfig.width / 2,   // x: centro horizontal
          gameConfig.height / 3,  // y: tercio superior (160)
          'THE ESCAPE GAME',
          {
            // Configuración de la fuente
            fontSize: '48px',
            fill: '#ffffff',
            fontFamily: 'Arial',
            stroke: '#000000',
            strokeThickness: 4
          }
        ).setOrigin(0.5); // Centrar el texto

        const botonJugar = this.add.text(
          gameConfig.width / 2,   // x: centro horizontal
          gameConfig.height / 2,  // y: centro vertical
          'JUGAR',
          {
            // Configuración de la fuente
            fontSize: '32px',
            fill: '#00ff00',
            fontFamily: 'Arial',
            stroke: '#333333',
            padding: { x: 20, y: 10 } // Espaciado con respecto al borde
          }
        ).setOrigin(0.5); 

        // Boton interactivo
        botonJugar.setInteractive();

        // Efecto hover, el botón cambia de color cuando pasa el mouse
        botonJugar.on('pointerover', () => {
          botonJugar.setStyle({ fill: '#ffffff' });
        });

        // Cuando quitas el mouse, regresa al color original
        botonJugar.on('pointerout', () => {
          botonJugar.setStyle({ fill: '#00ff00' });
        });

        // Cuando haces click, cambia a la scena principal
        botonJugar.on('pointerdown', () => {
          this.scene.start('GameScene');
        });       
    }
}
lang-javascript


Enemigo.js

/**
 * Clase Enemigo - Crea enemigos que patrullan automáticamente
 * 
 * Esta clase extiende de Phaser.Physics.Arcade.Sprite para crear enemigos
 * que se mueven de forma automática y rebotan en los bordes de la pantalla
 */
class Enemigo extends Phaser.Physics.Arcade.Sprite {
  /**
   * Constructor del enemigo
   * scene - La escena donde se creará el enemigo
   * x - Posición inicial en el eje X
   * y - Posición inicial en el eje Y  
   * dir - Dirección de movimiento: 'horizontal' o 'vertical'
   */
  constructor(scene, x, y, dir = 'horizontal') {
    // Llamar al constructor padre con la imagen 'enemigo'
    super(scene, x, y, 'enemigo');
    
    
    // Agregar este sprite a la escena (para que sea visible)
    scene.add.existing(this);
    this.setScale(4);


    // Agregar física a este sprite (para que pueda moverse y colisionar)
    scene.physics.add.existing(this);

    // Propiedades del enemigo
    this.velocidad = 100;        // Velocidad de movimiento en píxeles por segundo
    this.direccion = dir;        // Guardar la dirección para referencia futura

    /**
     * IMPORTANTE: Usamos delayedCall porque Phaser necesita un frame completo
     * para inicializar completamente el cuerpo físico (this.body) del sprite.
     * Sin este delay, las propiedades de física no se aplicarían correctamente.
     */
    scene.time.delayedCall(0, () => {
      // Hacer que el enemigo rebote al tocar los bordes de la pantalla
      this.body.setCollideWorldBounds(true);
      
      // setBounce(1) = rebote perfecto (no pierde velocidad al rebotar)
      // setBounce(0.5) = rebote con pérdida de velocidad
      this.body.setBounce(1);
      
      // Establecer la velocidad inicial según la dirección
      if (dir === 'horizontal') {
        // Movimiento horizontal (izquierda-derecha)
        this.body.setVelocityX(this.velocidad);
      } else {
        // Movimiento vertical (arriba-abajo)  
        this.body.setVelocityY(this.velocidad);
      }
    });
  }
}
lang-javascript


Jugador.js

class Player extends Phaser.Physics.Arcade.Sprite {
    constructor(scene, x, y) {
        super(scene, x, y, 'jugador');

        // Agregar a la escena
        scene.add.existing(this);
        this.setScale(4);
        scene.physics.add.existing(this);

        // Configuración inicial
        this.body.setCollideWorldBounds(true);
        this.speed = 200;

        // Configurar controles
        this.teclas = scene.input.keyboard.createCursorKeys();
        this.wasd = scene.input.keyboard.addKeys('W,S,A,D');

        // Variables para futuras funcionalidades
        this.vidas = 3;
        this.invulnerable = false;
        this.tiempoInvulnerable = 0;
    }

    update() {
        // Resetear movimiento
        this.body.setVelocity(0);

        // Movimiento horizontal
        if (this.teclas.left.isDown || this.wasd.A.isDown) {
            this.body.setVelocityX(-this.speed);
        } else if (this.teclas.right.isDown || this.wasd.D.isDown) {
            this.body.setVelocityX(this.speed);
        }

        // Movimiento vertical
        if (this.teclas.up.isDown || this.wasd.W.isDown) {
            this.body.setVelocityY(-this.speed);
        } else if (this.teclas.down.isDown || this.wasd.S.isDown) {
            this.body.setVelocityY(this.speed);
        }
    }
}
lang-javascript


index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Mi Primer Juego</title>
    <script src="https://cdn.jsdelivr.net/npm/phaser@3.70.0/dist/phaser.js"></script>
    <style>
      body {
        margin: 0;
        background: #000;
      }
      canvas {
        display: block;
        margin: auto;
      }
    </style>
  </head>
  <body>
    <script src="js/config.js"></script>
    <script src="js/entidades/enemigo.js"></script>
    <script src="js/entidades/jugador.js"></script>
    <script src="js/escenas/MenuScene.js"></script>
    <script src="js/escenas/GameScene.js"></script>
    <script src="js/main.js"></script>
  </body>
</html>
lang-html


Lo que logramos hoy...


Ahora al cargar el juego debería aparecer nuestra escena de menú. Aún no sé como bautizar el juego pero por ahora utilizaremos The Escape Game.
intro.jpg 7.11 KB
Los nuevos enemigos y nuestro nuevo heroe con una mejor apariencia pixelart.
game.jpg 10.2 KB
A medida que avancemos en el proyecto, podremos ir introduciendo más mejoras. En el próximo tutorial el plan es:
  • Pantalla de fin del juego
  • Pixelart de una llave que debemos conseguir primero para poder abrir la puerta
En una futura entrega, agregaremos vidas a nuestro heroe, sistema de puntaje, diseño de nivel (necesitamos poner algo en ese fondo), y finalmente será desarrollar 3 niveles.

Alguna idea de algo que te gustaría que estudiemos en estas entregas? Dejalo en los comentarios y hasta la próxima!