Dev Log: Pixel Movement and Event Interaction
This article is part of the pixel movement dev log series. The script so far can be found here.
In the first part, we looked at how to customize the map’s movement grid from 32×32 pixels to alternative sizes such as 16×16, 8×8, or 4×4.
In the second part, we explored how our new movement grid could be integrated with the tilemap system using a new collision detection technique because our movement grid now uses a different size than the tile grid.
As mentioned in the previous section, there are two general places where collision detection takes place:
- Map passability
- Event interaction
We have the map passage settings covered, so next we want to look at how we should handle interaction between characters on the map, such as players and events.
Background
In RPG Maker, we have objects on the maps. These objects are stored as Game_Character
objects, which encompasses every non-map object that you see including events, followers, vehicles, and the player. Characters interact with each other in various ways
- The player can trigger events
- Events can touch the player to trigger itself
- The player can board vehicles
All of these operations require some form of collision detection to determine whether the trigger conditions are met.
Let’s look at how it works in the default system to get an idea what we need to do.
Analyzing event triggering
Consider a simple use case: you talk to a librarian over a counter.
To interact with the event, you would press the action key. Let’s look at how the engine handles this. In Game_Player
there is a method called update_nonmoving
:
def update_nonmoving(last_moving) return if $game_map.interpreter.running? if last_moving $game_party.on_player_walk return if check_touch_event #
We see that this is where touch triggers or action triggers are processed. We are interested in check_action_event
:
def check_action_event return false if in_airship? check_event_trigger_here([0]) # (1) return true if $game_map.setup_starting_event check_event_trigger_there([0,1,2]) # (2) $game_map.setup_starting_event end
Two things happen when you press the action button:
- Check if there are any events on your tile (usually “below trigger” events)
- Check if there are any events across from you (if there’s a counter, check the tile across from that)
Since we are interested in how the engine checks events, let’s look at how the event is triggered.
def check_event_trigger_here(triggers) start_map_event(@x, @y, triggers, false) end def start_map_event(x, y, triggers, normal) return if $game_map.interpreter.running? $game_map.events_xy(x, y).each do |event| #<---- if event.trigger_in?(triggers) && event.normal_priority? == normal event.start end end end
Now we see how it’s gathering events. It calls $game_map.events_xy
and passes in a tile position.
def events_xy(x, y) @events.values.select {|event| event.pos?(x, y) } end
Which basically grabs all of the events on the map at the specified (x, y) position.
Single-Point Check Again
The first problem that should jump out to you is the familiar Single-Point Check. Recall that the original design was that you have your tilemap, and characters move from tile to tile per step. This means that if you wanted to know whether there was an event in front of you, you could simply check that one position and get all of the required events.
With a custom movement grid on top of a 32×32 tile map, your sprites will overlap, but your positions will not be equal.
Area based position checking
Just like before, we use the assumption that a bounding box to represents the character collision areas. Rather than directly checking whether you collide with an event directly, we go with the approach that the engine uses: we simply return a list of events within a given region and assume those collide with the player.
Assuming our bounding box is 32×32 around the sprite (exactly one tile), let’s introduce an area-based position check. Note that the sprite’s origin is centered in the center of the sprite.
class Game_CharacterBase def pos_area?(x, y) x_lo = @x - 0.5 y_lo = @y - 0.5 x_hi = @x + 0.5 y_hi = @y + 0.5 x >= x_lo && x <= x_hi && y >= y_lo && y <= y_hi end def pos_area_nt?(x, y) !@through && pos_area?(x, y) end end
And when we fetch events, we want to use the new area-based position checks:
class Game_Map def events_xy(x, y) @events.values.select {|event| event.pos_area?(x, y)} end def events_xy_nt(x, y) @events.values.select {|event| event.pos_area_nt?(x, y) } end end
And now you can interact with events as you would expect!
Counter Tiles
Currently, we can check event triggers for events that are right in front of you, or right under you. However, if you tried to talk to someone over the counter, it won’t work. Let’s look at the definition of check_event_trigger_there
:
def check_event_trigger_there(triggers) x2 = $game_map.round_x_with_direction(@x, @direction) y2 = $game_map.round_y_with_direction(@y, @direction) start_map_event(x2, y2, triggers, true) return if $game_map.any_event_starting? return unless $game_map.counter?(x2, y2) #<--- x3 = $game_map.round_x_with_direction(x2, @direction) y3 = $game_map.round_y_with_direction(y2, @direction) start_map_event(x3, y3, triggers, true) end
We see the single-point check at work here again. Let’s go back to our pixel movement methods and store the original tile-based movement methods somewhere:
class Game_Map include TH::Pixel_Movement alias :tile_x_with_direction :x_with_direction alias :tile_y_with_direction :y_with_direction alias :round_tile_x_with_direction :round_x_with_direction alias :round_tile_y_with_direction :round_y_with_direction
We need to change the way counters are checked, because it needs to check an area like everything else:
class Game_CharacterBase def counter_tile_area?(x, y) delta = Tiles_Per_Step * 2 x_lo = x + delta y_lo = y + delta x_hi = x + 1 - delta y_hi = y + 1 - delta return true if $game_map.counter?(x_lo, y_lo) return true if $game_map.counter?(x_lo, y_hi) return true if $game_map.counter?(x_hi, y_lo) return true if $game_map.counter?(x_hi, y_hi) return false end end
And finally, we update how to check across the counter
class Game_Player < Game_Character def check_event_trigger_there(triggers) x2 = $game_map.round_x_with_direction(@x, @direction) y2 = $game_map.round_y_with_direction(@y, @direction) start_map_event(x2, y2, triggers, true) return if $game_map.any_event_starting? return unless counter_tile_area?(x2, y2) x3 = $game_map.round_tile_x_with_direction(x2, @direction) + 0.5 y3 = $game_map.round_tile_y_with_direction(y2, @direction) + 0.5 # offset due to sprite origin being in the center, not corner x3 += @direction == 6 ? 0.5 : @direction == 4 ? -0.5 : 0 y3 += @direction == 2 ? 0.5 : @direction == 8 ? -0.5 : 0 start_map_event(x3, y3, triggers, true) end end
Now we can trigger events across counters!
Events can’t move!
One issue came up, and it’s related to the way event collision is handled. Because we use an area check instead of a single point, the event will always have itself in its own collision area because the map doesn’t filter the list to exclude the calling character. A simple fix could be done like this
class Game_Event < Game_Character def collide_with_events?(x, y) $game_map.events_xy_nt(x, y).any? do |event| event != self && (event.normal_priority? || self.is_a?(Game_Event)) end end end
Event to Player Collision
Everything above focused on players interacting with an event, and not so much events interacting with players. When you look through the collision checking methods, you’ll find that events never actually check whether they collide with the player. Consequently, this means that an event could run into a player, and now the player is stuck because if it tries to move in one direction, it will collide with the event, and therefore it cannot move.
To address the issue, we need to add a few more checks.
class Game_Event < Game_Character alias :th_pixel_movement_collide_with_characters? :collide_with_characters? def collide_with_characters?(x, y) collide_with_player?(x,y) || th_pixel_movement_collide_with_characters?(x, y) end def collide_with_player?(x, y) $game_player.pos_area?(x, y) end def check_event_trigger_touch(x, y) return if $game_map.interpreter.running? if @trigger == 2 && $game_player.pos_area?(x, y) start if !jumping? && normal_priority? end end end
And finally, I went and overwrote the way check_event_trigger_touch
is defined because it was using single-point checks again.
Closing
After another session, we have managed to implement event triggering into our pixel movement system. After 30 lines of code, we have managed to accomplish the following
- Customize our movement grid into finer units
- Implement proper map passage checks
- Implement event triggering
However, after some quick testing, a few problems immediately arose
- The area checking doesn’t apply to vehicles
- Followers don’t move properly
There are still a list of issues to tackle, but we are slowly getting there.
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 on Twitter or like my Facebook page to get the latest updates or suggest topics that you would like to read about.