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_procoradd_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 callstartyourself. - You unregister a task with
__remove_taskwhen 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:
| Reason | When it fires |
|---|---|
:on_update | Every frame, continuously while the scene is on screen. This is the main loop, the place for anything that must run all the time. |
:on_init | Right after the scene is created, just before it appears on screen. The place to set things up. |
:on_transition | When the scene plays its visual transition, the fade in or out between two scenes. |
:on_scene_switch | Just before the scene hands control to another one, that is, when the game changes scene. |
:on_dispose | When the scene closes and frees its resources. The place to clean up what you created. |
:on_warp_start | The moment the player starts moving to another map (this is called a warp), before anything else happens. |
:on_warp_process | Once the player has arrived on the new map, but before the game states are refreshed. |
:on_warp_end | When the map change is finished, just before the new map's transition plays. |
:on_hour_update | When 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_name | When 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 likeScene_Map, or the symbol:anyto 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:
| Feature | Reason | What it does |
|---|---|---|
| Berry growth | :on_update | Grows planted berries using the time system. |
| Dynamic lighting | :on_update, :on_init, :on_warp_end | Updates 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 thenameof a lifecycle task. Used to remove it later.event_id: which character triggers it.-1is the player,-2its first follower,-3the second, and so on; a positive number is a map event by its id;:anymatches every character. Defaults to:any.map_id: which map it triggers on, or:anyfor 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):
| Task | Action | What it does |
|---|---|---|
| Repel count | :end_step | Counts down the remaining Repel steps. |
| Poison | :end_step | Applies poison damage while walking. |
| Hatch check | :end_step | Advances eggs toward hatching. |
| Daycare | :end_step | Updates the Day Care state. |
| Step evolution | :end_step | Handles evolutions that depend on steps walked. |
| Battle starting | :begin_step | Rolls for a wild encounter as the step begins. |
| Jump dust | :end_jump | Spawns 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 withadd_proc(block) oradd_message(method on an object); the engine triggers them withstart; you remove them with__remove_task. - Priority orders lifecycle tasks (higher runs first), and
:anytasks 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 withonand remove withdelete; the block receives the character that moved. You can also invent your own action type and fire it withtrigger. - 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.