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).
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.
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.


- 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!
Alguna idea de algo que te gustaría que estudiemos en estas entregas? Dejalo en los comentarios y hasta la próxima!
0 comentario
Aún no hay comentarios. ¡Sé el primero en comentar!