Rot.js tutorial, part 2

From RogueBasin
(Difference between revisions)
Jump to: navigation, search
Line 40: Line 40:
 
</syntaxhighlight>
 
</syntaxhighlight>
 
</div>
 
</div>
 
  
 
== Preparing the game turn engine ==
 
== Preparing the game turn engine ==
Line 47: Line 46:
  
 
How does this work? After creating an instance of <code>ROT.Engine</code>, we feed it with all available ''actors''. The engine will then automatically take care about proper turn scheduling and letting these actors perform their actions.
 
How does this work? After creating an instance of <code>ROT.Engine</code>, we feed it with all available ''actors''. The engine will then automatically take care about proper turn scheduling and letting these actors perform their actions.
 +
 +
It is very important to embrace the fact that everything is asynchronous in the world of client-side JavaScript: there are basically no blocking calls. This eliminates the possibility of having a simple ''while'' loop as our main timing/scheduling instrument. Fortunately, the <code>ROT.Engine</code> is well prepared for this.
  
 
Creating the engine is just a matter of adding a few lines to our code:  
 
Creating the engine is just a matter of adding a few lines to our code:  
Line 62: Line 63:
 
</div>
 
</div>
  
== Console output: ROT.Display ==
+
== Interaction between actors and the engine ==
  
Being a JS app, our game can modify the HTML page in many ways. However, rot.js encourages only one kind of output: printing to its "tty console", which is represented by a HTML <canvas> tag. In order to draw anything, we first need to create this console and store it for later usage.
+
There is a tight symbiotic relationship between the engine and its actors. When running, the engine repeatedly picks a proper actor from its queue (based on actor's speed) and calls the actor's <code>act()</code> method. Actors are allowed to interrupt this loop (when waiting asynchronously, for example) by calling <code>ROT.Engine::lock</code> and resume it (<code>ROT.Engine::unlock</code>).
  
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
+
It is possible to have multiple lock levels (the lock is recursive); this allows for complex chaining of asynchronous calls. Fortunately, this won't be needed in our simple game.
<syntaxhighlight lang="javascript">
+
var Game = {
+
    display: null,
+
 
+
    init: function() {
+
        this.display = new ROT.Display();
+
        document.body.appendChild(this.display.getContainer());
+
    }
+
}
+
</syntaxhighlight>
+
</div>
+
 
+
Note that this console has a default size of 80x25 cells; if we wanted different default dimensions, we would configure them via <code>ROT.DEFAULT_WIDTH</code> and <code>ROT.DEFAULT_HEIGHT</code>.
+
 
+
== Generating a dungeon map ==
+
 
+
We will use one of rot.js's built-in map generators to create the game level. One of the design paradigms of rot.js is that people should not be forced to use some pre-defined data structures; this is why the generator is ''callback-based''. We will pass our custom function to the generator; it will get called repeatedly during the process.
+
 
+
This might be a good time to check out the [http://ondras.github.com/rot.js/manual/ rot.js manual], which contains useful code samples and usage overview.
+
 
+
How should we store the resulting map data? We will use a very basic method of storage: an ordinary JS object ("hashmap"), indexed by strings (having the format "x,y"), values representing floor tiles. We are not going to store wall / solid cells.
+
  
'''NOTE:''' We are passing <code>digCallback.bind(this)</code> instead of just <code>digCallback</code> to the Digger. This is necessary to ensure that our callback is called within a correct context (''activation object'' in ECMA parlance).
+
So, what is an actor? Any JS object with methods '''<code>act</code>''' and '''<code>getSpeed</code>'''.
  
 
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
 
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
Game.map = {};
+
Player.prototype.getSpeed = function() {
Game._generateMap = function() {
+
     return 100;
     var digger = new ROT.Map.Digger();
+
}
 
+
      
     var digCallback = function(x, y, value) {
+
Player.prototype.act = function() {
        if (value) { return; } /* do not store walls */
+
    Game.engine.lock();
 
+
    /* wait for user input; do stuff when user hits a key */
        var key = x+","+y;
+
    window.addEventListener("keydown", this);
        this.map[key] = "·";
+
    }
+
    digger.create(digCallback.bind(this));
+
 
}
 
}
</syntaxhighlight>
 
</div>
 
  
We still cannot see anything, because we have not written a single character to the display yet. Time to fix this: iterate through all the floor tiles and draw their visual representation.
+
Player.prototype.handleEvent = function(e) {
 
+
     /* process user input */
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
+
<syntaxhighlight lang="javascript">
+
Game._drawWholeMap = function() {
+
     for (var key in this.map) {
+
        var parts = key.split(",");
+
        var x = parseInt(parts[0]);
+
        var y = parseInt(parts[1]);
+
        this.display.draw(x, y, this.map[key]);
+
    }
+
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 
</div>
 
</div>
  
== Randomly generated boxes ==
 
 
Finally, let's create some boxes - potential ananas storage. We will hide the ananas in one of them in later parts of this tutorial. To place 10 random boxes around, we will leverage rot.js's [[RNG|Random number generator]].
 
 
<code>ROT.RNG</code> can do a lot of stuff, but we need something simple: a random, evenly distributed number between zero (inclusive) and one (exclusive), just like the <code>Math.random</code> does. The proper way of doing this is calling <code>ROT.RNG.getUniform()</code>.
 
 
We will store the empty cells in an array; for each box placed, we will pick a random empty cell, remove it from a list and mark that place as a box (asterisk) in our storage structure.
 
 
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
 
<syntaxhighlight lang="javascript">
 
Game._generateMap = function() {
 
    var digger = new ROT.Map.Digger();
 
    var freeCells = [];
 
 
    var digCallback = function(x, y, value) {
 
        if (value) { return; } /* do not store walls */
 
 
        freeCells.push(key);
 
        var key = x+","+y;
 
        this.map[key] = "·";
 
    }
 
    digger.create(digCallback.bind(this));
 
 
    this._generateBoxes(freeCells);
 
 
    this._drawWholeMap();
 
};
 
 
Game._generateBoxes = function(freeCells) {
 
    for (var i=0;i<10;i++) {
 
        var index = Math.floor(ROT.RNG.getUniform() * freeCells.length);
 
        var key = freeCells.splice(index, 1)[0];
 
        this.map[key] = "*";
 
    }
 
};
 
</syntaxhighlight>
 
</div>
 
  
  
 
And that's all for part 2. The whole working code is available at [http://jsfiddle.net/rotjs/CZ8YJ/ jsfiddle.net].
 
And that's all for part 2. The whole working code is available at [http://jsfiddle.net/rotjs/CZ8YJ/ jsfiddle.net].

Revision as of 21:00, 12 December 2012

This is the second part of a rot.js tutorial.

FIXME NOT COMPLETED!

The Player Character

Time to make some interesting interactive shinies! First, the player needs a decent representation. It would be sufficient to use a plain JS object to represent the player, but it is generally more robust to define the player via its constructor function and instantialize it.

By this time, you probably got used to the fact that some variable names start with an underscore. This is a relatively common technique of marking them private. JavaScript does not offer true private variables, so this underscore-based nomenclature is just our useful way of marking stuff as "internal".

We would like to place the player to some spare floor tile: let's use exactly the same technique we used in Part 1 of this tutorial to place the boxes: just pick one free location from our list.

var Player = function(x, y) {
    this._x = x;
    this._y = y;
    this._draw();
}
 
Player.prototype._draw = function() {
    Game.display.draw(this._x, this._y, "@");
}
 
Game.player = null;
 
Game._generateMap = function() {
    /* ...previous stuff... */
    this._createPlayer(freeCells);
};
 
Game._createPlayer = function(freeCells) {
    var index = Math.floor(ROT.RNG.getUniform() * freeCells.length);
    var key = freeCells.splice(index, 1)[0];
    var parts = key.split(",");
    var x = parseInt(parts[0]);
    var y = parseInt(parts[1]);
    this.player = new Player(x, y);
};

Preparing the game turn engine

There will be two entities taking turns in our game: the Player Character and Pedro (The Enemy). To make things simple, these two will have the same speed, alternating their turns evenly. But even in this simple case, we can use the ROT.Engine timing framework to our advantage.

How does this work? After creating an instance of ROT.Engine, we feed it with all available actors. The engine will then automatically take care about proper turn scheduling and letting these actors perform their actions.

It is very important to embrace the fact that everything is asynchronous in the world of client-side JavaScript: there are basically no blocking calls. This eliminates the possibility of having a simple while loop as our main timing/scheduling instrument. Fortunately, the ROT.Engine is well prepared for this.

Creating the engine is just a matter of adding a few lines to our code:

Game.engine = null;
 
Game.init = function() {
    this.engine = new ROT.Engine();
    this.engine.addActor(this.player);
    this.engine();
}

Interaction between actors and the engine

There is a tight symbiotic relationship between the engine and its actors. When running, the engine repeatedly picks a proper actor from its queue (based on actor's speed) and calls the actor's act() method. Actors are allowed to interrupt this loop (when waiting asynchronously, for example) by calling ROT.Engine::lock and resume it (ROT.Engine::unlock).

It is possible to have multiple lock levels (the lock is recursive); this allows for complex chaining of asynchronous calls. Fortunately, this won't be needed in our simple game.

So, what is an actor? Any JS object with methods act and getSpeed.

Player.prototype.getSpeed = function() {
    return 100;
}
 
Player.prototype.act = function() {
    Game.engine.lock();
    /* wait for user input; do stuff when user hits a key */
    window.addEventListener("keydown", this);
}
 
Player.prototype.handleEvent = function(e) {
    /* process user input */
}


And that's all for part 2. The whole working code is available at jsfiddle.net.

Personal tools