Performance Analysis: Event Trigger Checks

performanceAnalysis_eventTriggerChecking0

This is a series of articles dedicated to performance analysis in RPG Maker. The purpose is to understand what causes performance issues which result in what developers or players usually refer to as “lag”.

This article is focused on one aspect of the engine: checking for events triggers.

I assume you are familiar with events in RPG Maker. If you are not, I would recommend looking at what they offer in the editor first.

What is Event Trigger Checking

Whenever you wish to interact with an event, or you bump into an event, or a switch is turned ON or OFF, there is the possibility that the event will be triggered, which will then execute the commands that it has been set up with.

RPG Maker comes with five different triggers by default:

  • Action Trigger – runs when you interact with it using the action button
  • Player Touch – runs when the player touches the event, or uses the action button
  • Event Touch – runs when either the event or player touch each other, or the action button
  • Autorun – automatically runs
  • Parallel Process – automatically runs in parallel

Additional triggers can be defined as well, some of which are available in this script.

Each event page has its own trigger, and the trigger is only relevant if the page is active.

Understanding Trigger Checking

The Scenario

performanceAnalysis_eventTriggerChecking1

We start with a simple scenario: you’re standing in front of a counter and you want to talk to the person behind the counter. The person behind the counter is typically implemented as an event using an action trigger, and triggering the event will lead to some sort of discussion via “show text” commands.

The Code

Let’s look at how the code is processed in the default engine. To trigger the event, we press the action button, which is the “C” button (Z or enter keys by default). You cannot trigger events while you are moving, so your player will be standing on the spot when the button is pressed.

When the game runs its update logic, it will update the player while it’s not moving. We look at the Game_Player class

def update_nonmoving(last_moving)
  return if $game_map.interpreter.running?
  if last_moving
    $game_party.on_player_walk
    return if check_touch_event
  end
  if movable? && Input.trigger?(:C)
    return if get_on_off_vehicle
    return if check_action_event     ###
  end
  update_encounter if last_moving
end

It all begins with the check for an action event. Performing a search for the method definition, we see it in the same class:

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

It should be pretty obvious from the method names what the code’s doing:

  1. Check for any events “here” (where you’re standing on”)
  2. Check for any events “there”, if no events were “here”

Keep in mind the arrays of integers that are being passed to the method as their meaning becomes apparent later.

Checking Events “Here”

Searching for the definition for checking event triggers here leads to

def check_event_trigger_here(triggers)
  start_map_event(@x, @y, triggers, false)
end

You can see that the player’s position is passed on, as well as the array of integers from before, and a boolean. The start_map_event method is right above it:

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

In the engine, triggers are represented as numbers. They are mapped as follows:

performanceAnalysis_eventTriggerChecking2

  • Action Trigger = 0
  • Player Touch = 1
  • Event Touch = 2
  • Autorun = 3
  • Parallel Process = 4

Recall from earlier that we passed in a 0. This means that the event will only be triggered if it has an action trigger.

Priority refers to the “above character”, “same as character”, or “below character” settings. Normal priority just means it’s on the same level as the player. Because we passed in a false, that means the event must not have the “same as character” priority (though it could be below or above the character).

So basically this method grabs all of the events on the map that

  1. At the same position as the character, and
  2. Have an action trigger, and
  3. Have below or above priority

and starts them.

Checking Events “There”

If there were no events that satisfied any of the criteria above, we move into this method.

By now, you should be able to follow the logic with the triggers and the positions and the priority. I only mention this because it is important to know what it’s doing.

Looking at the definition:

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


What it’s doing should be intuitive

  • It checks the position immediately in front of you for events
  • It also checks the position two tiles in front of you if there’s a counter in front of you, and no events were found earlier

For the most part, it’s pretty straightforward, and it seems very intuitive: if you want to know if there are any events in front of you or under you, just check if there are any events here and there right?

The Point?

You may be wondering how any of what you just read had anything to do with performance analysis. And indeed, I didn’t talk about performance at all. All I did was tell you something about what the engine does when you press the action button when you’re standing still.

Hopefully it has taught you something, or served as a review in case you haven’t looked at at it for a long time. However, understanding the code is a necessary step before we begin an analysis on performance.

A Subtlety

If you paid attention to what I wrote, you may have noticed that I briefly went over a few points in the code. Let’s look at the start_map_event method again

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

Earlier I said the engine simply grabs all of the events on the map that satisfies the following condition and starts them. Well, this is not entirely accurate.

What the code really does is take ALL of the events and then checks each of them one by one.

Is there a difference in what I said? Not really, one just seems more natural and the other is basically translating the code line by line.

What’s important is what’s going on when the engine grabs all of the events.

How Many Events are you Checking?

Let’s take a closer look at what happens when you want to grab all of the events on the map at a certain position. In Game_Map, we find the method

def events_xy(x, y)
  @events.values.select {|event| event.pos?(x, y) }
end

The engine takes all of the events and selects those whose position is equal to the (x, y) position you are looking for. It then returns this filtered list of events, which we then proceed to check which ones can be started.

There is a potential problem here. As the number of events grow, so does the amount of time required to filter the list.

How Many Times are you Checking?

When we press the action button, we don’t just check once. In fact, we make at least two checks: once to check for events under you, and another to check for events in front of you. And if there’s a counter, you’ll perform another check.

This means that you’re performing event filtering up to three times every time you press the action button.

But OK, three times. Doesn’t sound like a big deal. I mean, if you had 500 events, it just means you’re checking 1500 events.

Recall that we have 5 different types of triggers. We have only looked at one of them. Let’s look at another.

Event Checking with Player Touch

Return to the update_nonmoving method and you should see that touch events are checked if you were last moving. It calls this method to perform the actual touch event checks

def check_touch_event
  return false if in_airship?
  check_event_trigger_here([1,2])
  $game_map.setup_starting_event
end

It performs one check, which doesn’t seem too bad. But this check is performed every time you take a step (unless you’re in an airship of course). So if you had 500 events on the map, every step you take requires 500 checks just to determine whether you’re touching any events.

Event Checking with Event Touch

As I mentioned before, event touch can be triggered if a player touches the event, or a event touches the player. We already know how the engine checks for touch events from above. How does it check whether an event touches the player?

Fortunately, this is not too bad. In Game_Event:

def check_event_trigger_touch(x, y)
  return if $game_map.interpreter.running?
  if @trigger == 2 && $game_player.pos?(x, y)
    start if !jumping? && normal_priority?
  end
end

It simply checks the player’s position.

Parallel Process and Autorun Checks

These two triggers are not related to the player or the events on the map. They will be run automatically if the page is active.

This is not a good thing. Recall that when you want to grab all of the events to check whether they can be triggered, the engine grabs all of the events on the map and selects only those that are at a certain position.

Well, parallel and autorun events are also part of that list. And guess what? They don’t matter!

There is no reason to check whether they can be triggered or not, because they will never satisfy any action or touch conditions. All of those methods only worked with triggers 0, 1, and 2. Autorun is trigger 3, and parallel is trigger 4.

Simply including these events for checking is simply a waste of processing power, which directly results in lowered performance.

Checking if an Event has Started

If you look back at the action check and the touch check methods and you’ll see something like this

return if $game_map.any_event_starting?

If any events were starting to run because they satisfied the trigger condition, the engine would stop checking for events to run.

How does it work? We look at the map again:

def any_event_starting?
  @events.values.any? {|event| event.starting }
end

Again, we see something familiar: the engine grabs all of the events on the map and then checks whether any of the events are starting.

Note that this uses the any? method, which is somewhat better. If your first event was running, then it just checks one event and returns true. If you had 500 events and event 250 was running, then it would check half of your events and return true. If none of your events were running, it would check every event, and then return false.

On average, it probably would find an event about half way through the list. In the worst case, it would end up checking every event. Think about your project: on average, how many steps do you take before an event is executed? Well you probably aren’t triggering events every few steps,  but during all this time, the engine has dutifully checked every single event on the map multiple times to make sure it didn’t miss anything.

The worst case is probably more common than the average case.

Starting an Event

Finally, after all this talk about checking whether an event can start, we look at how an event actually starts. If you took the initiative to look at the start method in Game_Event, you’ll see that it doesn’t actually start anything.

def start
  return if empty?
  @starting = true
  lock if trigger_in?([0,1,2])
end

Instead, it only sets a flag that tells the engine that it’s starting.
The actual event starting occurs in the map class, something called setup_starting_event which you should have seen already in the action and touch check methods:

def setup_starting_event
  refresh if @need_refresh
  return true if @interpreter.setup_reserved_common_event
  return true if setup_starting_map_event
  return true if setup_autorun_common_event
  return false
end

Assuming you aren’t running a common event, it will proceed to try to start a map event.

def setup_starting_map_event
  event = @events.values.find {|event| event.starting }
  event.clear_starting_flag if event
  @interpreter.setup(event.list, event.id) if event
  event
end

This is another beautiful method that goes and iterates over every event on the map and looks for an event that has specified that it is starting. Similar to the any? method, once it finds an event that is starting, it will stop and return that to the caller.

Again, this method is called whenever an action or touch check is performed regardless of whether there are actually any events that can run, and on average you are probably not going to be running any events every 3 steps, or 30 steps even, so this method ends up searching through the whole list of events and finds nothing.

Very productive use of processing power.

Summary

At this point it should probably be obvious that we have some potential issues in our event checking logic. We have identified three steps in the process that involves doing a lot of work and, most of the time, not getting anything useful out of it:

  • Checking whether there are any events to execute
  • Checking whether any events have started
  • Checking where there are any events to start

All of which involve checking every event on the map, every time it is called, which is quite often. Whether this is an actual source of performance degradation is up to you, but this seems like something to look into.

Naturally, it is better safe than sorry. The way the methods are currently written guarantee correctness.

I think the only safe way to avoid performance issues due to event checking is to stay very still and press nothing except maybe browse the party menu. But then you don’t have much of a game.

On the other hand, you can simply limit the amount of events on your map. That way, even if every event is checked almost all the time, there aren’t too many events to check.

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...

2 Responses

  1. Kaelan says:

    I’m writing a tactics-style game, and I had a similar problem when implementing the combat system. Every time I needed to find whether a unit was standing on a tile, I had to check the position of every single unit on the map. For 5 or 6 units, this is fine, but when there’s more, it can get very expensive.

    I fixed this by adding an extra array the size of the map, representing each tile. For each tile [x,y], it would have a reference (I only do 1 unit per tile, but you could have a list of references if you want) to whatever is standing on it.

    Whenever something moves, you update its reference in the list. That way, you can do “find” operations as often as you want per frame without much of a performance impact, since now it’s just an O(1) array lookup instead of an O(n) search.

    You’d probably want something like that in a game with dozens (or more) events on-screen at once.

    • Hime says:

      That's one of the solutions I will be discussing in the next article. I like how it works, but there are some complications that I think might come up.

      One of the issues I've had with it is what it means for something to "move", and how to guarantee that your extra array reflects the state of the game somewhat accurately. That is, it doesn't have to be accurate all the time, but it has to be accurate when I'm checking whether an event can be triggered or not.

      How many different ways are there to change an event's position? I can think of a few off the top of my head, but it would be pretty terrible if I missed one. There is also the possibility that others would include their own way to change event positions that are beyond my knowledge.

      Taking it even further, suppose an event was spawned dynamically. Would the array know about this event? Or what happens if an event was deleted?

      Granted, these are outside of the scope of the default engine (where everything is static, even if it ‘s “erased”) and not something you had to consider in your own project, but there are already enough scripts that work with dynamic data to the point where it's probably going to come up as a compatibility issue in a few projects that might be interested in some optimization.

Leave a Reply

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