Difference between revisions of "Rot.js tutorial, part 2"

From RogueBasin
Jump to navigation Jump to search
(Created page with "This is the second part of a rot.js tutorial. FIXME NOT COMPLETED! == Preparing the game turn engine == Our game is played within a web page; a rudimentary HTML file sh...")
 
(From the rot.js source, 38 is VK_UP, and goes [0, -1], which is up (rather than upper-left))
 
(16 intermediate revisions by 3 users not shown)
Line 1: Line 1:
This is the second part of a [[rot.js tutorial]].
This is the second part of a [[rot.js tutorial]].


FIXME NOT COMPLETED!
== The Player Character ==


== Preparing the game turn engine ==
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.


Our game is played within a web page; a rudimentary HTML file should be sufficient.
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="xml">
<syntaxhighlight lang="javascript">
<!doctype html>
var Player = function(x, y) {
<html>
    this._x = x;
     <head>
     this._y = y;
        <title>Ananas aus Caracas: rot.js tutorial game</title>
    this._draw();
        <script src="https://raw.github.com/ondras/rot.js/master/rot.js"></script>
}
        <script src="/path/to/the/game.js"></script>
 
     </head>
Player.prototype._draw = function() {
     <body onload="Game.init()">
    Game.display.draw(this._x, this._y, "@", "#ff0");
        <h1>Ananas aus Caracas</h1>
}
     </body>
 
</html>
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>


We are going to put all the game code in one file, to maintain simplicity (when making larger games, it is far more useful to split the code across several files). We do not want to pollute the global name space with our variables; that's why we wrap all our code in an object named "Game".
== Preparing the game turn engine ==


<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
There will be two entities taking turns in our game: the Player Character and Pedro (The Enemy). To make things simple, let's just alternate their turns evenly. But even in this simple case, we can use the <code>ROT.Engine</code> timing framework to our advantage.
<syntaxhighlight lang="javascript">
 
var Game = {
How does this work? First, the <code>ROT.Scheduler.Simple</code> will be fed with all available ''actors'' - this component will take care about fair turn scheduling. The <code>ROT.Engine</code> will then use this scheduler to automatically call relevant actors in a loop. (Note: we pass <code>true</code> as a second argument to <code>scheduler.add</code> - this means that our actor is not a one-shot event, but rather a recurring item.)
    init: function() {}
}
</syntaxhighlight>
</div>


== Console output: ROT.Display ==
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.


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.
Creating the scheduler and 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">
var Game = {
Game.engine = null;
    display: null,


    init: function() {
Game.init = function() {
        this.display = new ROT.Display();
    var scheduler = new ROT.Scheduler.Simple();
        document.body.appendChild(this.display.getContainer());
    scheduler.add(this.player, true);
     }
    this.engine = new ROT.Engine(scheduler);
     this.engine.start();
}
}
</syntaxhighlight>
</syntaxhighlight>
</div>
</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>.
== Interaction between actors and the engine ==
 
== 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.
There is a tight symbiotic relationship between the engine and its actors. When running, the engine repeatedly picks a proper actor (using the scheduler) 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>).


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.
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.


'''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 the '''<code>act</code>''' method.


<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.act = function() {
Game._generateMap = function() {
    Game.engine.lock();
     var digger = new ROT.Map.Digger();
    /* wait for user input; do stuff when user hits a key */
     window.addEventListener("keydown", this);
}


    var digCallback = function(x, y, value) {
Player.prototype.handleEvent = function(e) {
        if (value) { return; } /* do not store walls */
    /* process user input */
 
        var key = x+","+y;
        this.map[key] = "·";
    }
    digger.create(digCallback.bind(this));
}
}
</syntaxhighlight>
</syntaxhighlight>
</div>
</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.
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.
 
== 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):


<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._drawWholeMap = function() {
var keyMap = {};
    for (var key in this.map) {
keyMap[38] = 0;
        var parts = key.split(",");
keyMap[33] = 1;
        var x = parseInt(parts[0]);
keyMap[39] = 2;
        var y = parseInt(parts[1]);
keyMap[34] = 3;
        this.display.draw(x, y, this.map[key]);
keyMap[40] = 4;
    }
keyMap[35] = 5;
}
keyMap[37] = 6;
keyMap[36] = 7;
</syntaxhighlight>
</syntaxhighlight>
</div>
</div>


== Randomly generated boxes ==
Numeric values are not chosen randomly: they correspond to directional constants in <code>rot.js</code> (8-topology, clockwise, starting at the top - the same as CSS does).


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]].
We need to perform a two-step validation of user input:


<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>.
# 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


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.
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;">
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
<syntaxhighlight lang="javascript">
<syntaxhighlight lang="javascript">
Game._generateMap = function() {
Player.prototype.handleEvent = function(e) {
     var digger = new ROT.Map.Digger();
     var keyMap = {};
     var freeCells = [];
    keyMap[38] = 0;
    keyMap[33] = 1;
    keyMap[39] = 2;
    keyMap[34] = 3;
    keyMap[40] = 4;
    keyMap[35] = 5;
     keyMap[37] = 6;
    keyMap[36] = 7;
 
    var code = e.keyCode;
 
    if (!(code in keyMap)) { return; }


     var digCallback = function(x, y, value) {
     var diff = ROT.DIRS[8][keyMap[code]];
        if (value) { return; } /* do not store walls */
    var newX = this._x + diff[0];
    var newY = this._y + diff[1];


        freeCells.push(key);
    var newKey = newX + "," + newY;
        var key = x+","+y;
    if (!(newKey in Game.map)) { return; } /* cannot move in this direction */
        this.map[key] = "·";
}
    }
</syntaxhighlight>
    digger.create(digCallback.bind(this));
</div>


    this._generateBoxes(freeCells);
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._drawWholeMap();
<div style="padding:5px; background-color:#eee; margin-bottom:2em;">
};
<syntaxhighlight lang="javascript">
Player.prototype.handleEvent = function(e) {
    /* ...previous stuff... */


Game._generateBoxes = function(freeCells) {
    Game.display.draw(this._x, this._y, Game.map[this._x+","+this._y]);
     for (var i=0;i<10;i++) {
     this._x = newX;
        var index = Math.floor(ROT.RNG.getUniform() * freeCells.length);
    this._y = newY;
        var key = freeCells.splice(index, 1)[0];
    this._draw();
        this.map[key] = "*";
    window.removeEventListener("keydown", this);
    }
    Game.engine.unlock();
};
}
</syntaxhighlight>
</syntaxhighlight>
</div>
</div>


And that's all for part 2. The whole working code is available at [http://jsfiddle.net/rotjs/CZ8YJ/ jsfiddle.net]. Feel free to continue in [[Rot.js tutorial, part 3]].


And that's all for part 2. The whole working code is available at [http://jsfiddle.net/rotjs/CZ8YJ/ jsfiddle.net].
[[Category:Developing]]

Latest revision as of 20:33, 17 June 2017

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

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 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.

var Player = function(x, y) {
    this._x = x;
    this._y = y;
    this._draw();
}

Player.prototype._draw = function() {
    Game.display.draw(this._x, this._y, "@", "#ff0");
}

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, let's just alternate their turns evenly. But even in this simple case, we can use the ROT.Engine timing framework to our advantage.

How does this work? First, the ROT.Scheduler.Simple will be fed with all available actors - this component will take care about fair turn scheduling. The ROT.Engine will then use this scheduler to automatically call relevant actors in a loop. (Note: we pass true as a second argument to scheduler.add - this means that our actor is not a one-shot event, but rather a recurring item.)

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 scheduler and the engine is just a matter of adding a few lines to our code:

Game.engine = null;

Game.init = function() {
    var scheduler = new ROT.Scheduler.Simple();
    scheduler.add(this.player, true);
    this.engine = new ROT.Engine(scheduler);
    this.engine.start();
}

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 (using the scheduler) 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 the act method.

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 */
}

We are using somewhat uncommon (but very useful!) technique of assigning event handlers: we pass a JS object as a second argument to the addEventListener call. Such object (this, in this case) must have the handleEvent method, which will be called once the event ("keydown") occurs.

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 (handleEvent) gets executed with one argument: the Event object. Its keyCode 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):

var keyMap = {};
keyMap[38] = 0;
keyMap[33] = 1;
keyMap[39] = 2;
keyMap[34] = 3;
keyMap[40] = 4;
keyMap[35] = 5;
keyMap[37] = 6;
keyMap[36] = 7;

Numeric values are not chosen randomly: they correspond to directional constants in rot.js (8-topology, clockwise, starting at the top - the same as CSS does).

We need to perform a two-step validation of user input:

  1. If the key code is not present in keyMap, the user pressed a key which we cannot handle
  2. If the key code is present, we need to check whether the PC can move in that direction

To convert a directional constant (0..7) to a map coordinates, we can use the ROT.DIRS set of topological diffs:

Player.prototype.handleEvent = function(e) {
    var keyMap = {};
    keyMap[38] = 0;
    keyMap[33] = 1;
    keyMap[39] = 2;
    keyMap[34] = 3;
    keyMap[40] = 4;
    keyMap[35] = 5;
    keyMap[37] = 6;
    keyMap[36] = 7;

    var code = e.keyCode;

    if (!(code in keyMap)) { return; }

    var diff = ROT.DIRS[8][keyMap[code]];
    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 */
}

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 (unlock()).

Player.prototype.handleEvent = function(e) {
    /* ...previous stuff... */

    Game.display.draw(this._x, this._y, Game.map[this._x+","+this._y]);
    this._x = newX;
    this._y = newY;
    this._draw();
    window.removeEventListener("keydown", this);
    Game.engine.unlock();
}

And that's all for part 2. The whole working code is available at jsfiddle.net. Feel free to continue in Rot.js tutorial, part 3.