Illustration

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

Ouverture du level graph
  • 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.

Astuce : Vous pouvez consulter la documentation d'une node directement depuis la bibliothèque de nodes. Rendez-vous dans la documentation des nodes pour plus de détails.
Types de nœuds

🧠 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 et B) avec un opérateur (==, !=, <, etc.) et exécute selon le résultat (sorties true ou false).
  • For — exécute une boucle n fois. Utilisez la sortie loop pour les instructions répétées n fois, et next 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 sorties A et B à 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...)
Nœuds logiques
Remarque : Les variables utilisées avec 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 collision
  • OnExit — déclenché à la sortie du joueur de la collision
  • OnOverlap — déclenché tant que le joueur reste dans la collision
  • OnStart — au lancement du mode Play
  • OnTick — déclenché à chaque frame (⚠️ à utiliser avec modération)
  • OnEnd — à la fermeture du mode Play
Types de nœuds d'événements
Info : Lors de la sauvegarde d'un niveau, le graphe nodal est automatiquement enregistré dans un fichier .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é
Error
Attention : Les erreurs non typées (ex : un setter avec une mauvaise valeur de type) peuvent ne pas être interceptées et faire planter l'éditeur.

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

Conseil : Le nœud 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 exception
Attention : Le nœud 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 brut
  • Emit Sound From : joue un son spatialisé depuis un Location Point
Système audio
Info : Les sons doivent être placés dans ./Ressources/audio_files pour apparaître dans les sélecteurs.
Limite : Par défaut, 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 de nodes désactivés

Exemple : certains nœuds comme Teleport sont désactivés tant qu'aucun point de localisation n'est placé sur la scène.

Remarque : Si un nœud ne peut pas être posé, il sera désactivé. L'éditeur empêche alors automatiquement son ajout pour garantir la cohérence du graphe.

🔄 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 exemple new_exemple.json). Il permet de séparer les ressources internes de démonstration des projets utilisateurs.
Conseil : Pour garder vos projets propres et autonomes, placez vos fichiers sonores dans ./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.

Important : Tous les nœuds personnalisés doivent être définis dans le fichier ./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.

Type : Action - ce nœud déclenche un effet mais ne transfère pas de données.

💡 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 les properties 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.
  • Éditeur Nodal en action