Rot.js tutorial, part 3

From RogueBasin
(Difference between revisions)
Jump to: navigation, search
(Created page with "This is the third part of a rot.js tutorial. '''FIXME THIS IS A WORK IN PROGRESS, NOT FINISHED YET!''' == The Player Character == Time to make some interesting interact...")
 
Line 3: Line 3:
 
'''FIXME THIS IS A WORK IN PROGRESS, NOT FINISHED YET!'''
 
'''FIXME THIS IS A WORK IN PROGRESS, NOT FINISHED YET!'''
  
== The Player Character ==
+
== Looking inside the box ==
  
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.
+
The game generates several boxes, but so far, none of the contains the prized ananas. Let us store the ananas in the first box generated:
 
+
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 on 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.
+
  
 
<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">
var Player = function(x, y) {
+
Game.ananas = null;
    this._x = x;
+
    this._y = y;
+
    this._draw();
+
}
+
  
Player.prototype._draw = function() {
+
Game._generateBoxes = function(freeCells) {
     Game.display.draw(this._x, this._y, "@", "#ff0");
+
     for (var i=0;i<10;i++) {
 +
        /* ...previous stuff... */
 +
        if (!i) { this.ananas = key; } /* first box contains an ananas */
 +
    }
 
}
 
}
 
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);
 
};
 
 
</syntaxhighlight>
 
</syntaxhighlight>
 
</div>
 
</div>
  
== Preparing the game turn engine ==
+
Apart from moving, there is one more interaction a player must perform: looking into boxes. We will allow both Enter (keyCode 13) and Spacebar (keyCode 32) for this action:
 
+
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 <code>ROT.Engine</code> timing framework to our advantage.
+
 
+
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 of 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:  
+
  
 
<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.engine = null;
+
Player.prototype.handleEvent = function(e) {
 
+
     var code = e.keyCode;
Game.init = function() {
+
     if (code == 13 || code == 32) {
     this.engine = new ROT.Engine();
+
        this._checkBox();
     this.engine.addActor(this.player);
+
        return;
    this.engine();
+
    }
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 
</div>
 
</div>
  
== Interaction between actors and the engine ==
+
Opening a box to verify its contents is as simple as comparing current player's position with our list of boxes and the stored ananas position:
 
+
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>).
+
 
+
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 '''<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">
Player.prototype.getSpeed = function() {
+
Player.prototype._checkBox = function() {
     return 100;
+
     var key = this._x + "," + this._y;
}
+
     if (Game.map[key] != "*") {
      
+
        alert("There is no box here!");
Player.prototype.act = function() {
+
     } else if (key == Game.ananas) {
     Game.engine.lock();
+
        alert("Hooray! You found an ananas and won this game.");
    /* wait for user input; do stuff when user hits a key */
+
        Game.engine.lock();
    window.addEventListener("keydown", this);
+
        window.removeEventListener("keydown", this);
}
+
    } else {
 
+
        alert("This box is empty :-(");
Player.prototype.handleEvent = function(e) {
+
     }
     /* process user input */
+
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 
</div>
 
</div>
  
We are using somewhat uncommon (but very useful!) technique of assigning event handlers: we pass a JS object as a second argument to the <code>addEventListener</code> call. Such object (<code>this</code>, in this case) must have the <code>handleEvent</code> method, which will be called once the event ("keydown") occurs.
+
== Pedro, the angry owner ==
 
+
== Working with the keyboard and moving the player around ==
+
 
+
There is one last bit remaining to implement: detect the pressed key, decide whether it is valid and move the player accordingly.
+
  
Our event handler (<code>handleEvent</code>) gets executed with one argument: the Event object. Its <code>keyCode</code> property is a number code of the key being pressed. Let's create a mapping of allowed key codes (this code sample uses numpad keys, but it is trivial to extend it to other layouts as well):
+
The game is now winnable! Let's add a villain as a second actor. We will place him using the same algorithm we used previously. To do this, let's refactor the original <code>_createPlayer</code> method into a more useful parametrized ''factory'' <code>_createBeing</code> by passing a constructor function as an argument:
  
 
<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">
var keyMap = {};
+
var Pedro = function(x, y) {
keyMap[38] = 0;
+
    this._x = x;
keyMap[33] = 1;
+
    this._y = y;
keyMap[39] = 2;
+
    this._draw();
keyMap[34] = 3;
+
}
keyMap[40] = 4;
+
   
keyMap[35] = 5;
+
Pedro.prototype.getSpeed = function() { return 100; }
keyMap[37] = 6;
+
   
keyMap[36] = 7;
+
Pedro.prototype._draw = function() {
</syntaxhighlight>
+
    Game.display.draw(this._x, this._y, "P", "red");
</div>
+
}
  
Numeric values are not chosen randomly: they correspond to directional constants in <code>rot.js</code> (8-topology, clockwise, starting in top-left - the same as CSS does).
+
Game._createBeing = function(what, 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]);
 +
    return new what(x, y);
 +
}
  
We need to perform a two-step validation of user input:
+
Game._generateMap = function() {
 
+
     /* ...previous stuff... */
# If the key code is not present in <code>keyMap</code>, the user pressed a key which we cannot handle
+
       
# If the key code '''is''' present, we need to check whether the PC can move in that direction
+
     this.player = this._createBeing(Player, freeCells);
 
+
     this.pedro = this._createBeing(Pedro, freeCells);
To convert a directional constant (0..7) to a map coordinates, we can use the <code>ROT.DIRS</code> set of topological diffs:
+
 
+
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
+
<syntaxhighlight lang="javascript">
+
Player.prototype.handleEvent = function(e) {
+
     if (!(e.keyCode in direction)) { return; }
+
 
+
     var direction = keyMap[e.keyCode];
+
    var diff = ROT.DIRS[8][direction];
+
     var newX = this._x + diff[0];
+
    var newY = this._y + diff[1];
+
 
+
    var newKey = newX + "," + newY;
+
    if (!(newKey in Game.map)) { return; } /* cannot move in this direction */
+
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>
 
</div>
 
</div>
  
The actual move is performed in two steps - redrawing the old position and redrawing the new position. After that, we remove our keyboard listener (the turn has ended!) and - '''importantly''' - resume the game engine (<code>unlock()</code>).
+
This might be confusing to some, but passing functions around (as function arguments, for instance) is very common in JavaScript.
  
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
+
== Pathfinding-based AI ==
<syntaxhighlight lang="javascript">
+
Player.prototype.handleEvent = function(e) {
+
    /* ...previous stuff... */
+
  
    Game.display.draw(this._x, this._y, Game.map[this._x+","+this._y]);
+
Pedro is missing its <code>act</code> method so far: we are going to use one of rot.js's pathfinding functions to model Pedro's behavior.
    this._x = newX;
+
    this._y = newY;
+
    this._draw();
+
    window.removeEventListener("keydown", this);
+
    Game.engine.unlock();
+
}
+
</syntaxhighlight>
+
</div>
+
  
 
And that's all for part 3. The whole working code is available at [http://jsfiddle.net/rotjs/qRnFY/ jsfiddle.net].
 
And that's all for part 3. The whole working code is available at [http://jsfiddle.net/rotjs/qRnFY/ jsfiddle.net].

Revision as of 15:47, 13 December 2012

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

FIXME THIS IS A WORK IN PROGRESS, NOT FINISHED YET!

Looking inside the box

The game generates several boxes, but so far, none of the contains the prized ananas. Let us store the ananas in the first box generated:

Game.ananas = null;
 
Game._generateBoxes = function(freeCells) {
    for (var i=0;i<10;i++) {
        /* ...previous stuff... */
        if (!i) { this.ananas = key; } /* first box contains an ananas */
    }
}

Apart from moving, there is one more interaction a player must perform: looking into boxes. We will allow both Enter (keyCode 13) and Spacebar (keyCode 32) for this action:

Player.prototype.handleEvent = function(e) {
    var code = e.keyCode;
    if (code == 13 || code == 32) {
        this._checkBox();
        return;
    }
}

Opening a box to verify its contents is as simple as comparing current player's position with our list of boxes and the stored ananas position:

Player.prototype._checkBox = function() {
    var key = this._x + "," + this._y;
    if (Game.map[key] != "*") {
        alert("There is no box here!");
    } else if (key == Game.ananas) {
        alert("Hooray! You found an ananas and won this game.");
        Game.engine.lock();
        window.removeEventListener("keydown", this);
    } else {
        alert("This box is empty :-(");
    }
}

Pedro, the angry owner

The game is now winnable! Let's add a villain as a second actor. We will place him using the same algorithm we used previously. To do this, let's refactor the original _createPlayer method into a more useful parametrized factory _createBeing by passing a constructor function as an argument:

var Pedro = function(x, y) {
    this._x = x;
    this._y = y;
    this._draw();
}
 
Pedro.prototype.getSpeed = function() { return 100; }
 
Pedro.prototype._draw = function() {
    Game.display.draw(this._x, this._y, "P", "red");
}
 
Game._createBeing = function(what, 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]);
    return new what(x, y);
}
 
Game._generateMap = function() {
    /* ...previous stuff... */
 
    this.player = this._createBeing(Player, freeCells);
    this.pedro = this._createBeing(Pedro, freeCells);
}

This might be confusing to some, but passing functions around (as function arguments, for instance) is very common in JavaScript.

Pathfinding-based AI

Pedro is missing its act method so far: we are going to use one of rot.js's pathfinding functions to model Pedro's behavior.

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

Personal tools