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.png 105 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.png 1.36 KB
Vamos al código!


Nuevos Archivos



constants.js


En primer lugar tenemos js/constants.js

/**
 * CONSTANTES DEL JUEGO
 *
 * Valores reutilizables para mantener consistencia
 * y facilitar cambios globales
 */

// Escalas de sprites
const ESCALAS = {
    JUGADOR: 3,
    ENEMIGO: 3,
    OBJETOS: 2,
    PUERTA: 3
};

// Assets del juego organizados
const ASSETS = {
    SPRITES: {
        'jugador': 'assets/greenie.png',
        'enemigo': 'assets/reddie.png',
        'puerta': 'assets/door.png',
        'moneda': 'assets/coin.png',
    },
    SPRITESHEETS: {
        'roof_wall_tiles': {
            src: 'assets/roof_wall.png',
            frameWidth: 16,
            frameHeight: 32
        }
    }
};

// Configuración de gameplay
const GAMEPLAY = {
    VELOCIDAD_JUGADOR: 200,
    VELOCIDAD_ENEMIGO: 100,
    PUNTOS: {
        MONEDA: 100,
    }
};
lang-javascript
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 = new LevelLoader(this);
        this.gameManager = new GameManager();
    }

    /**
     * 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 = new Enemigo(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 = new Player(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.png 70.5 KB
Me 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!

Foto de orva studio en Unsplash