RogueTS: Building A Roguelike in TypeScript

RogueTS is a simple Roguelike game engine which was ported over from an AS3 project I started a few years back called F*Rogue. I wrote F*Rogue so I could gain a better understanding of how Roguelikes work and explore random level generation. In this post I am going to talk about the game engine’s main class and how to get it up and running so you can build your own HTML5 Roguelike game on top of it. It’s important to note that RogueTS is still unfinished and is more of a proof of concept at this point. Over time I will continue to work on the project until I can actually build my own Roguelike game with it.

At its core, RogueTS has four main areas:

  • Game class which handles the game loop and setting up all of the classes needed to run the game.

  • Map classes which handle abstracting map data, ability to select regions of the map to render, utilities for populating the map and a simple ray trace engine for line of sight calculations.

  • Renderers which handles visualizing the map and player.

  • Controls which handle moving the player and offer up a starting point for capturing user input.

F*Rogue was originally created to be extendable and I was able to maintain the same architectural approach I had taken with ActionScript in TypeScript. For the most part, RogueTS is a direct port of the AS3 code with the exception of a few classes I decided to clean up to make them easier to use. You can read more about how I ported over the code here. The core game classes are typed to interfaces and you will find most classes have been abstracted in a way which will allow you to easily swap out or extend them. The best example of this is the MapSelection and the FogOfWarMapSelection which I will talk about in more detail in a later post. For this post, let’s take a look at the core game and how it runs.

Game Class

The main class of RogueTS is the Game class. You can create a new RogueTS game by passing a Canvas element into the constructor of the like so:

var canvas = <htmlcanvaselement>document.getElementById('display');
var rogueTS = new rogue.Game(canvas);
</htmlcanvaselement>

Our reference to a canvas element will allow us to properly render the game. For the TypeScript version of the engine I opted to pass in the Canvas element RogueTS should use as its main display which follows a similar design pattern as the sample app you get when you start a new TypeScript project. Likewise you can write your own renderer to visualize the game however you want. I think in future versions I may change this have you pass in a renderer to the constructor so you can open up the possibility of not having to rely on canvas. This would be helpful if you wanted to do use WebGL or even have a text based renderer that outputs the display to a div’s innerHTML.

Inside of the game class you will find all of the core classes needed to run the game. Here is a list and quick summary of each. I will go over them in more detail later on:

  • Renderer – this is the default renderer the game uses to display its graphics.

  • Map is a reference to the tile map and offers up convent APIs around getting tiles, seeing their values and the width/height of the entire map.

  • Selection allows you to get a range of tiles around a center point, which is usually the player’s position, and is mostly used to limit the amount of tiles fed into the renderer to increase performance.

  • Input – represents the current input listeners and manages interaction by the player to the game.

  • Movement Helper – this is a proxy to help manage input from the player, via the Input class, and update the player’s position.

  • Map Populator – this handles populating the map with tiles, usually representing monsters, treasures or obstacles, and can query the map for open tiles and locations where things should be placed.

Outside of these important classes, the game contains basic logic to set up the game loop. In AS3 I used EnterFame but in JavaScript games we will need to take advantage of the requestAnimationFrame() API in modern browsers. Here is an example of how I set this up:

onEnterFrame() {
    // Invalidate the display
    this.invalidate();
 
    // Begin rendering the display
    var that = this;
    var _cb = function () { that.render(); requestAnimationFrame(_cb); }
    _cb(that);
}

You may notice I am still using onEnterFrame as the function name which is a throwback to my AS3 port. In essence though, this will now act exactly like it did in Flash and will call render() on each frame render. I’ll talk about the call to invalidate() later on.

The render() function is the main game loop and is responsible for handling the update and render logic.

render(instance: Game) {
 
	if (this.input.state) {
		console.log("state", this.input.state);
                this.move(this.input.state);
	}
 
	if (instance.invalid) {
		instance.renderer.renderMap(instance.selection); //fogOfWarSelection);
 
                var pos: geom.Point = this.movementHelper.playerPosition;
                var x: number = pos.x - this.selection.getOffsetX();
                var y: number = pos.y - this.selection.getOffsetY();
 
                this.renderer.renderPlayer(x, y, "@");
 
                instance.invalid = false;
	}
 
	this.input.clear();
}

In traditional game loops update and draw would be broken out but for now I just wanted to keep things simple. The first thing I do is check for new input from the input class. If I detect a new state change in the input class, I attempt to move the player by calling move() then passing in the current input state. Right now the engine only supports “left”, “up”, “right” and “down” states but I plan on extending this with a more dynamic way of binding keys. Next I check if the display has been invalidated. This is critical in a simple render engine like this so I’ll explain it in more detail.

When using requestAnimationFrame your game is going to call render() a lot more then you need. It may even call it more than the 30 or 60 times a second that you would typically want in fast game to redraw. Roguelikes are turn based so we only want to update the display when we detect a change in the game’s state such as the player moving. To handle this, I use a technique found in component architecture that relies on invalidation which notifies the render loop that on the next render loop the display should be repainted. In this implementation, when the player is moved, the display is invalidated and we render a new scene on the next requestAnimationFrame. You can see the call to the renderer after the invalidate check. Once the render has been called we get the player’s position, which is stored outside of the map data, and we tell the renderer to draw the player.

The final function of importance in the game class is move(). This is where we do some pre-calculation to determine if the player can actually move. We use the MovementHelper class to get a “previewMove” which is the x,y position of the next move based on the input state we passed in. With this point we can ask the map for the value of the tile the player would be standing on. In RogueTS there are 4 basic tiles:

  • “ “ – empty space
  • x – Item or Monster
  • # – Wall
  • @ – the player

If the preview move tile is a space or an X we can move and the movementHelper applies the new position to the player and updates the map selection’s center.

Outside of this, the main mechanics of how RogueTS work are a little more complex. I plan on doing some more posts no each part of the game engine very soon. Feel free to play around with both the source code for RogueTS and F*Rogue. I’ll be adding more to RogueTS very soon such as items, monsters, combat and touch controls.

Subscribe To My Mailing List

Want to learn how to make a game? Not sure where to start? Even if you are a seasoned game maker there is still a lot you can learn from my mailing list. I'll be covering tips and tricks for how to build, release and market games each month.

Simply sign up for my mailing list and also get access to a 50% off discount code for my eBooks and other content. I promise to not spam your inbox!

Join Now