Create a Maze Engine in HTML5

In this tutorial, you will learn how to create a maze-like navigation system which you could apply to “point and click”, graphical adventures games, and more innovative genres as well. This system was used in my game Me Mnemonic, an HTML5 action memory game for Android, Firefox OS devices and web. At the end of this tutorial, you will be able to create any maze structure, with just few lines of codes, and navigate through it with keyboard arrows, mouse click or touch.

Requirements

You should be familiar with HTML, CSS, JavaScript and basic object oriented concepts. I will be using limeJS, which is the one most used game frameworks. Please check the documentation on their
web site and install it.

What to expect

You can download the game files here. The root folder contains the non-compiled files, which you can open and read. You’ll need limeJS installed if you want to run them. There is also compiled version, in the “compiled” folder, which you can run standalone in your web browser (“compiled” in the limeJS lingo really means “minified” so that all the dependencies are in a single JavaScript file). Now we will browse through the files and I will explain how this all works.

How it works

Here is the list of files that we will be using:

  • direction.js
  • Level.js
  • Level_1.js
  • maze.css
  • maze.html
  • maze.js
  • Room.js

and a short explanation:

  1. maze.html is a game view, run this to see the maze in action
  2. maze.js is the starting point of the game, something like the main controller. There, we will create the Director, who’s job is to manage the game screens. It will load our maze screen.
  3. The maze screen is created by the class maze.Level_1. All you have to do is populate the array this.rooms, in the constructor, with maze.Room objects.
  4. The rest will be done by it’s parent class maze.Level. Based on the list of rooms, in the array this.rooms, it will create a maze and set up navigation events/controls.

That’s it.

Let’s go deeper

maze.js

maze.start = function() {
    // create game director to manage screens
    var director = new lime.Director(document.getElementById('maze'), maze.size.width, maze.size.height);
    // create maze first level
    var level = new maze.Level_1(); // in constructor you set up the maze rooms, see maze.Level_1 class
    level.create(); // we start the engine to build the maze
    director.replaceScene(level); // show it!
    director.setDisplayFPS(false);
}

Nothing special here, just create and show the maze.

maze.Level_1.js

/** 
 * @constructor
 * @extends {maze.Level}
 */
maze.Level_1 = function() {
    goog.base(this);
    this.rooms = [
        new maze.Room([maze.direction.UP, maze.direction.RIGHT], [0, 0]),
        new maze.Room([maze.direction.RIGHT, maze.direction.LEFT], [0, 1]),
        new maze.Room([maze.direction.LEFT], [0, 2]),
        new maze.Room([maze.direction.UP, maze.direction.DOWN], [1, 0]),
        new maze.Room([maze.direction.RIGHT, maze.direction.DOWN], [2, 0]),
        new maze.Room([maze.direction.UP, maze.direction.LEFT], [2, 1]),
        new maze.Room([maze.direction.RIGHT, maze.direction.DOWN], [3, 1]),
        new maze.Room([maze.direction.UP, maze.direction.LEFT], [3, 2]),
        new maze.Room([maze.direction.UP, maze.direction.DOWN], [4, 2]),
        new maze.Room([maze.direction.RIGHT], [5, 0]),
        new maze.Room([maze.direction.RIGHT, maze.direction.LEFT], [5, 1]),
        new maze.Room([maze.direction.DOWN, maze.direction.LEFT], [5, 2])
    ];
}

This is the maze setup. The maze is a set of “rooms”, where we see only one at the time (see demo). Every maze.Room object has to know where is the exit (to up, right, down or left) to other rooms (first parameter array) and its position in the maze (second parameter array).

Room directions

For direction, we use enum maze.direction

/** 
 * Constants for direction
 * @enum {string}
 */
maze.direction = {
    UP: 'up',
    RIGHT: 'right',
    DOWN: 'down',
    LEFT: 'left'
}

In order for this to work, you have to use directions always in the same order: up, right, down or left. So [maze.direction.LEFT, maze.direction.DOWN] is not going to work, but [maze.direction.DOWN, maze.direction.LEFT] is OK. I like to use enums like this because then you make less errors while typing.

Rooms positions

Here is the maze map, the letter Z:

map

To create this structure in the code, we use an array with 2 dimensions for the room position – the first index is for row, and the second for column. It’s like we are building the table with rows and columns, from bottom to top:

  • [0, 0] means 1st row, 1st column,
  • [0, 1] means 1st row, 2nd column,
  • [0, 2] means 1st row, 3rd column,
  • [1, 0] means 2nd row, 1st column,

Directions and positions

Putting it all together:
new maze.Room([maze.direction.UP, maze.direction.RIGHT], [0, 0]) means that this room has 2 directions, to UP and RIGHT, and its position is 1st row, 1st column. This is the maze starting room, the bottom left room. By default, the starting room is always the first item in this.rooms array.

The engine

Class maze.Level will, based on it’s subclass maze.Level_1, create the maze. maze.Level_1 is just a setup or configuration, but maze.Level is the engine. Let see some of the methods:

/**
 * Create maze.
 */ 
maze.Level.prototype.create = function() {
    /** @type {maze.Room} */
    var room;
    for (var i = 0; i < this.rooms.length; i++) {
        room = this.rooms[i];
        // set room image and move room outside of the screen
        room.setFill(room.getImage()).setPosition(-1000, 0).setSize(480, 320).setAnchorPoint(0, 0);
        if (i == 0) {
            // set first room as starting point
            room.setPosition(0, 0);
            maze.Level.currentRoom = room;
        }
        // set room neighbours, so we can know which room to show based on direction
        room.setNeighbours(this.getNeighbours(room, i));
        this.appendChild(room);
    }
    // add click/touch navigation
    this.setTouchNav();
}

This is the place where maze is created. It loops through all rooms and add them to the screen.

/**
 * A flag which tells whether room animation is in progress.
 * We need this to NOT interrupt moving to another room.
 * @type {bool}
 */
maze.Level.moving = false;

/**
 * Move to another room based on direction.
 * @param {maze.direction} direction
 */
maze.Level.prototype.move = function(direction) {
    // check if room has this direction and if animation is in progress
    if (goog.array.contains(maze.Level.currentRoom.directions, direction) === false || maze.Level.moving === true) {
        return;
    }
    var nextRoom = maze.Level.currentRoom.neighbours[direction];
    /** @type {goog.math.Coordinate} */
    var coordinate;
    // based on direction, set next room starting position and coordinates where should animation end
    switch(direction) {
        case maze.direction.LEFT:
            nextRoom.setPosition(-maze.size.width, 0);
            coordinate = new goog.math.Coordinate(maze.size.width, 0);
            break;
        case maze.direction.RIGHT:
            nextRoom.setPosition(maze.size.width, 0);
            coordinate = new goog.math.Coordinate(-maze.size.width, 0);
            break;
        case maze.direction.UP:
            nextRoom.setPosition(0, -maze.size.height);
            coordinate = new goog.math.Coordinate(0, maze.size.height);
            break;
        case maze.direction.DOWN:
            nextRoom.setPosition(0, maze.size.height);
            coordinate = new goog.math.Coordinate(0, -maze.size.height);
            break;
    }
    // move both, next and current, rooms 
    // we can see current room is going away and next room is coming
    var moveAction = new lime.animation.MoveBy(coordinate).setDuration(0.5);
    moveAction.addTarget(maze.Level.currentRoom);
    moveAction.addTarget(nextRoom);
    moveAction.play();
    // we don't want to interrupt current animation
    maze.Level.moving = true;
    // set event to know when animation is finished
    // so we can set next room to current and maze.Level.moving that animation is finished
    goog.events.listen(
        moveAction, 
        lime.animation.Event.STOP, function(){
            maze.Level.moving = false;
            maze.Level.currentRoom = nextRoom;
        });
}

This method is responsible for the moving animation. Based on room neighbours, it will set next room and move it along with the current one, so we get a felling that we are moving through the maze.

Where to go now?

Now you can try to make your own maze. Create maze.Level_2 and populate this.rooms array with maze.Room. Ok, that’s fine, now we know how to create maze, but this is not a game yet. We are missing the game logic. Maybe in a future, we can add some bad guys, monsters or any other obstacle to beat and play.

If you have any questions, you can contact me on Twitter: @bmilakovic

Other LimeJS tutorials at the GameDev Academy: