A common feature (or request) that I see is the use of “Pixel Movement” in games. Since I don’t understand how it works fully myself, what better way to learn than to implement it.
In this post I describe some basic concepts about movement and positioning on the map, and describe how to implement pixel movement.
At the end of the post, I show a piece of code that accomplishes the actual “pixel movement”, and discuss some of the issues that the script has (and why it is not usable).
How maps and positions work
In RM, the map you walk on is represented by a “tile map”, which you can think of as a grid of tiles. Each tile is 32 pixels in width and 32 pixels in height.
All character positions are represented in terms of which tile you are on, so (0, 0) is the upper-left corner of the map, and if you take one step to the right, you are at position (1, 0).
The game keeps track of positions based on the tile map, which I call “tile coordinates” (or tile-coords for short). They are stored in
Game_CharacterBase and defined as follows:
attr_reader :x # map X coordinate (logical) attr_reader :y # map Y coordinate (logical) attr_reader :real_x # map X coordinate (real) attr_reader :real_y # map Y coordinate (real)
Your position is stored in (x, y) as integers.
The “real” (x, y) just means it’s a floating point value. This is used to keep track of your position while you are moving from one tile to another and is used for things like scrolling the map (so you have a smooth scroll rather than a choppy one).
Introducing pixel movement
In the default system, whenever you take a step, you move one tile. This is what I call tile-based movement.
“Pixel-based Movement” refers to how many pixels you move per step. So in the default system, I would say you move 32 pixels at a time.
But what if you would like to move 16 pixels at a time? Or 8 pixels at a time? Or even 4 pixels at a time? We would need to do something about how our positions are stored. If one tile is represented by 32 pixels, then doing a little math we have something like this:
- 16 pixels = 0.5 tiles
- 8 pixels = 0.25 tiles
- 4 pixels = 0.125 tiles
So rather than storing our position as integers, why not just store it as real values?
Implementing our pixel movement
When you look at how movement works, you will find that it is pretty straightforward. This is how
move_straight is implemented in
def move_straight(d, turn_ok = true) @move_succeed = passable?(@x, @y, d) if @move_succeed set_direction(d) @x = $game_map.round_x_with_direction(@x, d) @y = $game_map.round_y_with_direction(@y, d) @real_x = $game_map.x_with_direction(@x, reverse_dir(d)) @real_y = $game_map.y_with_direction(@y, reverse_dir(d)) increase_steps elsif turn_ok set_direction(d) check_event_trigger_touch_front end end
From here you can see that the relevant methods are defined in
Game_Map. Suppose I want to move 16 pixels per step, which is equivalent to moving half a tile. This is all I need to do
class Game_Map def tiles_per_step 0.5 end def x_with_direction(x, d) x + (d == 6 ? tiles_per_step : d == 4 ? -tiles_per_step : 0) end def y_with_direction(y, d) y + (d == 2 ? tiles_per_step : d == 8 ? -tiles_per_step : 0) end def round_x_with_direction(x, d) round_x(x + (d == 6 ? tiles_per_step : d == 4 ? -tiles_per_step : 0)) end def round_y_with_direction(y, d) round_y(y + (d == 2 ? tiles_per_step : d == 8 ? -tiles_per_step : 0)) end end
And now we are moving at half a tile per step, or 16 pixels. Which is what we wanted!
Impacts on the tilemap
There are a number of issues, however, that comes with changing how your position is stored.
Remember that a map is stored in a tilemap? This means that all of the tiles are referenced in terms of tile coordinates. This means that if you want to get the tile in the upper-left corner, you would access it as (0, 0).
So what happens if you are at position (0.5, 0) because you only move half a tile at a time? Fortunately, the game truncates your arguments.
# Initialize a test table, which is how tile info is stored t = Table.new(2,2) t[0,0] = 1 t[0,1] = 2 t[1,0] = 3 t[1,1] = 4 # Test some values puts t[0.1,0] // => 1, same as (0, 0) puts t[0.9,0] // => 1, same as (0, 0) puts t[1,0] // => 3, same as (1, 0)
Is this correct? In terms of backwards compatibility, I would say so. Since the map editor only supports 32×32 tiles, it is ok to assume that if you are walking anywhere within one tile, then you would use the properties for that particular tile.
Impacts on Collision
Collision detection becomes an issue. Collision is used in different places
- Checking whether a tile is passable
- Checking whether an event will be triggered
Remember that it truncates your position, so even if you’re at (0.9, 0), it will still think you’re at (0, 0). Now, if you’re at (0.9, 0), your sprite is already very close to the next tile beside it. If the next tile is a wall, you will look like you’re sticking in a wall.
Why? Because the way collision is checked is check a single-point, namely your position, and NOT the size of your sprite.
This same problems occurs with event triggers, except with events the issue is more obvious. Event triggering again uses character positions for comparison, but they check whether the values are exactly the same.
This means that if you’re standing at (1.5, 0) and the event is at (1.0, 0), even if you look like you’re touching it, it isn’t triggered. You have to be exactly aligned with the event to trigger it.
There is plenty of room for improvement, however, at this point we have a very basic pixel movement system working (and I use the term “working” very loosely).
Spread the Word
If you liked the post and feel that others could benefit from it, consider tweeting it or sharing it on whichever social media channels that you use. You can also follow @HimeWorks to get the latest updates or suggest topics that you would like to read about.
[…] previous dev log, we explored how movement is done and I provided a very bare-bones implementation of “pixel […]
[…] article is part of the pixel movement dev log series. In the first part, we looked at how to customize the map’s movement grid from 32×32 pixels to alternative […]