Tile Movement

From Spherical
Jump to: navigation, search


This tutorial is for beginners on up. So, you are wondering how to make a good tile movement system for your game? In Sphere it's not that hard and can be made as a natural extension of the map engine. You will need to know how to integrate existing systems into your game, and understand how Sphere's SetUpdateScript works.

--Radnen (talk) 05:55, 4 March 2014 (UTC)

Step 0: the class

As you set up any system, we need a class to hold the information. In JavaScript we can do so by creating a new function.

function TileSystem(input) {
    this.input = input;
}

Step 1: the listener

The next thing we do is create what I call a listener. It's code that must run always in an update loop. A listener in specific is something that must run all the time, but does nothing until you hit a key or a button or something. We add it to our tile movement system like so:

function TileSystem(input) {
    this.input = input;
}

TileSystem.prototype.update = function() {
}

When the player hits the movement keys, it should move the entity. We know what the entity is by the this.input field. We first want to wait until the person is no longer moving, by using the function IsCommandQueueEmpty(). If the queue is not empty we return early, letting it do nothing. If the command queue is empty then we are good for seeing if we can move the player left or right, up or down by hitting those 4 keys respectively.

TileSystem.prototype.update = function() {
    if (!this.input || !IsCommandQueueEmpty(this.input)) return;

    if (IsKeyPressed(KEY_LEFT))       // move left
    else if (IsKeyPressed(KEY_RIGHT)) // move right
    else if (IsKeyPressed(KEY_UP))    // move up
    else if (IsKeyPressed(KEY_DOWN))  // move down
}

Good. We created a listener. Now, let's make it move the player.

Step 2: movement

Adding movement means creating a new method stub called move which queues enough move commands to move the sprite in the given direction. We use the arrow keys to set that direction.

TileSystem.prototype.update = function() {
    if (!this.input || !IsCommandQueueEmpty(this.input)) return;

    if (IsKeyPressed(KEY_LEFT )) this.move(COMMAND_MOVE_WEST);
    else if (IsKeyPressed(KEY_RIGHT)) this.move(COMMAND_MOVE_EAST); 
    else if (IsKeyPressed(KEY_UP   )) this.move(COMMAND_MOVE_NORTH);
    else if (IsKeyPressed(KEY_DOWN )) this.move(COMMAND_MOVE_SOUTH);
}

TileSystem.prototype.move = function(command) {
    var pixels = GetTileWidth();
    for (var i = 0; i < pixels; ++i) {
        QueuePersonCommand(this.input, command, false);
    }
}

Good. We are almost done. The last major step is to figure out facing.

Step 3: facing

Facing is best done adding on to the directional keys, and queuing a face command in the same direction as the move command.

TileSystem.prototype.update = function() {
    if (!this.input || !IsCommandQueueEmpty(this.input)) return;

    if (IsKeyPressed(KEY_LEFT)) {
        QueuePersonCommand(this.input, COMMAND_FACE_WEST, true);
        this.move(COMMAND_MOVE_WEST);
    }
    else if (IsKeyPressed(KEY_RIGHT)) {
        QueuePersonCommand(this.input, COMMAND_FACE_EAST, true);
        this.move(COMMAND_MOVE_EAST);
    }
    else if (IsKeyPressed(KEY_UP)) {
        QueuePersonCommand(this.input, COMMAND_FACE_NORTH, true);
        this.move(COMMAND_MOVE_NORTH);
    }
    else if (IsKeyPressed(KEY_DOWN)) {
        QueuePersonCommand(this.input, COMMAND_FACE_SOUTH, true);
        this.move(COMMAND_MOVE_SOUTH);
    }
}

Step 4: overwriting Sphere

Sphere uses the movement keys internally for movement, and we must stop that from happening. We can do this by binding the 4 keys to nothing.

function TileSystem(input) {
    this.input = input;

    BindKey(KEY_UP, '', '');
    BindKey(KEY_DOWN, '', '');
    BindKey(KEY_LEFT, '', '');
    BindKey(KEY_RIGHT, '', '');
}

This step was crucial since Sphere would still try to move the sprite per pixel rather than our newer per tile method.

Step 5: using it

Good, at this stage you have a basic tile mover ready to go. Now, how do you use it? We make a new instance of TileMover, giving it the name of our attached entity, and set an update script, like so.

RequireScript('tilemover.js'); // or however you saved it

var my_mover = new TileMover("player");

function game() {
    SetUpdateScript("my_mover.update()");
    CreatePerson("player");
    AttachInput("player");
    AttachCamera("player");
    MapEngine("map.rmp", 60);
}

And that's it!

Intermediate: checking obstructions

If you are using a map that does not have tile based collisions, then checking collisions can get tricky. If you followed the tutorial up until now, you'll notice that it is possible to walk 'off tile' so to speak. In order to make sure the player always stays on tile, we can employ a check to make sure it only walks if the next tile is clear.

TileSystem.prototype.check = function(command) {
    var x = GetPersonX(this.input);
    var y = GetPersonY(this.input);

    switch(command) {
        case COMMAND_MOVE_NORTH: y -= GetTileHeight(); break;
        case COMMAND_MOVE_SOUTH: y += GetTileHeight(); break;
        case COMMAND_MOVE_EAST:  x += GetTileWidth(); break;
        case COMMAND_MOVE_WEST:  x -= GetTileWidth(); break;
    }

    return IsPersonObstructed(this.input, x, y);
}

So, how does this work? It checks the next tile in the direction the person is walking and returns false if there is no obstruction and true if there is anything in the way. This is so far, just the check. Now we should go add it like so.

TileSystem.prototype.update = function() {
    if (!this.input || !IsCommandQueueEmpty(this.input)) return;

    var command = null;
    var face    = null;

    if (IsKeyPressed(KEY_LEFT)) {
        face = COMMAND_FACE_WEST;
        command = COMMAND_MOVE_WEST;
    }
    else if (IsKeyPressed(KEY_RIGHT)) {
        face = COMMAND_FACE_EAST;
        command = COMMAND_MOVE_EAST;
    }
    else if (IsKeyPressed(KEY_UP)) {
        face = COMMAND_FACE_NORTH;
        command = COMMAND_MOVE_NORTH;
    }
    else if (IsKeyPressed(KEY_DOWN)) {
        face = COMMAND_FACE_SOUTH;
        command = COMMAND_MOVE_SOUTH;
    }

    if (command != null && !this.check(command)) {
        if (face != null) QueuePersonCommand(this.input, face, true);
        this.move(command);
    }
}

What this does is defer the command until we check the direction. If all is good, we are allowed to move. Otherwise it returns doing nothing.

Bonus: adding a sound

Alright you got it to check if something is blocking in the way. Now you are wondering like how it is in Pokémon that it creates a sound when you hit something? First we should load that sound up when we create our tile mover, like so. You can use bfxr to create the thump sound.

function TileSystem(input, sound) {
    this.input = input;
    this.sound = sound;

    BindKey(KEY_UP, '', '');
    BindKey(KEY_DOWN, '', '');
    BindKey(KEY_LEFT, '', '');
    BindKey(KEY_RIGHT, '', '');
}

Next we can add the sound by slightly modifying the move section of the update loop.

    if (command != null) {
        if (this.check(command)) {
            if (this.sound) this.sound.play();
            command = COMMAND_ANIMATE;
        }
        if (face != null) QueuePersonCommand(this.input, face, true);
        this.move(command);
    }

And that's it! I add the animate command to make it still appear as if the sprite were walking in place; otherwise he's just standing still.

Appendix: full code

More advanced systems can always be made. They are simply, too much to go into detail here but all of them would be built off a method similar to this. So, feel free to add your own things to this if you want.

function TileSystem(input, sound) {
    this.input = input;
    this.sound = sound;
 
    BindKey(KEY_UP, '', '');
    BindKey(KEY_DOWN, '', '');
    BindKey(KEY_LEFT, '', '');
    BindKey(KEY_RIGHT, '', '');
}
 
TileSystem.prototype.update = function() {
    if (!this.input || !IsCommandQueueEmpty(this.input)) return;
 
    var command = null;
    var face    = null;
 
    if (IsKeyPressed(KEY_LEFT)) {
        face = COMMAND_FACE_WEST;
        command = COMMAND_MOVE_WEST;
    }
    else if (IsKeyPressed(KEY_RIGHT)) {
        face = COMMAND_FACE_EAST;
        command = COMMAND_MOVE_EAST;
    }
    else if (IsKeyPressed(KEY_UP)) {
        face = COMMAND_FACE_NORTH;
        command = COMMAND_MOVE_NORTH;
    }
    else if (IsKeyPressed(KEY_DOWN)) {
        face = COMMAND_FACE_SOUTH;
        command = COMMAND_MOVE_SOUTH;
    }
 
    if (command != null) {
        if (this.check(command)) {
            if (this.sound) this.sound.play();
            command = COMMAND_ANIMATE;
        }
        if (face != null) QueuePersonCommand(this.input, face, true);
        this.move(command);
    }
}
 
TileSystem.prototype.check = function(command) {
    var x = GetPersonX(this.input);
    var y = GetPersonY(this.input);
 
    switch(command) {
        case COMMAND_MOVE_NORTH: y -= GetTileHeight(); break;
        case COMMAND_MOVE_SOUTH: y += GetTileHeight(); break;
        case COMMAND_MOVE_EAST:  x += GetTileWidth(); break;
        case COMMAND_MOVE_WEST:  x -= GetTileWidth(); break;
    }
 
    return IsPersonObstructed(this.input, x, y);
}
 
TileSystem.prototype.move = function(command) {
    var pixels = GetTileWidth();
    for (var i = 0; i < pixels; ++i) {
        QueuePersonCommand(this.input, command, false);
    }
}

That's all there is to it! It seems like a lot of code, but really we had to handle 4 separate directions. I could condense this down a bit, but then it'd get harder to understand.