Continuando con nuestra serie para crear un juego estilo “top-down” en PhaserJS, en esta ocasión traigo el código necesario para que podamos comenzar a crear niveles.
Mi idea en este particular es que sea muy fácil poder diseñar el mapa directamente en el código, lo que nos simplifica un poco la vida al no depender tanto de una herramienta gráfica (por ahora). Así que el plan es usar ASCII para poder diseñar el nivel a nuestro gusto:
Captura de pantalla_20250723_125012.png105 KB
El carácter # nos sirve para decir donde va una pared. La letra “C” nos permite definir donde va una moneda. Nuestro jugador esta representado con la letra “P”, la puerta de salida o el objetivo con la letra “D” y los enemigos se manejan con dos letras: la “e” minúscula para un enemigo que se mueve de forma vertical, y la “E” mayúscula para un enemigo que se mueve horizontalmente.
Para nuestra pared agregué este nuevo asset: “roof_wall.png”, que nos permitirá mostrar la pared. Lo puedes descargar aquí y colocarlo en la carpeta assets. roof_wall.png1.36 KBVamos al código!
Este archivo se encargara de las constantes, es decir, controlará el estado del juego. Si queremos ajustar el tamaño de nuestros gráficos, o cambiar la velocidad del jugador, en vez de tener que ir a cada archivo y cambiarlo podemos hacerlo aquí, por lo que el objetivo es centralizar algunas variables importantes.
main.js
Para nuestro archivo js/main.js el cambio es muy pequeño, sólo tenemos que agregar una nueva variable global (y aprovechamos la oportunidad para poner algo de orden):
/** * ESTADO GLOBAL DEL JUEGO * * Objeto que contiene todos los elementos principales del juego * organizados de forma clara y accesible */// Referencias globales para compatibilidad (se eliminarán gradualmente)
let player, enemigos, puerta;
let nivelActual = 1;
// Inicializar el juego con la configuración definida en config.js
const game = new Phaser.Game({
...gameConfig,
scene: [MenuScene, GameScene]
});
lang-javascript
Lo más importante aquí es agregar "let nivelActual = 1", esto nos permitirá definir en que nivel comienza el juego.
GameScene.js
Nuestro archivo js/escenas/GameScene.js recibió un cambio donde se simplificó significativamente. Los principales cambios sucedieron en la función create() y ganar() ya que introduciremos nuevas funciones que se encargaran de crear los niveles y gestionar los elementos:
class GameScene extends Phaser.Scene {
constructor() {
super({ key: 'GameScene' });
this.levelLoader = newLevelLoader(this);
this.gameManager = newGameManager();
}
/** * 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
this.load.image('roof_wall_tiles', 'assets/roof_wall.png'); // Sprite de la pared
this.load.image('moneda', 'assets/coin.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() {
// === CARGAR Y CREAR NIVEL ===// Cargar el nivel actual usando GameManager
const nombreNivel = this.gameManager.getNivelActual();
const levelMap = this.levelLoader.cargarNivel(nombreNivel);
if (levelMap) {
this.levelLoader.crearNivelDesdeASCII(levelMap);
}
}
/** * 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() {
nivelActual += 1;
this.scene.restart({
hayMasNiveles: this.gameManager.hayMasNiveles(),
progreso: this.gameManager.getProgreso()
});
}
}
lang-javascript
Este archivo recibirá varias actualizaciones conforme avancemos en el plan del juego.
GameManager.js
Aparece un nuevo elemento: js/helpers/GameManager.js, este archivo se encargara de gestionar los niveles del juego, donde se encuentra actualmente el jugador y a donde va:
class GameManager {
constructor() {
// Contar dinámicamente los niveles disponibles
this.maxNivel = this.contarNiveles();
}
/** * Contar cuántos niveles hay en el objeto niveles */contarNiveles() {
return Object.keys(niveles).length;
}
/** * Obtener el nombre del nivel actual */getNivelActual() {
return `nivel${nivelActual}`;
}
/** * Avanzar al siguiente nivel */siguienteNivel() {
if (nivelActual < this.maxNivel) {
nivelActual++;
return true; // Hay siguiente nivel
}
return false; // No hay más niveles
}
/** * Reiniciar al nivel actual */reiniciarNivel() {
// El nivel actual se mantiene igual
return this.getNivelActual();
}
/** * Verificar si hay un siguiente nivel */hayMasNiveles() {
return nivelActual < this.maxNivel;
}
/** * Obtener información del progreso */getProgreso() {
return {
nivelActual: nivelActual,
maxNivel: this.maxNivel,
progreso: `${nivelActual}/${this.maxNivel}`
};
}
/** * Reiniciar el juego desde el primer nivel */reiniciarJuego() {
nivelActual = 1;
return this.getNivelActual();
}
/** * Verificar si el nivel existe */existeNivel(nombreNivel) {
return niveles.hasOwnProperty(nombreNivel);
}
}
lang-javascript
LevelLoader.js
La pieza crítica de nuestros cambios, ya que se encargará precisamente de traducir los niveles que escribimos en ASCII: js/helpers/LevelLoader.js
class LevelLoader {
constructor(scene) {
this.scene = scene;
this.tileSize = 16;
}
crearNivelDesdeASCII(levelMap) {
// Grupos para organizar los elementos
this.scene.tiles = this.scene.physics.add.staticGroup();
enemigos = this.scene.physics.add.group();
// Grupos para objetos coleccionables
this.scene.monedas = this.scene.physics.add.staticGroup();
for (let fila = 0; fila < levelMap.length; fila++) {
for (let col = 0; col < levelMap[fila].length; col++) {
const char = levelMap[fila][col];
const x = col * this.tileSize + this.tileSize / 2;
const y = fila * this.tileSize + this.tileSize / 2;
switch (char) {
case '#':
// Pared - frame 1 (tile de pared)
const pared = this.scene.add.image(x, y, 'roof_wall_tiles', 1);
this.scene.tiles.add(pared);
break;
case 'E':
case 'e':
// Enemigo
let dir = 'horizontal';
if (char === 'e')
dir = 'vertical';
const enemigo = newEnemigo(this.scene, x, y, dir);
enemigos.add(enemigo);
break;
case 'D':
// Puerta
puerta = this.scene.physics.add.staticImage(x, y, 'puerta');
puerta.setScale(ESCALAS.PUERTA);
break;
case 'C':
// Moneda
const moneda = this.scene.physics.add.staticImage(x, y, 'moneda');
moneda.setScale(ESCALAS.OBJETOS);
this.scene.monedas.add(moneda);
break;
case 'P':
// Jugador
player = newPlayer(this.scene, x, y);
break;
case ' ':
// Espacio vacío - no hacer nada
break;
}
}
}
// Configurar colisiones
this.scene.physics.add.collider(player, this.scene.tiles);
this.scene.physics.add.collider(enemigos, this.scene.tiles);
this.scene.physics.add.overlap(player, enemigos, this.scene.perder, null, this.scene);
this.scene.physics.add.overlap(player, puerta, this.scene.ganar, null, this.scene);
}
cargarNivel(nombreNivel) {
// Usar la variable global 'niveles' del archivo ASCII.js
return niveles[nombreNivel] || null;
}
}
lang-javascript
Aunque parezca complejo, realmente no lo es. Simplemente se encarga de recorrer cada uno de nuestros niveles ASCII y remplazar las claves con un elemento (Ejemplo: P por el sprite del jugador).
Simple pero útil, porque nos permitirá enfocarnos en los niveles de una forma práctica y rápida, y podremos hacer prototipos, probarlos, y cambiarlos sin mayor complicación.
Finalmente: ASCII.js
En js/niveles/ASCII.js definiremos cada nivel, donde se cargan los elementos y que estructura tienen:
const niveles = {
'nivel1': [
"########################################",
"# # #",
"# # e #",
"# C # #",
"# # D #",
"# # #",
"# # #",
"# # #",
"# # #",
"# # C #",
"# # #",
"# # #",
"############### ################",
"# #",
"# #",
"# E #",
"# #",
"# #",
"############### ################",
"# #",
"# #",
"# #",
"# #",
"# #",
"# ## #",
"# ## #",
"# P ## C #",
"# ## #",
"# ## #",
"########################################"
],
'nivel2': [
"########################################",
"#P #",
"# ### E ### #",
"# ### ### #",
"# ### #",
"# C ### C #",
"# ### #",
"# ### ### #",
"# ### E ### #",
"# #",
"# e e #",
"# #",
"# ### ### #",
"# ### E ### #",
"# ### #",
"# ### #",
"# ### #",
"# ### ### #",
"# ### E ### #",
"# #",
"# e e #",
"# #",
"# ### ### #",
"# ### ### #",
"# ### #",
"# C ### C #",
"# ### #",
"# D #",
"# #",
"########################################"
],
};
lang-javascript
Para agregar un nuevo nivel, simplemente copiamos uno de los anteriores como un nuevo elemento del arreglo, y seguimos la secuencia "nivel1", "nivel2", "nivel3".
Próximos pasos
Por acá te dejo un vistazo de cómo se verá el juego cuando terminemos la serie, el objetivo final será introducir vidas, puntos, distintas escenas, diferentes niveles, en fin. Captura de pantalla_20250723_132249.png70.5 KBMe gustaría ver como van tus juegos, que te parece el tutorial, y si hay algo que te gustaría cambiar. Puedes dejarme tu comentario acá, o en nuestras redes. Y muchas gracias por seguirme hasta ahora!
Senior Software Engineer, consultor en ciberseguridad y escritor técnico. Con más de 20 años en tecnología, reflexiona sobre el impacto humano del software, la inteligencia artificial y la atención digital. Es fundador de Greyhat y comparte sus pensamientos desde la experiencia, la terminal y la introspección.
0 comentario
Aún no hay comentarios. ¡Sé el primero en comentar!