RPG Movement Systems
Category / Tutorial
“A role-playing video game is a video game genre where the player controls the actions of a character immersed in some well-defined world.” - Wikipedia
When I read the quote above, one particular phrase sticks out for me: “a character immersed in some well-defined world.” Role-playing games allow us to experience worlds both similar and strange, from the safety of our own home. But what is the point of being in such a world if you can’t explore it?
In this tutorial we will examine two main ways that role-playing games allow the player to traverse the game world, discussing their advantages and disadvantages as well as explaining how to implement them.
Enough sitting around, let’s get moving!
Setting the scene
All the examples in this article start with the same template, which you can download here (right-click and Save image as...):
Load that cartridge into the PICO-8, move to the sprite editor and you should see that there are five sprites (not including the one that is provided by the PICO-8 by default). The first four are for our player's character, with the sprites laid out in the same order as the d-pad buttons: left, right, up, down. Next is a floor tile, and finally a wall tile.
The wall tile, in position 6, has had it's first flag enabled. You can see the flags as the row of orbs to the bottom-right of the sprite-editing window. Flags allow us to mark specific information about a sprite, which we can use later. For our examples, we use the first flag to mean that we want this tile to be considered solid, and impassable.
We've also included a basic map to test the different movement systems.
Type 1: Simple Movement
The first style is the most basic: the character moves immediately in response to your input keys, in a pixel-perfect fashion. As this is the most intuitive movement system, it should be familiar to game developers of all levels. You’ll have seen it in classic RPGs such as Crystalis or Chrono Trigger.
Of course, while it is easy to implement this kind of movement, there are certain downsides associated with it. By allowing the character to exist at any position on the map, a game’s terrain needs to be designed carefully around the hitbox of the character and sophisticated collision responses may be needed to prevent the player from getting ‘snagged’ on a terrain feature unexpectedly.
Movement-logic for games often follows a similar pattern:
- Process Input
- Check for collisions
- If no collisions, accept the input and move
For the sake of simplicity, we will use a very basic collision response scheme. We will use the player’s input to calculate where the character will move to. Then we can check if that movement would cause a collision. If it does, we cancel the move. In the future, you might want to explore other ways to deal with collisions.
To follow this scheme, we will add the following _update() function:
function _update()
-- compute the input
local dx,dy,df = processInput()
-- check for collisions
if not colliding(p.x+dx, p.y+dy+p.h)
and not colliding(p.x+dx+p.w, p.y+dy+p.h) then
-- there are no collisions
-- so apply the movement
p.x += dx
p.y += dy
p.facing = df
end
end
You should be able to see the same structure we discussed above, even though we are obviously missing some code. The processInput() function will take the player's button presses and work out how the character should move. It looks like this:
function processInput()
-- set the default, for when no buttons are pressed.
local dx = 0
local dy = 0
local df = p.facing
-- check each d-pad button direction
-- and set the appropriate variable, as well
-- as the character's facing direction, df.
if btn(d.left) then
dx = -1
df = d.left
end
if btn(d.right) then
dx = 1
df = d.right
end
if btn(d.up) then
dy = -1
df = d.up
end
if btn(d.down) then
dy = 1
df = d.down
end
return dx, dy, df
end
Usually, we check the state of each button based on its number, between 0 and 3, but that can make code less readable, so we've included a lua table in our template which lets us name each direction for clarity.
Now that we can move, we just a way to check if we are going to collide, which is. where this colliding() function comes in:
function colliding(x, y)
local tilex, tiley = gettile(x, y)
local tile = mget(tilex, tiley)
local flag = fget(tile, 0)
return tile == 0
or flag == true
end
This function does a lot all at once, so we will break it down into steps:
- Work out which map tile the player is about to enter. gettile() takes the player's coordinates and divides each by 8 to work out which tile to check.
- Now we use mget() to find out which sprite is representing that tile.
- Finally, fget() tells us if the 'solid' flag is set for that sprite, or not.
- At this point, we know whether we are about to walk off the map (the tile would equal zero), or the flag is set, so we can return these results. If either case is true, we are considered to be "colliding".
Exit the code editor, save and run the code. You should be able to move around and collide with the edges of the map or the wall tiles. That's all there is to Simple Movement. Now let's look at something a little more advanced.
Tile-based Movement
The hallmarks of tile-based movement are that the player character is locked to a grid system. Pressing a directional button causes the character to move from the current grid square to the next, usually gliding into place over a second or two. This is seen in a number of classic game franchises, including Pokemon, Phantasy Star and the early Final Fantasy games.
Building a tile-based movement system requires a little bit more work than classical movement, however the collision detection system is fortunately less complex, since the player can only enter tiles which are not considered to be solid. This means the game developer does not need to fiddle with hitboxes, making terrain easier to design. As a downside, snapping the player’s movement to a grid reduces the player’s freedom of movement which affects the way combat might take place in an overworld. In addition, the player may feel that this kind of movement is not realistic, breaking the immersion of your game world.
To account for the additional complexity of tile-based movement, we are going to expand the process laid out in the classical movement scheme in the following manner:
- Process Input
If we are not moving, compute our destination.
If we are moving, work out the direction to our destination - If we are about to move:
If the destination is not solid, accept the movement and begin moving
If the destination is solid, ignore the input - If we are already moving, continue moving closer to our destination.
- Go to step 1
Starting from the template, we can begin by modifying the player's state to include variables for the current destination, destx and desty, as well as a variable so we know if we are moving:
-- player state
p = {
x=0, y=0,
destx=0, desty=0,
facing=d.down,
moving=false
}
Next, reuse the processInput() and colliding() function from the Simple Movement section, but with a new _update() implementation:
function _update()
local dx, dy, df
if not p.moving then
dx, dy, df = processinput()
p.destx = p.x + dx * 8
p.desty = p.y + dy * 8
p.facing = df
if colliding(p.destx, p.desty) then
p.destx = p.x
p.desty = p.y
else
p.moving = true
end
end
if p.x != p.destx then
p.x += sgn(p.destx - p.x)
elseif p.y != p.desty then
p.y += sgn(p.desty - p.y)
elseif p.moving then
p.moving = false
end
end
Let's break down what we are doing here:
- If we aren't moving, we want to check if the player has pushed any buttons. The resulting vector only provides us with a direction, so we need to multiply it by 8 to get a destination co-ordinate in pixels for positioning the player.
- We check if the destination is a solid tile, in the same way as before. If it is, we set the destination to our current position, otherwise we set the moving flag to show that we want to start moving.
- Finally, if the destination doesn't match the current position, we compute the vector necessary to move one pixel closer. If we reached our destination, then we stop moving.
That's everything we need for tile-based movement. Exit the code editor, save and run. You should be gliding easily between tiles, without being able to move through the walls. From here, there are infinite possibilities, as you can begin implementing your own classic RPG style worlds.
Good luck and safe travels!