Skip to main content
View

Scheduler

The Scheduler runs your code at precise moments without editing the engine: scene lifecycle events like every frame or a warp, and map actions like a step or a jump. This guide covers both schedulers and when to reach for each.

Why it exists

A PSDK scene already does a lot during its life: it initializes, updates every frame, handles warps, disposes itself. The map does its own work too: it moves the player, makes them jump, counts steps. When you want to add behaviour at one of those moments, the wrong reflex is to edit the engine's source. The next update would overwrite it, and you would fight the engine on every release.

The Scheduler is the clean alternative. Instead of editing the engine, you register a task: a piece of code plus the moment it should run. The engine runs it for you at the right time. It is the runtime companion to monkey-patching: monkey-patching changes what a method does, the Scheduler adds code at well-known moments without touching any method at all.

Two ways to schedule

PSDK ships two complementary schedulers, and the first thing to get right is which one a given need belongs to.

  • Scene lifecycle tasks (Scheduler) react to the life of a scene: every frame, when it initializes, on a warp, when it disposes. Use them for anything tied to a scene being on screen.
  • Map event tasks (Scheduler::EventTasks) react to the actions of a character on the map: a step, a jump, a slide. Use them for anything tied to the player (or an event) moving.

The rest of this guide covers each in turn, then how to choose.

Scene lifecycle tasks

How it works

The lifecycle scheduler is a registry of tasks indexed by two keys: a reason (the kind of moment) and a scene class (where it applies).

# Conceptually:
tasks[reason][scene_class] = [task, task, ...]

There are only three operations, and keeping them straight removes most of the confusion:

  • You register a task with add_proc or add_message. This is what you write.
  • The engine triggers a reason with start, which runs every task registered under it. The engine already calls this at each lifecycle point, so you almost never call start yourself.
  • You unregister a task with __remove_task when it is no longer needed.

In short: your job is to register; the engine does the triggering.

The reasons

A reason is the lifecycle moment that triggers a task. There are ten, each fired by the engine at a specific point:

ReasonWhen it fires
:on_updateEvery frame, continuously while the scene is on screen. This is the main loop, the place for anything that must run all the time.
:on_initRight after the scene is created, just before it appears on screen. The place to set things up.
:on_transitionWhen the scene plays its visual transition, the fade in or out between two scenes.
:on_scene_switchJust before the scene hands control to another one, that is, when the game changes scene.
:on_disposeWhen the scene closes and frees its resources. The place to clean up what you created.
:on_warp_startThe moment the player starts moving to another map (this is called a warp), before anything else happens.
:on_warp_processOnce the player has arrived on the new map, but before the game states are refreshed.
:on_warp_endWhen the map change is finished, just before the new map's transition plays.
:on_hour_updateWhen the in-game clock rolls over to a new hour (day and night system), for example to refresh which wild Pokémon are available.
:on_getting_tileset_nameWhen the map engine decides which tileset, the sheet of map tiles, to load for the current map.

:on_update is the one you will reach for most often: it is the per-frame loop, the equivalent of a scene's own update, but contributed from outside.

Registering: add_proc or add_message

There are two ways to register, and the choice is only about where your code lives.

add_proc runs a block. Use it for inline logic:

Scheduler.add_proc(:on_update, Scene_Map, 'MyGame::MapHud', 100) do
$map_hud&.update
end

add_message sends a method to an object. Use it when the behaviour already lives in a method of an object you have at hand:

Scheduler.add_message(:on_update, Scene_Map, 'MyGame::MapHud', 100, $map_hud, :update)

Both take the same first four arguments:

  • reason : one of the reasons above.
  • klass : the scene class the task belongs to (a class like Scene_Map, or the symbol :any to run on every scene). The task only fires while a scene of that class is active.
  • name : a string identifying the task. Namespace it (for example 'MyGame::MapHud') so it is unique and easy to remove later.
  • priority : an integer ordering the task against the others (see below).

There is a subtle but important difference. add_message captures the object at registration time, so $map_hud must already exist when you register. add_proc evaluates its block each time the task runs, so it can read a global that is set later. When the target may not exist yet, prefer add_proc with safe navigation, as shown above.

Priority and :any

When several tasks share the same reason and class, priority decides the order: a higher priority number runs first. A task registered with priority 1000 runs before one registered with priority 100.

The :any class is special. Its tasks run for every scene of that reason, and they run before the tasks tied to a specific class. So with both an :any task and a Scene_Map task on :on_update, the :any task runs first on the map. Use :any for behaviour that must follow the player across every scene (a global hotkey, a debug overlay), and a concrete class when the behaviour only makes sense in one scene.

Removing a task, and a word on start

A task lives until you remove it. Use __remove_task with the same reason, class, name and priority you registered with:

Scheduler.__remove_task(:on_update, Scene_Map, 'MyGame::MapHud', 100)

The name and priority are how the Scheduler finds the exact task to drop, which is why a unique, namespaced name matters. This is how a feature that toggles on and off (a weather effect, a dynamic light) registers itself when enabled and removes itself when disabled.

You will see Scheduler.start(reason, scene_class) in the engine, but you rarely call it yourself: it is the engine triggering a reason, not you. For instance Graphics.update runs Scheduler.start(:on_update) every frame, and a scene runs Scheduler.start(:on_init, self.class) inside its main. You only call start if you invent your own reason, which also means declaring it in Scheduler.init first, and choose where to fire it. This is uncommon (see Firing your own action for the easier event-side equivalent).

What PSDK schedules this way

These tasks are not a special API: the engine's own features are built on the exact calls above. Seeing them makes the role concrete:

FeatureReasonWhat it does
Berry growth:on_updateGrows planted berries using the time system.
Dynamic lighting:on_update, :on_init, :on_warp_endUpdates the map's light sources each frame and rebuilds them on map change.
Debug overlay:on_update (:any)Refreshes the debugger on top of every scene.
Soft reset:on_update (:any)Watches for the soft-reset key combination on every scene.

The dynamic lighting system, for example, registers its per-frame update on the map and removes it when it is no longer needed, exactly as your own code would:

Scheduler.add_message(:on_update, Scene_Map, 'NuriYuri::DynamicLight', 100, self, :update)
# ...later, when disabled:
Scheduler.__remove_task(:on_update, Scene_Map, 'NuriYuri::DynamicLight', 100)

A complete example

Suppose you built a custom HUD that must refresh every frame while the player is on the map. Put a single script in your project (for example scripts/My_Project/0001 Map HUD.rb) that registers the task at load time:

module MyProject
# Your custom map HUD, created and stored in a global when the map opens.
module_function

def update_map_hud
$map_hud&.update
end
end

# Register once, at script load. The engine handles the rest.
Scheduler.add_proc(:on_update, Scene_Map, 'MyProject::MapHud', 100) do
MyProject.update_map_hud
end

That is the whole integration. Because the task is tied to Scene_Map, it only runs on the map, never in battle or in a menu, so no scene check is needed inside the block. You never edited a single engine file, and the next PSDK update leaves your code untouched.

Map event tasks

The lifecycle scheduler knows nothing about the player walking around. That is the job of the second scheduler, Scheduler::EventTasks. It reacts to the actions of a character on the map, and it is what powers movement-driven mechanics.

How it works

An event task is registered against three things: an action type, an event and a map. When a character performs that action, the engine triggers the matching tasks and passes them the character that moved.

The six action types are the start and end of the three ways a character can move:

  • :begin_step / :end_step : a tile-by-tile step.
  • :begin_jump / :end_jump : a jump, such as dropping down a ledge or a "Jump" move command.
  • :begin_slide / :end_slide : a slide, such as on ice or rapids.

:end_step is by far the most common: "the player just took a step" is the hook behind most overworld mechanics.

Registering and removing

You register with on, and the block receives the character that triggered the task:

# Run code every time the player finishes a step
Scheduler::EventTasks.on(:end_step, 'MyGame::StepCounter', -1) do |event|
$game_variables[STEP_COUNT_ID] += 1
end

The arguments are:

  • task_type : one of the six actions above.
  • description : a string identifying the task, like the name of a lifecycle task. Used to remove it later.
  • event_id : which character triggers it. -1 is the player, -2 its first follower, -3 the second, and so on; a positive number is a map event by its id; :any matches every character. Defaults to :any.
  • map_id : which map it triggers on, or :any for every map. Defaults to :any.

The block is called with |event, event_id, map_id|: the Game_Character that moved, and (useful for :any tasks) which event and map it was.

Remove a task with delete, using the same type, description, event and map:

Scheduler::EventTasks.delete(:end_step, 'MyGame::StepCounter', -1, :any)

Firing your own action

The six actions above are the ones the map engine triggers, but the system does not hard-code them: EventTasks accepts any action type you invent. To add your own, register a handler with on under a new symbol, then call trigger wherever that action should happen, passing the character it concerns:

# A custom action you fire yourself from your own (monkey-patched) code
Scheduler::EventTasks.on(:begin_surf, 'MyGame::SurfSplash', -1) do |event|
# play a splash where the player starts surfing
end

# ...at the point where surfing begins, in your patch:
Scheduler::EventTasks.trigger(:begin_surf, $game_player)

This works because an event task type is created on demand. The lifecycle scheduler is stricter: its reasons are a fixed list set up at boot, and registering a task on an unknown reason is silently ignored. Adding a brand-new lifecycle reason means patching Scheduler.init to declare it first. That is why, in practice, you reuse the ten existing reasons and only invent new triggers on the event side.

What PSDK schedules this way

Most of what happens "as the player walks" is an event task on the player (event_id -1):

TaskActionWhat it does
Repel count:end_stepCounts down the remaining Repel steps.
Poison:end_stepApplies poison damage while walking.
Hatch check:end_stepAdvances eggs toward hatching.
Daycare:end_stepUpdates the Day Care state.
Step evolution:end_stepHandles evolutions that depend on steps walked.
Battle starting:begin_stepRolls for a wild encounter as the step begins.
Jump dust:end_jumpSpawns dust or water particles where a character lands.

The jump-dust task is a good template because it applies to every character, not just the player:

Scheduler::EventTasks.on(:end_jump, 'Dust after jumping') do |event|
next if event.particles_disabled

particle = Game_Character::SurfTag.include?(event.system_tag) ? :water_dust : :dust
Yuki::Particles.add_particle(event, particle)
end

Which one should I use?

  • The behaviour must run continuously or at a scene boundary (every frame, on init, on warp, on dispose): use a lifecycle task (Scheduler.add_proc / add_message).
  • The behaviour must run when a character moves on the map (a step, a jump, a slide): use a map event task (Scheduler::EventTasks.on).

A step counter is an event task. A per-frame HUD refresh is a lifecycle task. When in doubt, ask whether the trigger is "the player did something" (event task) or "the scene reached a moment" (lifecycle task).

Conclusion

  • The Scheduler runs your code at precise moments without editing the engine, through two complementary systems.
  • Lifecycle tasks (Scheduler) are indexed by a reason and a scene class. You register with add_proc (block) or add_message (method on an object); the engine triggers them with start; you remove them with __remove_task.
  • Priority orders lifecycle tasks (higher runs first), and :any tasks run on every scene, before the class-specific ones.
  • Event tasks (Scheduler::EventTasks) are indexed by an action (:end_step, :end_jump, ...), an event and a map. You register with on and remove with delete; the block receives the character that moved. You can also invent your own action type and fire it with trigger.
  • PSDK's own features ride on both: dynamic lighting and berry growth are lifecycle tasks; Repel, poison, hatching and jump dust are event tasks.
  • Choose by the trigger: a scene moment is a lifecycle task, a character moving is an event task.