Aller au contenu principal
Voir

Scheduler

Le Scheduler exécute son code à des moments précis sans modifier le moteur : les événements du cycle de vie d'une scène comme chaque frame ou un warp, et les actions sur la carte comme un pas ou un saut. Ce guide couvre les deux schedulers et quand utiliser chacun.

Pourquoi il existe

Une scène PSDK fait déjà beaucoup pendant sa vie : elle s'initialise, se met à jour à chaque frame, gère les warps, se libère. La carte fait aussi son propre travail : elle déplace le joueur, le fait sauter, compte ses pas. Quand on veut ajouter un comportement à l'un de ces moments, le mauvais réflexe est de modifier le code source du moteur. La prochaine mise à jour l'écraserait, et on se battrait contre le moteur à chaque version.

Le Scheduler est l'alternative propre. Au lieu de modifier le moteur, on enregistre une tâche : un morceau de code plus le moment où il doit s'exécuter. Le moteur l'exécute au bon moment. C'est le compagnon runtime du monkey-patch : le monkey-patch change ce que fait une méthode, le Scheduler ajoute du code à des moments bien connus sans toucher à aucune méthode.

Deux façons de planifier

PSDK fournit deux schedulers complémentaires, et la première chose à bien cerner est celui auquel un besoin donné appartient.

  • Les tâches de cycle de vie (Scheduler) réagissent à la vie d'une scène : chaque frame, à son initialisation, lors d'un warp, à sa libération. À utiliser pour tout ce qui est lié à une scène affichée à l'écran.
  • Les tâches d'événement (Scheduler::EventTasks) réagissent aux actions d'un personnage sur la carte : un pas, un saut, une glissade. À utiliser pour tout ce qui est lié au déplacement du joueur (ou d'un événement).

La suite du guide couvre chacune, puis comment choisir.

Les tâches de cycle de vie

Comment ça marche

Le scheduler de cycle de vie est un registre de tâches indexé par deux clés : une raison (le genre de moment) et une classe de scène (où elle s'applique).

# Conceptuellement :
tasks[raison][classe_de_scene] = [tache, tache, ...]

Il n'y a que trois opérations, et bien les distinguer lève l'essentiel de la confusion :

  • On enregistre une tâche avec add_proc ou add_message. C'est ce qu'on écrit.
  • Le moteur déclenche une raison avec start, ce qui exécute toutes les tâches enregistrées dessous. Le moteur appelle déjà cela à chaque point du cycle de vie, donc on n'appelle quasiment jamais start soi-même.
  • On désenregistre une tâche avec __remove_task quand elle n'est plus nécessaire.

En résumé : notre rôle est d'enregistrer ; c'est le moteur qui déclenche.

Les raisons

Une raison est le moment du cycle de vie qui déclenche une tâche. Il en existe dix, chacune déclenchée par le moteur à un point précis :

RaisonQuand elle se déclenche
:on_updateÀ chaque frame, en continu tant que la scène est affichée. C'est la boucle principale, l'endroit pour tout ce qui doit tourner en permanence.
:on_initJuste après la création de la scène, avant qu'elle n'apparaisse à l'écran. L'endroit pour préparer ses éléments.
:on_transitionQuand la scène joue sa transition visuelle, le fondu d'entrée ou de sortie entre deux scènes.
:on_scene_switchJuste avant que la scène ne cède la place à une autre, c'est-à-dire quand le jeu change de scène.
:on_disposeQuand la scène se ferme et libère ses ressources. L'endroit pour nettoyer ce qu'on a créé.
:on_warp_startAu moment où le joueur commence à passer sur une autre carte (on appelle ça un warp), avant tout le reste.
:on_warp_processUne fois le joueur arrivé sur la nouvelle carte, mais avant que les états du jeu ne soient rafraîchis.
:on_warp_endQuand le changement de carte est terminé, juste avant que la transition de la nouvelle carte ne se joue.
:on_hour_updateQuand l'horloge en jeu passe à l'heure suivante (système jour/nuit), par exemple pour rafraîchir les Pokémon sauvages disponibles.
:on_getting_tileset_nameQuand le moteur de carte choisit quel tileset, la planche de tuiles graphiques, charger pour la carte courante.

:on_update est celle qu'on utilise le plus souvent : c'est la boucle par frame, l'équivalent du update d'une scène, mais apporté depuis l'extérieur.

Enregistrer : add_proc ou add_message

Il y a deux façons d'enregistrer, et le choix ne porte que sur l'endroit où vit le code.

add_proc exécute un bloc. On l'utilise pour de la logique inline :

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

add_message envoie une méthode à un objet. On l'utilise quand le comportement vit déjà dans une méthode d'un objet qu'on a sous la main :

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

Les deux prennent les mêmes quatre premiers arguments :

  • raison : l'une des raisons ci-dessus.
  • klass : la classe de scène à laquelle la tâche appartient (une classe comme Scene_Map, ou le symbole :any pour s'exécuter sur toutes les scènes). La tâche ne se déclenche que tant qu'une scène de cette classe est active.
  • name : une chaîne identifiant la tâche. On la préfixe d'un namespace (par exemple 'MyGame::MapHud') pour qu'elle soit unique et facile à retirer plus tard.
  • priority : un entier qui ordonne la tâche par rapport aux autres (voir plus bas).

Il y a une différence subtile mais importante. add_message capture l'objet au moment de l'enregistrement, donc $map_hud doit déjà exister quand on enregistre. add_proc évalue son bloc à chaque exécution de la tâche, donc il peut lire un global défini plus tard. Quand la cible peut ne pas encore exister, on privilégie add_proc avec la navigation sûre, comme montré ci-dessus.

Priorité et :any

Quand plusieurs tâches partagent la même raison et la même classe, la priorité décide de l'ordre : un numéro de priorité plus élevé s'exécute en premier. Une tâche enregistrée avec la priorité 1000 s'exécute avant une enregistrée avec 100.

La classe :any est spéciale. Ses tâches s'exécutent pour toutes les scènes de cette raison, et elles s'exécutent avant les tâches liées à une classe précise. Donc avec à la fois une tâche :any et une tâche Scene_Map sur :on_update, la tâche :any s'exécute en premier sur la carte. On utilise :any pour un comportement qui doit suivre le joueur à travers toutes les scènes (un raccourci global, une surcouche de debug), et une classe concrète quand le comportement n'a de sens que dans une seule scène.

Retirer une tâche, et un mot sur start

Une tâche vit jusqu'à ce qu'on la retire. On utilise __remove_task avec les mêmes raison, classe, nom et priorité que ceux de l'enregistrement :

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

Le name et la priority sont ce qui permet au Scheduler de retrouver la tâche exacte à supprimer, d'où l'importance d'un nom unique et préfixé. C'est ainsi qu'une fonctionnalité que l'on active et désactive (un effet météo, une lumière dynamique) s'enregistre quand elle est activée et se retire quand elle est désactivée.

On croise Scheduler.start(raison, classe_de_scene) dans le moteur, mais on l'appelle rarement soi-même : c'est le moteur qui déclenche une raison, pas nous. Par exemple, Graphics.update exécute Scheduler.start(:on_update) à chaque frame, et une scène exécute Scheduler.start(:on_init, self.class) dans son main. On n'appelle start que si l'on invente sa propre raison, ce qui implique aussi de la déclarer d'abord dans Scheduler.init, et qu'on choisit où la déclencher. C'est rare (voir Déclencher sa propre action pour l'équivalent plus simple côté événements).

Ce que PSDK planifie ainsi

Ces tâches ne reposent pas sur une API spéciale : les fonctionnalités du moteur lui-même sont construites sur exactement les appels ci-dessus. Les voir rend le rôle concret :

FonctionnalitéRaisonCe qu'elle fait
Croissance des baies:on_updateFait pousser les baies plantées via le système de temps.
Lumière dynamique:on_update, :on_init, :on_warp_endMet à jour les sources de lumière de la carte à chaque frame et les reconstruit au changement de carte.
Surcouche de debug:on_update (:any)Rafraîchit le débogueur par-dessus chaque scène.
Soft reset:on_update (:any)Surveille la combinaison de touches de soft reset sur chaque scène.

Le système de lumière dynamique, par exemple, enregistre sa mise à jour par frame sur la carte et la retire quand elle n'est plus nécessaire, exactement comme le ferait notre propre code :

Scheduler.add_message(:on_update, Scene_Map, 'NuriYuri::DynamicLight', 100, self, :update)
# ...plus tard, une fois désactivé :
Scheduler.__remove_task(:on_update, Scene_Map, 'NuriYuri::DynamicLight', 100)

Un exemple complet

Supposons que l'on ait construit un HUD personnalisé qui doit se rafraîchir à chaque frame tant que le joueur est sur la carte. On place un seul script dans son projet (par exemple scripts/My_Project/0001 Map HUD.rb) qui enregistre la tâche au chargement :

module MyProject
# Le HUD de carte personnalisé, créé et stocké dans un global à l'ouverture de la carte.
module_function

def update_map_hud
$map_hud&.update
end
end

# Enregistrement unique, au chargement du script. Le moteur s'occupe du reste.
Scheduler.add_proc(:on_update, Scene_Map, 'MyProject::MapHud', 100) do
MyProject.update_map_hud
end

C'est toute l'intégration. Comme la tâche est liée à Scene_Map, elle ne s'exécute que sur la carte, jamais en combat ni dans un menu, donc aucun test de scène n'est nécessaire dans le bloc. On n'a modifié aucun fichier du moteur, et la prochaine mise à jour de PSDK laisse le code intact.

Les tâches d'événement

Le scheduler de cycle de vie ne sait rien du joueur qui se déplace. C'est le rôle du second scheduler, Scheduler::EventTasks. Il réagit aux actions d'un personnage sur la carte, et c'est lui qui fait tourner les mécaniques liées au déplacement.

Comment ça marche

Une tâche d'événement est enregistrée contre trois choses : un type d'action, un événement et une carte. Quand un personnage effectue cette action, le moteur déclenche les tâches correspondantes et leur transmet le personnage qui s'est déplacé.

Les six types d'action sont le début et la fin des trois façons dont un personnage peut se déplacer :

  • :begin_step / :end_step : un pas, case par case.
  • :begin_jump / :end_jump : un saut, comme descendre un rebord ou une commande de déplacement « Saut ».
  • :begin_slide / :end_slide : une glissade, comme sur la glace ou les rapides.

:end_step est de loin la plus courante : « le joueur vient de faire un pas » est le hook derrière la plupart des mécaniques de la carte.

Enregistrer et retirer

On enregistre avec on, et le bloc reçoit le personnage qui a déclenché la tâche :

# Exécuter du code chaque fois que le joueur termine un pas
Scheduler::EventTasks.on(:end_step, 'MyGame::StepCounter', -1) do |event|
$game_variables[STEP_COUNT_ID] += 1
end

Les arguments sont :

  • task_type : l'une des six actions ci-dessus.
  • description : une chaîne identifiant la tâche, comme le name d'une tâche de cycle de vie. Sert à la retirer plus tard.
  • event_id : quel personnage la déclenche. -1 est le joueur, -2 son premier suiveur, -3 le deuxième, et ainsi de suite ; un nombre positif est un événement de la carte par son id ; :any correspond à tous les personnages. Vaut :any par défaut.
  • map_id : sur quelle carte elle se déclenche, ou :any pour toutes les cartes. Vaut :any par défaut.

Le bloc est appelé avec |event, event_id, map_id| : le Game_Character qui s'est déplacé, et (utile pour les tâches :any) quel événement et quelle carte c'était.

On retire une tâche avec delete, en réutilisant les mêmes type, description, événement et carte :

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

Déclencher sa propre action

Les six actions ci-dessus sont celles que le moteur de carte déclenche, mais le système ne les code pas en dur : EventTasks accepte n'importe quel type d'action qu'on invente. Pour ajouter le sien, on enregistre un handler avec on sous un nouveau symbole, puis on appelle trigger là où cette action doit se produire, en passant le personnage concerné :

# Une action personnalisée que l'on déclenche soi-même depuis son propre code (monkey-patché)
Scheduler::EventTasks.on(:begin_surf, 'MyGame::SurfSplash', -1) do |event|
# jouer une éclaboussure là où le joueur commence à surfer
end

# ...à l'endroit où le surf commence, dans son patch :
Scheduler::EventTasks.trigger(:begin_surf, $game_player)

Cela fonctionne parce qu'un type de tâche d'événement est créé à la demande. Le scheduler de cycle de vie est plus strict : ses raisons sont une liste fixe mise en place au démarrage, et enregistrer une tâche sur une raison inconnue est silencieusement ignoré. Ajouter une raison de cycle de vie inédite implique de patcher Scheduler.init pour la déclarer d'abord. C'est pourquoi, en pratique, on réutilise les dix raisons existantes et on n'invente de nouveaux déclencheurs que du côté des événements.

Ce que PSDK planifie ainsi

L'essentiel de ce qui se produit « pendant que le joueur marche » est une tâche d'événement sur le joueur (event_id -1) :

TâcheActionCe qu'elle fait
Repousse:end_stepDécompte les pas de Repousse restants.
Poison:end_stepApplique les dégâts de poison pendant la marche.
Éclosion:end_stepFait progresser les œufs vers l'éclosion.
Pension:end_stepMet à jour l'état de la Pension.
Évolution à la marche:end_stepGère les évolutions qui dépendent du nombre de pas.
Démarrage de combat:begin_stepTire une rencontre sauvage au début du pas.
Poussière au saut:end_jumpCrée des particules de poussière ou d'eau là où un personnage atterrit.

La tâche de poussière au saut est un bon modèle car elle s'applique à tous les personnages, pas seulement au joueur :

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

Lequel utiliser ?

  • Le comportement doit s'exécuter en continu ou à une frontière de scène (chaque frame, à l'init, sur un warp, à la libération) : on utilise une tâche de cycle de vie (Scheduler.add_proc / add_message).
  • Le comportement doit s'exécuter quand un personnage se déplace sur la carte (un pas, un saut, une glissade) : on utilise une tâche d'événement (Scheduler::EventTasks.on).

Un compteur de pas est une tâche d'événement. Un rafraîchissement de HUD par frame est une tâche de cycle de vie. En cas de doute, on se demande si le déclencheur est « le joueur a fait quelque chose » (tâche d'événement) ou « la scène a atteint un moment » (tâche de cycle de vie).

Conclusion

  • Le Scheduler exécute son code à des moments précis sans modifier le moteur, à travers deux systèmes complémentaires.
  • Les tâches de cycle de vie (Scheduler) sont indexées par une raison et une classe de scène. On enregistre avec add_proc (bloc) ou add_message (méthode sur un objet) ; le moteur les déclenche avec start ; on les retire avec __remove_task.
  • La priorité ordonne les tâches de cycle de vie (la plus haute s'exécute en premier), et les tâches :any s'exécutent sur toutes les scènes, avant celles liées à une classe précise.
  • Les tâches d'événement (Scheduler::EventTasks) sont indexées par une action (:end_step, :end_jump, ...), un événement et une carte. On enregistre avec on et on retire avec delete ; le bloc reçoit le personnage qui s'est déplacé. On peut aussi inventer son propre type d'action et le déclencher avec trigger.
  • Les fonctionnalités de PSDK reposent sur les deux : la lumière dynamique et la croissance des baies sont des tâches de cycle de vie ; le Repousse, le poison, l'éclosion et la poussière au saut sont des tâches d'événement.
  • On choisit selon le déclencheur : un moment de scène est une tâche de cycle de vie, un personnage qui se déplace est une tâche d'événement.