This is the fourth post in the Building a Roguelike in Javascript series. I recommend you start at the beginning unless you've been following along. This part corresponds to the map scrolling segment of the third part in Trystan's series. All the code for this part can be found at the part 3b tag of the jsrogue repository. At the time of writing, I am still using the d4ea2ab commit for rot.js.

Just to quickly recap last post we created a cave and made it show up in our Play screen. This works really nicely and makes random caves every time we refresh the game, but we'd like to be able to have levels that are bigger than the screen! This is usually done by centering the screen on our player and only showing the nearby area! We don't have the notion of a player yet, and that will only be really implemented in the next post, but for now we'll want to center the screen at a given x and y position which can be moved around using the keys! By the end of this post we'll be generating huge caves and we'll be able to move around them and explore them!

Demo Link

The results after this post can be seen here

assets/game.js

Before we start, we're going to do a small bit of refactoring. Rather than hardcoding our screen size, we're going to store it as variables in the Game namespace which can be accessed globally. We're going to keep track of how wide and tall our screen is in cells:

1
2
3
4
5
6
7
var Game =  {
    _display: null,
    _currentScreen: null,
    _screenWidth: 80,
    _screenHeight: 24,
    //...
}

Now we have to modify our init function to use these variables rather than the hardcoded values:

1
2
3
4
5
6
7
8
init: function() {
    // Any necessary initialization will go here.
    this._display = new ROT.Display({width: this._screenWidth,
                                     height: this._screenHeight});
    // Create a helper function for binding to an event
    // and making it send it to the screen
    // ...
}

Finally, we want to create globally accessible functions, similar to getDisplay, which will allow other files (eg. inside our Screen objects) to get the width and height:

1
2
3
4
5
6
7
8
9
getDisplay: function() {
    return this._display;
},
getScreenWidth: function() {
    return this._screenWidth;
},
getScreenHeight: function() {
    return this._screenHeight;
},

This is a great little change to put in place early, and if we are consistent in using these functions elsewhere in our code, will allow us to change our screen size (think mobile!) without any headaches.

assets/screens.js

The first thing we want to do is create big maps! Let's change our map generating code in the PlayScreen's enter function so that we generate maps that are 500 cells wide and 500 cells tall:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Game.Screen.playScreen = {
    _map: null,
    enter: function() {  
        var map = [];
        // Create a map based on our size parameters
        var mapWidth = 500;
        var mapHeight = 500;
        for (var x = 0; x < mapWidth; x++) {
            // Create the nested array for the y values
            map.push([]);
            // Add all the tiles
            for (var y = 0; y < mapHeight; y++) {
                map[x].push(Game.Tile.nullTile);
            }
        }
        // Setup the map generator
        var generator = new ROT.Map.Cellular(mapWidth, mapHeight);
        generator.randomize(0.5);
        var totalIterations = 3;
        // Iteratively smoothen the map
        for (var i = 0; i < totalIterations - 1; i++) {
            generator.create();
        }
        // Smoothen it one last time and then update our map
        generator.create(function(x,y,v) {
            if (v === 1) {
                map[x][y] = Game.Tile.floorTile;
            } else {
                map[x][y] = Game.Tile.wallTile;
            }
        });
        // Create our map from the tiles
        this._map = new Game.Map(map);
    },
    //...
}

This will now generate big maps for us! However we can't render it all because it's bigger than the screen, so we're going to make it so that we only see part of the screen at once. As I mentioned above, we will keep track of where our cursor currently is on the screen using x and y coordinates, so we must add these as variables of the screen. Initially we are going to start at the top left corner of the map:

1
2
3
4
5
6
Game.Screen.playScreen = {
    _map: null,
    _centerX: 0,
    _centerY: 0,
    // ...
}

Now we'll want to be able to move our player in any direction. So for now, to keep things simple, we'll create a function allowing us to move in the x and y direction by a certain amount of cells. For example, if we want to move down by 2, it would be move(0, 2). If we want to move left by 1, it would be move(-1, 0). We also want to make sure the player stays in the bounds of the map, so we use Math.min and Math.max to make sure it says between 0 and the map width / height. Note that we have to subtract 1 from the map's width and height to get the real index of the last cell as arrays are 0-based.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Game.Screen.playScreen = {
    // ...
    move: function(dX, dY) {
        // Positive dX means movement right
        // negative means movement left
        // 0 means none
        this._centerX = Math.max(0,
            Math.min(this._map.getWidth() - 1, this._centerX + dX));
        // Positive dY means movement down
        // negative means movement up
        // 0 means none
        this._centerY = Math.max(0,
            Math.min(this._map.getHeight() - 1, this._centerY + dY));
    }
}

Now we're going to update our handleInput function so that we can actually move our player around using the arrow keys! As a small challenge, you should try to re-do this code so that it uses your preferred method of movement (WASD, HJKL, using the numpad, etc.). Remember that you can see all the key constants here (they all start with VK_).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Game.Screen.playScreen = {
    // ...
    handleInput: function(inputType, inputData) {
        if (inputType === 'keydown') {
            // If enter is pressed, go to the win screen
            // If escape is pressed, go to lose screen
            if (inputData.keyCode === ROT.VK_RETURN) {
                Game.switchScreen(Game.Screen.winScreen);
            } else if (inputData.keyCode === ROT.VK_ESCAPE) {
                Game.switchScreen(Game.Screen.loseScreen);
            }
            // Movement
            if (inputData.keyCode === ROT.VK_LEFT) {
                this.move(-1, 0);
            } else if (inputData.keyCode === ROT.VK_RIGHT) {
                this.move(1, 0);
            } else if (inputData.keyCode === ROT.VK_UP) {
                this.move(0, -1);
            } else if (inputData.keyCode === ROT.VK_DOWN) {
                this.move(0, 1);
            }
        }    
    },
    // ...
}

So now we have a cursor that can move around the screen! Great! But we haven't changed our rendering yet, so nothing actually looks different! We're going to rewrite our render function so that it matches our new scrolling system! The first thing we have to do is determine what cells to render. We already know how many cells wide and tall we need to render thanks to our refactoring we did at the beginning, but what cell will go in the left corner? A pretty common strategy for this is to center the screen based on where the player is. So we want our top corner to be one half of a screen's width to the left and one half of a screen's height up from our center x and y. There's also an extra consideration - what do we do when the player is less than half a screen's width away from the bound, for example if they are at x = 5 and the screen's width is 80? What we're going to do is stop scrolling if we can't fit half a screen on all sides of the player. So for the left and top bound, we want to make sure our top left corner is at a position of at least x = 0 and y = 0 (can't have negative positions!). For our right and bottom bound, we want to make sure our top left corner can still fit the entire screen. So let's write some code for determining the top left corner first! Note that because we are working with halfs of a screen, it's generally a good idea to make your screen width and height even numbers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Game.Screen.playScreen = {
    // ...
    render: function(display) {
        var screenWidth = Game.getScreenWidth();
        var screenHeight = Game.getScreenHeight();
        // Make sure the x-axis doesn't go to the left of the left bound
        var topLeftX = Math.max(0, this._centerX - (screenWidth / 2));
        // Make sure we still have enough space to fit an entire game screen
        topLeftX = Math.min(topLeftX, this._map.getWidth() - screenWidth);
        // Make sure the y-axis doesn't above the top bound
        var topLeftY = Math.max(0, this._centerY - (screenHeight / 2));
        // Make sure we still have enough space to fit an entire game screen
        topLeftY = Math.min(topLeftY, this._map.getHeight() - screenHeight);
    }
    // ...
}

Now that we've figured out our top left cell, we can render the cells like we were doing before! We have to change the bounds of our for nested for loops which traverse the map so that we start at our top left cell and render one screen. We also have to make it so that when we draw a tile, we draw it relative to the top left cell (ie. render it at x - topLeftX rather than x).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Iterate through all visible map cells
for (var x = topLeftX; x < topLeftX + screenWidth; x++) {
    for (var y = topLeftY; y < topLeftY + screenHeight; y++) {
        // Fetch the glyph for the tile and render it to the screen
        // at the offset position.
        var glyph = this._map.getTile(x, y).getGlyph();
        display.draw(
            x - topLeftX,
            y - topLeftY,
            glyph.getChar(), 
            glyph.getForeground(), 
            glyph.getBackground());
    }
}

Finally we can scroll around our map! However while we're modifying our rendering function, let's add a call after we render the map to render our cursor (represented by @). We have to offset the rendering position as we want to know where to render our cursor relative to the top left cell and not the 0,0 cell! Our render function should know look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Game.Screen.playScreen = {
    // ...
    render: function(display) {
        var screenWidth = Game.getScreenWidth();
        var screenHeight = Game.getScreenHeight();
        // Make sure the x-axis doesn't go to the left of the left bound
        var topLeftX = Math.max(0, this._centerX - (screenWidth / 2));
        // Make sure we still have enough space to fit an entire game screen
        topLeftX = Math.min(topLeftX, this._map.getWidth() - screenWidth);
        // Make sure the y-axis doesn't above the top bound
        var topLeftY = Math.max(0, this._centerY - (screenHeight / 2));
        // Make sure we still have enough space to fit an entire game screen
        topLeftY = Math.min(topLeftY, this._map.getHeight() - screenHeight);
        // Iterate through all visible map cells
        for (var x = topLeftX; x < topLeftX + screenWidth; x++) {
            for (var y = topLeftY; y < topLeftY + screenHeight; y++) {
                // Fetch the glyph for the tile and render it to the screen
                // at the offset position.
                var glyph = this._map.getTile(x, y).getGlyph();
                display.draw(
                    x - topLeftX,
                    y - topLeftY,
                    glyph.getChar(), 
                    glyph.getForeground(), 
                    glyph.getBackground());
            }
        }
        // Render the cursor
        display.draw(
            this._centerX - topLeftX, 
            this._centerY - topLeftY,
            '@',
            'white',
            'black');
    },
    // ...
}

Now let's refresh our page! Look at that, there's our cursor in the top left! Now le's try to move it around... uh oh! It's not moving!

assets/game.js

You may recall from the Screen Management entry that the handleInput function gets called whenever an input event is received. However after we handle input, we don't actually re-render the screen! So even though our cursor is moving we don't actually see it moving! Let's rewrite our Game.init function to make sure we call render after we handle the input. Note that I've also commented out the keyup and keypress handlers for now as we're not using them.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var Game = {
    // ...
    init: function() {
        // Any necessary initialization will go here.
        this._display = new ROT.Display({width: this._screenWidth,
                                         height: this._screenHeight});
        // Create a helper function for binding to an event
        // and making it send it to the screen
        var game = this; // So that we don't lose this
        var bindEventToScreen = function(event) {
            window.addEventListener(event, function(e) {
                // When an event is received, send it to the
                // screen if there is one
                if (game._currentScreen !== null) {
                    // Send the event type and data to the screen
                    game._currentScreen.handleInput(event, e);
                    // Clear the screen
                    game._display.clear();
                    // Render the screen
                    game._currentScreen.render(game._display);
                }
            });
        }
        // Bind keyboard input events
        bindEventToScreen('keydown');
        //bindEventToScreen('keyup');
        //bindEventToScreen('keypress');
    },
    // ...
}

Note that we may refactor this code at some point in the future to only re-render the screen when it's required, but for now this will do!

Conclusion

We now have a cursor that lets us explore the caves! The next post will focus on converting this cursor to an actual player, and will be a big step up for our game. Play around with it, generating random caves and resizing the map as well.

Remember that all the code for this part can be found at the part 3b tag of the jsrogue repository. I hope you enjoyed this post and that you'll stick around for the next part!

Thanks for reading,

Dominic

Extra

In the last post, I mentioned there were a variety of map generators provided in rot.js. I just tought I would give you an example of using a different one if you wanted to play around with it. This little extra section will be changing the map generator to use ROT.Map.Uniform which generate maps more similar to games like Nethack. Note that for now the actual posts will be using the ROT.Map.Cellular generator. This is simply for fun!

In our assets/screens.js file in the enter function we used to generate our map like so:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enter: function() {
    // Create a map based on our size parameters
    var mapWidth = 500;
    var mapHeight = 500;
    // ...
    // Setup the map generator
    var generator = new ROT.Map.Cellular(mapWidth, mapHeight);
    generator.randomize(0.5);
    var totalIterations = 3;
    // Iteratively smoothen the map
    for (var i = 0; i < totalIterations - 1; i++) {
        generator.create();
    }
    // Smoothen it one last time and then update our map
    generator.create(function(x,y,v) {
        if (v === 1) {
            map[x][y] = Game.Tile.floorTile;
        } else {
            map[x][y] = Game.Tile.wallTile;
        }
    });
    // ...
}

We're now going to change this code! The first thing we are going to do is make the map smaller, as the generation tends to take much longer:

1
2
    var mapWidth = 250;
    var mapHeight = 250;

The ROT.Map.Uniform generator is much simpler to use and only requires 1 iteration of create! We also pass a time limit in the options, as by default if the generation takes more than 1000 milliseconds it will simply generate null, so we increase it to 5000 milliseconds. Another change is that with this generator, 1 represents a wall and 0 represents a floor. We can then modify our generator code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Setup the map generator
var generator = new ROT.Map.Uniform(mapWidth, mapHeight, 
    {timeLimit: 5000});
// Smoothen it one last time and then update our map
generator.create(function(x,y,v) {
    if (v === 0) {
        map[x][y] = Game.Tile.floorTile;
    } else {
        map[x][y] = Game.Tile.wallTile;
    }
});

And there you have it! The map now looks more like a traditional roguelike:

Note that the code for this extra is not up on the Github, but if you have any questions feel free to post in the comments!

Next Part

Part 4 - A Hero Appears!