Godot Engine Best Practices

 0
 0
Tps de lecture estimé:10 minutes 13 secondes

Voici une résumé des différentes pratiques conseillées dans le développement et l'utilisation de Godot engine.
Ce fichier est un canevas qui s'étoffera au fur et à mesure de mes expérimentations sur ce langage.

Références/Sources:

Les scènes et les scripts Godot sont des classes

  • La principale différence est que les scènes sont du code déclaratif, alors que les scripts peuvent contenir du code impératif.
  • Les classes internes de Godot ont des méthodes qui enregistrent les données d’une classe via ClassDB.
  • Même les scripts qui n’héritent pas d’un type intégré, (c’est-à-dire sans le mot-clé extend), héritent implicitement de la classe de référence du moteur.
  • La scène est toujours une extension du script attaché à son nœud racine

Performances

  • Utiliser de préférences des scenes dans l'éditeur plutôt que des objets (nodes) créés par script
    • les 'PackedScene' (type de base dont les scènes héritent) sont des ressources qui utilisent des données sérialisées (dans classDB) pour créer des objets.
        - Un code de script est beaucoup plus lent que le code C++ côté moteur.

Organisation de la scène

Établir des relations efficacement

  • Si c’est possible, il faut concevoir des scènes sans dépendance.
  • Si une scène doit interagir avec un contexte externe, utiliser des "injections de dépendance". Une classe expose des données, puis le contexte parent doit les initialiser.
  • 5 façons possibles pour masquer la source des accès au nœud enfant:
    • Connecter à un signal. Extrêmement sûr, mais ne doit être utilisé que pour « répondre » au comportement, pas pour le démarrer.
    • Appeler une méthode. Utilisé pour démarrer le comportement.
    • Initialiser une propriété FuncRef. Plus sûr qu’une méthode car la propriété de la méthode n’est pas nécessaire. Utilisé pour démarrer le comportement.
    • Initialiser un nœud ou une référence d’objet.
    • Initialiser un NodePath (le contexte parent indique à l'enfant le "chemin du nœud" à utiliser pour une action donnée)
  • ces principes s'appliquent aussi au relations entre objets

Choix d’une structure d’arborescence des nœuds

  • créer un point d'entrée (main.gd)

    Node “Main” (main.gd)
    Node2D/Spatial “World” (game_world.gd)
    Control “GUI” (gui.gd)
  • lors d'un changement de scene, remplacer le nœud "World"
  • les GUI devraient également être des singleton, des éléments transitoires du "World", ou être ajoutés manuellement en tant qu'enfant direct de la racine. Sinon, les GUI se supprimeraient également lors des transitions de scènes.
  • Dans un jeu plus complexe avec des ressources plus importantes, il peut être préférable de laisser le joueur à un autre endroit du SceneTree.
  • La clé de l'organisation est de considérer le SceneTree en termes relationnels plutôt qu'en termes spatiaux.

Quand utiliser les scènes ou les scripts

  • Type anonyme: Il est possible de définir complètement le contenu d’une scène en utilisant uniquement un script.
  • Type nommé (enregistrés)
      - Type personnalisé : accessibles par l'Editeur uniquement.
      - Classe de script : accessibles par l'Editeur et en runtime.
  • la meilleure approche consiste à :

    • pour créer un outil de base qui sera réutilisé dans plusieurs projets différents -> script
    • pour créer un concept particulier -> scène. Les scènes sont plus faciles à suivre / éditer et offrent plus de sécurité que les scripts.
        - pour donner un nom à une scène -> classe de script et lui donner une scène comme constante. Le script devient en fait un espace de noms.
    # dans game.gd
    extends Reference
    class_name Game # extends Reference, so it won't show up in the node creation dialog
    const MyScene = preload("my_scene.tscn")
    #
    # dans main.gd
    extends Node
    func _ready():
      add_child(Game.MyScene.instance())

Autoload versus nœud interne

  • Certains moteurs encouragent l'utilisation de classes "manager" (ex: SoundManager) qui gérent des fonctionnalités globalement et qui sont chargées automatiquement (autoload)
  • Dans Godot il est préférable de déplacer le contenu "managé dans des nœuds individuels liés à chaque scène.
  • Les justifications typiques du chargement automatique sont les suivantes: «J'ai des X communs qui impliquent de nombreux nœuds dans de nombreuses scènes et je veux que chaque scène comporte un X.»
    • Si X est une fonction -> créer un nouveau type de nœud qui fournit cette fonctionnalité à une scène.
    • Si X est une donnée ->
    • soit créer un nouveau type de ressource pour partager les données
    • soit stocker les données dans un objet auquel chaque nœud a accès (les nœuds d'une scène peuvent utiliser get_owner() pour obtenir la racine de la scène par exemple).
  • Quand faut-il utiliser un autoload?
      - Données statiques: données à associer à une classe (il n’existe donc qu’une copie des données),
      - Commodité: les nœuds autoloadés ont une variable globale à leur nom générée dans GDScript. Cela peut être très pratique pour définir des objets qui doivent toujours exister, mais qui nécessitent des informations d'instance d'objet.

Quand et comment éviter d'utiliser des nœuds pour tout

  • Les nœuds sont peu coûteux à produire, mais même ils ont leurs limites et un grand nombre peut diminuer les performances
  • Godot fournit des objets plus légers pour créer des API utilisées par les nœuds.
      - Objet: l’objet le plus léger: pour créer ses propres structures de données personnalisées, et même des structures de nœuds.
      - Référence: un peu plus complexe que Object. Ils suivent les références à eux-mêmes, ne supprimant la mémoire chargée que lorsqu'il n'existe aucune autre référence à eux-mêmes.
      - Ressource: un peu plus complexe que Référence. Ils ont la capacité de sérialiser/désérialiser leurs propriétés vers/à partir de fichiers de ressources. Bien qu'ils soient presque aussi légers que des Object ou Reference, ils peuvent afficher et exporter leur propriétés dans l'inspecteur.

Interfaces Godot

Acquisition de références d'objet

  • le moyen le plus simple de référencer un "object" est d'obtenir une référence à un objet existant à partir d'une autre instance acquise.

    var obj = node.object # Property access.
    var obj = node.get_object() # Method access.

-Le même principe s'applique aux "Reference/Ressources".

  var preres = preload(path) # Load resource during scene load
  var res = load(path) # Load resource when program reaches statement

  # Note that users load scenes and scripts, by convention, with PascalCase
  # names (like typenames), often into constants.
  const MyScene : = preload("my_scene.tscn") as PackedScene # Static load
  const MyScript : = preload("my_script.gd") as Script
  • le chargement d'une ressource récupère l`instance de ressource en cache. Pour obtenir un nouvel objet, il faut dupliquer une référence existante ou en instancier une avec new().
  • plusieurs méthodes possibles:

    extends Node
    
    # Slow.
    func dynamic_lookup_with_dynamic_nodepath():
    print(get_node("Child"))
    
    # Faster. GDScript only.
    func dynamic_lookup_with_cached_nodepath():
    print($Child)
    
    # Fastest. Doesn't break if node moves later.
    # Note that `onready` keyword is GDScript only.
    # Other languages must do...
    #     var child
    #     func _ready():
    #         child = get_node("Child")
    onready var child = $Child
    func lookup_and_cache_for_future_access():
    print(child)
    
    # Delegate reference assignment to an external source
    # Con: need to perform a validation check
    # Pro: node makes no requirements of its external structure.
    #      'prop' can come from anywhere.
    var prop
    func call_me_after_prop_is_initialized_by_parent():
    # Validate prop in one of three ways.
    
    # Fail with no notification.
    if not prop:
        return
    
    # Fail with an error message.
    if not prop:
        printerr("'prop' wasn't initialized")
        return
    
    # Fail and terminate.
    # Compiled scripts in final binary do not include assert statements
    assert prop.
    
    # Use an autoload.
    # Dangerous for typical nodes, but useful for true singleton nodes
    # that manage their own data and don't interfere with other objects.
    func reference_a_global_autoloaded_variable():
    print(globals)
    print(globals.prop)
    print(globals.my_getter())

Notifications Godot

  • Chaque objet dans Godot implémente une méthode de notification afin de permettre de répondre à des callbacks du moteur. Par exemple, si le moteur appelle la méthode "draw" de CanvasItem, il appellera _notification(NOTIFICATION_DRAW).
  • ces notifications peuvent être outrepassées (overriden) par script.
    • _ready(): NOTIFICATION_READY
    • _enter_tree(): NOTIFICATION_ENTER_TREE
    • _exit_tree(): NOTIFICATION_EXIT_TREE
    • _process(delta): NOTIFICATION_PROCESS
    • _physics_process(delta): NOTIFICATION_PHYSICS_PROCESS
    • _input(): NOTIFICATION_INPUT
    • _unhandled_input(): NOTIFICATION_UNHANDLED_INPUT
    • _draw(): NOTIFICATION_DRAW
  • notifications pour d'autres types que les nœuds:
    • Object::NOTIFICATION_POSTINITIALIZE: lors de l'initialisation de l'objet. Non accessible aux scripts.
    • Object::NOTIFICATION_PREDELETE: avant que le moteur supprime un objet, ie. un ‘destructeur'.
    • MainLoop::NOTIFICATION_WM_MOUSE_ENTER: lorsque la souris entre dans la fenêtre qui affiche le contenu du jeu.
  • la plupart des callbacks existant dans les nœuds n’ont pas de méthodes dédiées, mais sont utiles.
    • Node::NOTIFICATION_PARENTED: quand on ajoute un nœud enfant à un autre nœud.
    • Node::NOTIFICATION_UNPARENTED: quand on supprime un nœud enfant d'un autre nœud.
    • Popup::NOTIFICATION_POST_POPUP: une fois qu'un nœud Popup a terminé une méthode popup*. Notez la différence avec son signal about_to_show qui se déclenche avant son apparition.

_process VS _physics_process VS *_input

  • Utilisez _process lorsque vous avez besoin d'un deltatime dépendant du framerate.
  • Utilisez _physics_process lorsque vous avez besoin d'un deltatime indépendant du framerate, par exemple pour Les opérations de transformation kinematic et récurrentes d'objets.
  • Pour de meilleures performances, éviter de procéder à des contrôles d'entrée dans les méthodes _process ou_physics_process. Utiliser plutôt les callbacks *_input

    # Called every frame, even when the engine detects no input.
    func _process(delta):
    if Input.is_action_just_pressed("ui_select"):
      print(delta)
    # Called during every input event.
    func _unhandled_input(event):
    match event.get_class():
      "InputEventKey":
        if Input.is_action_just_pressed("ui_accept"):
          print(get_process_delta_time())

_init VS initialization VS export

  • Si le script initialise son propre sous-arbre de nœud, sans scène, ce code doit être exécuté ici.
  • Les autres initialisations de propriétés ou celles indépendantes du SceneTree doivent également être exécutées ici.

    # "one" is an "initialized value". These DO NOT trigger the setter.
    # If someone set the value as "two" from the Inspector, this would be an
    # "exported value". These DO trigger the setter.
    export(String) var test = "one" setget set_test
    func _init():
    # "three" is an "init assignment value".
    # These DO NOT trigger the setter, but...
    test = "three"
    # These DO trigger the setter. Note the `self` prefix.
    self.test = "three"
    
    func set_test(value):
    test = value
    print("Setting: ", test)

_ready VS _enter_tree VS NOTIFICATION_PARENTED

  • cf. DOC (trop d'info)

Type de données préférentiels

Array VS Dictionary VS Object

  • Godot implémente un Array comme un Vector<Variant>
      - Itérer: le plus rapide. Idéal pour les boucles.
      - Insérer, effacer, déplacer: dépend de la position. Généralement lent.
      - Get, Set: Le plus rapide par position.
      - Trouver: le plus lent.
  • Godot implémente un Dictionary comme un OrderedHashMap <Variant, Variant> et stocke un tableau géant (initialisé à 1000 enregistrements) de paires clé-valeur.
      - Itérer: rapide.
      - Insérer, effacer, déplacer: le plus rapide.
      - Get, Set: Le plus rapide.
      - Trouver: le plus lent.
  • Godot implémente un Object comme un conteneur de conteneur de données stupide et dynamique. Les tâches liées aux objets sont complexes: à chaque fois qu'une opération est effectuée, plusieurs boucles d'itération et de recherches HashMap sont nécessaires.

Enumerations: int VS string

  • cf. DOC (trop d'info)

AnimatedTexture VS AnimatedSprite VS AnimationPlayer VS AnimationTree

  • cf. DOC (trop d'info)

Logiques préférentielles

  • cf. DOC (trop d'info)

Article suivant