Do you like my tutorials?

Then consider supporting me on Ko-fi

Talking about Yanga game, Game development, HTML5, Javascript and Phaser.

If you love videogames, you don’t care which platform they are developed for. You just play great games, everywhere.

So I am introducing you Yanga, a game made by Serdjuk for the ZX Spectrum. Yes, the ZX Spectrum, the 8-bit personal home computer released in 1982, 39 years ago.

You should definitively play it, as it’s an interesting mix between Sokoban, a match 3 game and an action puzzle game.

This short GIF video represents level 1.

I am showing you how to draw a Yanga level, starting from a Tiled map array filled with objects. In this case, just like with Totem Destroyer prototype, I preferred to use objects rather than tiles because they allow more room for customization.

This is level 2:

And this is the Tiled scheme of the level:

As you can see, I did not draw walls tile by tile, but with bigger objects. Basically because I am lazy.

And this is my version of the level:

There is no interaction or input management at the moment, but there are the glowing runes and walls, both player and exit have their animations, and wall tiles are randomly placed, rotated, flipped and colored.

The orginal game has not been decompiled. You should never decompile games, the real challenge is the rebuild it on your own. I used the original graphics just to let you see how the prototype looks like the original game.

Now, the completely commented source code:

let game;

let gameOptions = {

    // tile size in Tiled map
    tiledSize: 32,

    // tile size in the game
    actualSize: 64,

    // wall colors
    wallColors: [0xcecb00, 0x00fb00, 0x00cbce],

    // rune colors
    runeColors: [0xff0000, 0x0000ff],

    // player color
    playerColor: 0xcecbce,

    // door colors
    doorColors: [0xff0000, 0x00fb00]
}

window.onload = function() {
    let gameConfig = {
        type: Phaser.AUTO,
        scale: {
            mode: Phaser.Scale.FIT,
            autoCenter: Phaser.Scale.CENTER_BOTH,
            parent: "thegame",
            width: 960,
            height: 640
        },
        scene: playGame
    }
    game = new Phaser.Game(gameConfig);
    window.focus();
}

class playGame extends Phaser.Scene {

    constructor() {
        super("PlayGame");
    }

    preload() {

        // load tiled map
        this.load.tilemapTiledJSON("level", "level.json");

        // player sprites
        this.load.spritesheet("player", "player.png", {
            frameWidth: gameOptions.actualSize,
            frameHeight: gameOptions.actualSize
        });

        // door sprites
        this.load.spritesheet("door", "door.png", {
            frameWidth: gameOptions.actualSize,
            frameHeight: gameOptions.actualSize
        });

        // wall tiles
        this.load.spritesheet("tiles", "tiles.png", {
            frameWidth: gameOptions.actualSize,
            frameHeight: gameOptions.actualSize
        });
    }

    create() {

        // add the tilemap
        let map = this.add.tilemap("level");

        // array to store runes
        this.runeWallArray = [];

        // player animation
        this.anims.create({
            key: "playerAnim",
            frames: this.anims.generateFrameNumbers("player", {
                frames: [0, 1, 0, 2]
            }),
            frameRate: 8,
            repeat: -1
        });

        // door animation
        this.anims.create({
            key: "doorAnim",
            frames: this.anims.generateFrameNumbers("door", {
                frames: [0, 2, 1, 3, 1, 2, 0]
            }),
            frameRate: 8,
            repeat: -1
        });

        // select all objects in Object Layer zero, the first - and only, at the moment - level
        let blocks = map.objects[0].objects;

        // looping through all blocks and execute addBlock method
        blocks.forEach (blocks => this.addBlock(blocks));

        // give some kind of glow effect to runes
        this.tweens.add({
            targets: this.runeWallArray,
            alpha: 0.8,
            duration: 100,
            yoyo: true,
            repeat: -1
        });
    }

    // method to add a level block
    addBlock(block) {

        // get block x and y position
        let blockX = block.x / gameOptions.tiledSize * gameOptions.actualSize + gameOptions.actualSize / 2;
        let blockY = block.y / gameOptions.tiledSize * gameOptions.actualSize + gameOptions.actualSize / 2;

        // check block type
        switch (block.type) {

            // wall
            case "Wall":

                // get wall width and height
                let wallWidth = block.width / gameOptions.tiledSize;
                let wallHeight = block.height / gameOptions.tiledSize;

                // place the tiles
                for (let i = 0; i < wallWidth; i ++) {
                    for (let j = 0; j < wallHeight; j ++) {

                        // place tile sprite
                        let tile = this.add.sprite(blockX + i * gameOptions.actualSize, blockY + j * gameOptions.actualSize, "tiles", Phaser.Math.RND.integerInRange(0, 3));

                        // give randomness to tile
                        tile.setFrame(Phaser.Math.RND.integerInRange(0, 3));
                        tile.angle = Phaser.Math.RND.integerInRange(0, 3) * 90;
                        tile.flipX = Phaser.Math.RND.integerInRange(0, 1) == 1;
                        tile.flipY = Phaser.Math.RND.integerInRange(0, 1) == 1;
                        tile.setTint(Phaser.Math.RND.pick(gameOptions.wallColors));
                    }
                }
                break;

            // rune
            case "Rune":
                let runeValue = block.properties[0].value;
                let rune = this.add.sprite(blockX, blockY, "tiles", 4 + 2 * runeValue);
                rune.setTint(gameOptions.runeColors[runeValue])
                break;

            // rune wall
            case "Rune Wall":
                let wallValue = block.properties[0].value;
                let runeWall = this.add.sprite(blockX, blockY, "tiles", 5 + 2 * wallValue);
                runeWall.setTint(gameOptions.runeColors[wallValue]);
                this.runeWallArray.push(runeWall);
                break;

            // player
            case "Player":
                let player = this.add.sprite(blockX, blockY, "player");
                player.play("playerAnim");
                player.setTint(gameOptions.playerColor);
                break;

            // entrance door
            case "Entrance":
                let entrance = this.add.sprite(blockX, blockY, "door");
                entrance.setTint(gameOptions.doorColors[0]);
                break;

            // exit door
            case "Exit":
                let exit = this.add.sprite(blockX, blockY, "door");
                exit.setTint(gameOptions.doorColors[1]);
                exit.play("doorAnim");
                break;
        }
    }
}

Next time I am going to add game logic and input management, then you can start building your Yanga-like game. Download the source code, Tiled project included.

And don’t forget to have a look at the original game.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.