Présentation générale
Le LevelGraph est un éditeur nodal intégré qui permet de définir des logiques personnalisées pour chaque collision présente dans un niveau. Lorsqu'une collision est sélectionnée, cliquez sur l'icône engrenage + clé à molette (en haut à droite de la section "Editing") pour ouvrir l'éditeur nodal associé.

Navigation et interaction
- Clique droit : ouvrir le menu de création de nœud à l'emplacement du curseur
- Scroll : zoomer/dézoomer dans le menu contextuel ou le viewport
- Drag avec molette ou alt+Clique gauche : déplacer la vue dans le viewport
- Drag clique gauche : sélectionner une ou plusieurs nodes
- Supr : supprimer les nodes sélectionnées
- Ctrl+C / Ctrl+V : copier-coller (fonctionne entre plusieurs LevelGraphs)
- Drag sur un pin : connecter à un autre pin ou ouvrir une recherche de node connectée
- Flèche gauche : ouvrir/fermer le panneau de node à gauche (catégorisé)
Types de nœuds
Chaque nœud représente une instruction. On distingue 4 grands types :
- Événements 🔴 (rouge) :
OnEnter
,OnExit
,OnStart
,OnTick
,OnEnd
,OnOverlap
- Actions ⚪ : exécution d'une tâche (ex :
Teleport
,Play Animation
) - Setters 🔧 : modification de données (ex :
Set Player Speed
) - Getters 🟣 : récupération de données (ex :
Get Player Health
)
Les pins blancs sont des exec pins pour déclencher des suites d'actions. Les pins violets sont des data pins pour transmettre des valeurs.

🧠 Nœuds logiques et événements
Pour construire des logiques dynamiques, l'éditeur propose plusieurs nœuds essentiels. Voici un aperçu des plus utilisés :
⚙️ Nœuds de logique
If
— compare deux valeurs (A
etB
) avec un opérateur (==
,!=
,<
, etc.) et exécute selon le résultat (sortiestrue
oufalse
).For
— exécute une bouclen
fois. Utilisez la sortieloop
pour les instructions répétées n fois, etnext
pour continuer à la fin de la boucle, i correspond à l'indice de cette dernière.Sequence
— déclenche une suite d'instructions dans l'ordre (utile pour enchaîner plusieurs actions).FlipFlop
— alterne entre deux sortiesA
etB
à chaque activation.Once
— n'exécute qu'une seule fois, même si le flux repasse dedans. Très utile pour déclencher des actions uniques.Set Variable
/Get Variable
— permet de stocker et réutiliser des valeurs (états, compteurs, flags...)

Set Variable
et Get Variable
sont propres à chaque LevelGraph.
Elles sont réinitialisées à chaque lancement du mode Play, ce qui signifie qu'elles ne conservent pas leur valeur entre deux sessions.
🎯 Nœuds d'événements
Chaque événement représente un déclencheur automatique. Un même événement ne peut être ajouté qu'une seule fois par LevelGraph.
OnEnter
— déclenché lorsque le joueur entre dans la collisionOnExit
— déclenché à la sortie du joueur de la collisionOnOverlap
— déclenché tant que le joueur reste dans la collisionOnStart
— au lancement du mode PlayOnTick
— déclenché à chaque frame (⚠️ à utiliser avec modération)OnEnd
— à la fermeture du mode Play

.lvg
, placé dans le même dossier que le fichier .json
du niveau.
Le chemin absolu de ce fichier est stocké dans le .json
du niveau.
Lors du chargement, l'éditeur tente d'abord d'ouvrir ce fichier via le chemin absolu. Si celui-ci est introuvable (par exemple après un déplacement), il essaiera alors de le retrouver en utilisant un chemin relatif basé sur le dossier du niveau.
💥 Gestion des erreurs
Le système est doté d'un mécanisme de détection d'erreur runtime. Si une erreur est rencontrée pendant l'exécution en mode Play :
- Le jeu est mis en pause automatiquement
- Le LevelGraph concerné est rouvert
- Un message d'erreur détaillé est affiché

🧵 Exécution mono-thread
L'exécution du LevelGraph se fait entièrement dans le fil principal du jeu, aussi appelé thread principal. Cela signifie que chaque instruction nodale est traitée les unes après les autres, sans interruption ni exécution en parallèle.
En pratique, cela veut dire que si une série de nœuds prend trop de temps à s'exécuter (par exemple une boucle très longue), le jeu peut se figer temporairement, le temps que toute la chaîne soit parcourue. Aucune autre logique ou animation ne pourra s'exécuter durant ce laps de temps.
Delay
est le seul à fonctionner de manière asynchrone. Il permet de suspendre l'exécution sans bloquer le reste du jeu. Vous pouvez l'utiliser pour insérer des pauses dans vos logiques sans impacter la fluidité globale.

Delay
ne peut pas être utilisé directement à la sortie du pin loop d'une boucle For, car les boucles sont exécutées de manière synchrone. Cette erreur est automatiquement détectée par l'éditeur et signalée.
- 🟢 Les exécutions sont séquentielles : une instruction à la fois
- 🔴 Aucune exécution parallèle n'est possible entre deux chaînes de nœuds
- ⏱️ Évitez les logiques coûteuses ou les boucles trop grandes sans contrôle
- 🧠 Pensez à isoler des comportements complexes avec des
Delay
ou des séquences bien découpées
🔁 Boucles infinies ?
En théorie, une boucle infinie est impossible : chaque node ne peut avoir qu'un seul pin exec
entrant et sortant. Toutefois, soyez attentif à vos logiques répétitives.
🔊 Système audio
Deux nœuds permettent de jouer des sons :
Play Sound
: joue un son brutEmit Sound From
: joue un son spatialisé depuis unLocation Point

./Ressources/audio_files
pour apparaître dans les sélecteurs.
pygame
ne permet que 8 canaux audio simultanés, ce qui signifie que seuls 8 sons peuvent être joués en même temps.
L'éditeur élève automatiquement cette limite à 32 canaux:
pygame.mixer.set_num_channels(32)
Cette ligne se trouve en bas du fichier
main.py
, dans le bloc if __name__ == "__main__"
.
➕ Si vous jouez beaucoup de sons (par exemple via des effets, musiques et sons spatialisés), certains peuvent être ignorés. Dans ce cas, vous pouvez augmenter la limite, par exemple :
pygame.mixer.set_num_channels(64)
✅ Si cette limite est atteinte en cours d'exécution, l'éditeur vous affichera automatiquement un message d'avertissement pour vous en informer, afin que vous puissiez ajuster cette valeur.
⚠️ Attention toutefois : augmenter le nombre de canaux augmente légèrement l'utilisation mémoire et peut entraîner des problèmes de mixage ou de saturation audio sur certaines machines.
🧩 Conditions de placement
Tous les nœuds ne sont pas disponibles immédiatement : certains dépendent du contexte du niveau ou de la présence de certaines ressources. Ces restrictions évitent des erreurs lors de l'exécution.

Exemple : certains nœuds comme Teleport
sont désactivés tant qu'aucun point de localisation n'est placé sur la scène.
🔄 Exemples fréquents
- 🔀 Teleport : nécessite au moins un
Location Point
placé dans le niveau. - 🎵 Nœuds Audio : pour utiliser
Play Sound
,Emit Sound From
, etc., il faut que des fichiers audio soient présents dans./Ressources/audio_files
. - 🔁 Emit Sound From : dépend de la présence d'un
Location Point
dans la scène.
📁 Emplacements des ressources
Pour que les nœuds audio puissent jouer des sons dans un niveau, les fichiers doivent être placés dans des dossiers spécifiques. L'éditeur les parcourt automatiquement pour alimenter le menu de sélection.
-
./Ressources/audio_files
— répertoire principal pour vos sons personnalisés. Tous les fichiers audio ajoutés ici seront disponibles dans l'éditeur. -
./editor/game_engine/Assets/sounds_effects
— utilisé exclusivement par les niveaux d'exemple fournis avec l'éditeur (par exemplenew_exemple.json
). Il permet de séparer les ressources internes de démonstration des projets utilisateurs.
./Ressources/audio_files
. Évitez de modifier les ressources système ou d'exemple.
🧩 Créer ses propres Custom Nodes
Le système nodal est extensible grâce aux Custom Nodes, que vous pouvez ajouter simplement en Python.
Chaque nœud personnalisé hérite de la classe Node
et est enregistré via le décorateur @register_node
.
./editor/blueprint_editor/blueprints/b_custom.py
pour être reconnus par l’éditeur.
Assurez-vous que vos classes soient bien décorées avec @register_node
.
🔧 Structure de base
@register_node("NomAffiché", category="MaCatégorie")
class MonNode(Node):
def __init__(self, pos, editor, properties):
super().__init__(pos, "TitreDuNode", editor, properties)
# Ajouter des pins ici
self.add_data_pin("nom", is_output=False, default=0, label="Entrée")
def execute(self, context):
# Code d'exécution ici
next_node = next((p.connection.node for p in self.outputs if p.pin_type == "exec"), None)
return next_node # ou None si c'est un getter car pas de pin exec à suivre
Chaque nœud hérite de Node
et doit redéfinir la méthode execute(self, context)
.
Les sorties (appelées pins) peuvent être ajoutées avec add_data_pin
.
📤 Exemple 1 : Getter
Expose une valeur du jeu via un pin de sortie.
@register_node("Get Score", category="Custom")
class GetScore(Node):
def __init__(self, pos, editor, properties):
super().__init__(pos, "Get Score", editor, properties)
self.inputs.clear() # Pas d'entrée
self.outputs.clear()
self.add_data_pin("score", is_output=True, default=0, label="Score")
def execute(self, context):
self.properties["score"] = self.editor.game_state.get("player_score", 0)
return None
📥 Exemple 2 : Setter
Reçoit une valeur via un pin d'entrée et l'enregistre.
@register_node("Set Health", category="Custom")
class SetHealth(Node):
def __init__(self, pos, editor, properties):
super().__init__(pos, "Set Health", editor, properties)
self.add_data_pin("hp", is_output=False, default=100, label="HP")
def execute(self, context):
pin = next(p for p in self.inputs if p.name == "hp")
value = pin.connection.node.properties[pin.connection.name] if pin.connection else self.properties["hp"]
self.editor.game_state["player_health"] = int(value)
return next((p.connection.node for p in self.outputs if p.pin_type == "exec"), None)
▶️ Exemple 3 : Action
Déclenche une animation ou effet sans manipulation de données.
@register_node("Play Animation", category="Custom")
class PlayAnim(Node):
def __init__(self, pos, editor, properties):
super().__init__(pos, "Play Animation", editor, properties)
self.add_data_pin("anim_name", is_output=False, default="idle", label="Animation")
def execute(self, context):
pin = next(p for p in self.inputs if p.name == "anim_name")
name = pin.connection.node.properties[pin.connection.name] if pin.connection else self.properties["anim_name"]
self.editor.game_engine.play_animation(name)
return next((p.connection.node for p in self.outputs if p.pin_type == "exec"), None)
🎬 Exemple 4 : Action avec UI - Play Animation
Ce nœud permet de jouer une animation prédéfinie dans l'éditeur. L'utilisateur peut sélectionner l'animation via une liste déroulante dynamique.
💡 Comportement
- Affiche dynamiquement les animations disponibles à partir du gestionnaire de l'éditeur.
- Joue l'animation sélectionnée lorsque le nœud est exécuté.
- Utilise un
DropdownButton
pour l'UI.
🧱 Code complet (./editor/blueprint_editor/blueprints/b_animations.py)
@register_node("Play Animation", category="Animation")
class PlayAnimation(Node):
def __init__(self, pos, editor, properties):
super().__init__(pos, 'Play Animation', editor, properties)
self.editor = editor
# DropdownButton avec les noms des animations
self.btn = DropdownButton(
self,
pygame.Rect(10, 30, 120, 24),
list(self.editor.LevelEditor.animations.animations.keys()),
callback=self._on_select
)
# Réutilisation d'un choix précédent
if self.properties.get('choice'):
self.btn.selected = self.properties['choice']
# Ajout à l'UI
self.ui_elements.append(self.btn)
def _on_select(self, opt: str):
self.properties['choice'] = opt
def updateDropDownField(self):
# Met à jour les options depuis le dictionnaire d'animations
self.btn.options = list(self.editor.LevelEditor.animations.animations.keys())
if not self.btn.selected in self.btn.options and self.btn.options:
self.btn.selected = self.btn.options[0]
def draw(self, surf, selected=False):
self.updateDropDownField()
super().draw(surf, selected)
for el in self.ui_elements:
el.update_position(self.x, self.y)
el.draw(surf)
def execute(self, context):
# Met à jour les entrées data si présentes
for pin in self.inputs:
if pin.pin_type == 'data' and pin.connection:
src = pin.connection
getter_node = src.node
getter_node.execute(context)
self.properties[pin.name] = getter_node.properties[src.name]
# Joue l'animation sélectionnée
anim = self.editor.LevelEditor.animations.animations[self.btn.selected]
anim.timeline.current = 0.0
anim.play(anim.timeline.loop)
# Passe au prochain nœud de la branche
out = next((p for p in self.outputs if p.pin_type == 'exec'), None)
return out.connection.node if out and out.connection else None
🖼️ Interface dans l'éditeur
Le nœud s'affichera avec un bouton déroulant contenant les noms des animations disponibles.
- Les options sont automatiquement extraites du gestionnaire
self.editor.LevelEditor.animations
. - Le champ
choice
dans lesproperties
stocke le nom choisi.
Envie d'en savoir plus ?
Consultez les pages suivantes pour explorer les fonctionnalités clés de l'éditeur :
- 🔧 Interface — Comprendre les outils de dessin, la gestion des layers et le fonctionnement du viewport.
- 🎞️ Animations — Créez, éditez et déclenchez des animations dans vos niveaux à l'aide de la timeline visuelle.
