Dev Log: Pixel Movement and Event Interaction

pixelMovementEventInteraction1

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.

pixelMovementEventInteraction2

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:

  1. Check if there are any events on your tile (usually “below trigger” events)
  2. 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!

pixelMovementEventInteraction3

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.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *