Godot: Ajoutez un système de trait GDScript.

Créé le 18 oct. 2018  ·  93Commentaires  ·  Source: godotengine/godot

(Éditer:
Pour minimiser d'autres problèmes de problème XY :
Le problème abordé ici est que le système Node-Scene / les langages de script de Godot ne prennent pas encore en charge la création d'implémentations groupées réutilisables qui sont 1) spécifiques aux fonctionnalités du nœud racine et 2) qui peuvent être permutées et/ou combinées. Des scripts avec des méthodes statiques ou des sous-nœuds avec des scripts peuvent être utilisés pour ce dernier bit, et dans de nombreux cas, cela fonctionne. Cependant, Godot préfère généralement que vous gardiez la logique du comportement global de votre scène stockée dans le nœud racine alors qu'il utilise des données calculées par les nœuds enfants ou leur délègue des sous-tâches significativement déviées, par exemple un KinematicBody2D ne gère pas l'animation donc il délègue cela à un AnimationPlayer.

Avoir un nœud racine plus fin qui utilise des nœuds enfants "composants" pour piloter son comportement est un système faible en comparaison. Avoir un nœud racine largement vide qui délègue tout son comportement aux nœuds enfants va à l'encontre de ce paradigme. Les nœuds enfants deviennent des extensions comportementales du nœud racine plutôt que des objets autonomes qui accomplissent une tâche à part entière. C'est très maladroit, et la conception pourrait être simplifiée/améliorée en nous permettant de consolider toute la logique dans le nœud racine, mais aussi de diviser la logique en différents morceaux composables.

Je suppose que le sujet de ce numéro concerne davantage la manière de traiter le problème ci-dessus que spécifiquement GDScript, mais je pense que GDScript Traits serait l'approche la plus simple et la plus directe pour résoudre le problème.
)

Pour les non-informés, les traits sont essentiellement un moyen de fusionner deux classes en une seule (à peu près un mécanisme de copier/coller), seulement, plutôt que de copier/coller littéralement le texte des fichiers, tout ce que vous faites est d'utiliser une déclaration de mot-clé pour lier les deux fichiers. (Edit: l'astuce est que même si un script ne peut hériter que d'une classe, il peut inclure plusieurs traits)

J'imagine quelque chose où n'importe quel fichier GDScript peut être utilisé comme trait pour un autre fichier GDScript, tant que le type traité étend une classe héritée par le script fusionné, c'est-à-dire qu'un GDScript à extension Sprite ne peut pas utiliser un GDScript de ressource comme trait, mais il peut utiliser un Node2D GDScript. J'imaginerais une syntaxe similaire à ceci:

# move_right_trait.gd
extends Node2D
class_name MoveRightTrait # not necessary, but just for clarity
func move_right():
    position.x += 1

# my_sprite.gd
extends Sprite
is MoveRightTrait # maybe add a 'use' or 'trait' keyword for this instead?
is "res://move_right_trait.gd" # alternative if class_name isn't used
func _physics_process():
    move_right() # MoveRightTrait's content has been merged into this script
    if MoveRightTrait in self:
        print("I have a MoveRightTrait")

Je vois deux manières de procéder :

  1. Pré-parsez le script via RegEx pour "^trait \processus de rechargement). Nous devrons ne pas prendre en charge l'imbrication des traits ou réexaminer en permanence le code source généré après chaque itération pour voir si d'autres insertions de traits ont été faites.
  2. Analysez le script normalement, mais apprenez à l'analyseur à reconnaître le mot-clé, chargez le script référencé, analysez CE script, puis ajoutez le contenu de son ClassNode au ClassNode généré du script actuel (en prenant effectivement les résultats analysés d'un script et en l'ajoutant aux résultats analysés de l'autre script). Cela prendrait automatiquement en charge l'imbrication des types traités.

D'un autre côté, les gens peuvent vouloir que le trait GDScript ait un nom mais ne veulent PAS que le nom de classe de ce GDScript apparaisse dans le CreateDialog (car il n'est pas destiné à être créé tout seul). Dans ce cas, ce n'est peut-être PAS une bonne idée de faire en sorte qu'un script le supporte ; seulement ceux qui sont spécialement marqués (peut-être en écrivant 'trait' en haut du fichier ?). Quoi qu'il en soit, des choses à penser.

Les pensées?

Edit: Après réflexion, je pense que l'option 2 serait bien meilleure puisque 1) nous saurions DE QUEL script provient un segment de script (pour un meilleur rapport d'erreur) et 2) nous serions en mesure d'identifier les erreurs au fur et à mesure qu'elles se produisent depuis le les scripts inclus doivent être analysés dans l'ordre, plutôt que de tout analyser à la fin. Cela réduirait le temps de traitement qu'il ajoute au processus d'analyse.

archived discussion feature proposal gdscript

Commentaire le plus utile

@aaronfranke Traits , fondamentalement la même chose que Mixins , ont un cas d'utilisation complètement différent des interfaces précisément parce qu'ils incluent des implémentations des méthodes. Si une interface donnait une implémentation par défaut, alors ce ne serait plus vraiment une interface.

Les traits/Mixins sont présents dans PHP, Ruby, D, Rust, Haxe, Scala et de nombreux autres langages (comme détaillé dans les Wikis liés), ils devraient donc déjà être largement familiarisés avec les personnes qui ont un large répertoire de familiarité avec les langages de programmation.

Si nous devions implémenter des interfaces (ce à quoi je ne suis pas opposé non plus, en particulier avec le typage statique facultatif à venir), ce ne serait en fait qu'un moyen de spécifier les signatures de fonction, puis d'exiger que les scripts GDScript pertinents implémentent ces signatures de fonction, avec des traits inclus (si ceux-ci existaient à ce moment-là).

Tous les 93 commentaires

Quel est l'avantage au lieu de : extends "res://move_right_trait.gd"

@MrJustreborn Parce que vous pouvez avoir plusieurs traits dans une classe, mais vous ne pouvez hériter que d'un seul script.

Si j'ai bien compris, c'est en gros ce que C# appelle des " interfaces ", mais avec des méthodes non abstraites ? Il serait peut-être préférable d'appeler les interfaces de fonctionnalités au lieu de traits pour être familiers aux programmeurs.

@aaronfranke Traits , fondamentalement la même chose que Mixins , ont un cas d'utilisation complètement différent des interfaces précisément parce qu'ils incluent des implémentations des méthodes. Si une interface donnait une implémentation par défaut, alors ce ne serait plus vraiment une interface.

Les traits/Mixins sont présents dans PHP, Ruby, D, Rust, Haxe, Scala et de nombreux autres langages (comme détaillé dans les Wikis liés), ils devraient donc déjà être largement familiarisés avec les personnes qui ont un large répertoire de familiarité avec les langages de programmation.

Si nous devions implémenter des interfaces (ce à quoi je ne suis pas opposé non plus, en particulier avec le typage statique facultatif à venir), ce ne serait en fait qu'un moyen de spécifier les signatures de fonction, puis d'exiger que les scripts GDScript pertinents implémentent ces signatures de fonction, avec des traits inclus (si ceux-ci existaient à ce moment-là).

Peut-être un mot-clé comme includes ?

extends Node2D
includes TraitClass

Bien que d'autres noms comme trait, mixin, has, etc. conviennent sûrement aussi.

J'aime aussi l'idée d'avoir une option pour exclure le type class_name du menu d'ajout. Il peut être très encombré de petits types qui ne fonctionnent pas seuls en tant que nœuds.

Il peut même s'agir d'un sujet à part entière.

(Suppression accidentelle de mon commentaire, woops! Aussi, obligatoire "pourquoi n'autorisez-vous pas simplement plusieurs scripts, l'unité le fait" )

Comment cela fonctionnera-t-il dans VisualScript, le cas échéant ?

En outre, pourrait-il être avantageux d'inclure une interface d'inspecteur pour les traits, si les traits étaient implémentés ? J'imagine que certains cas d'utilisation de traits peuvent inclure des cas d'utilisation où il n'y a que des traits et pas de script (au moins, pas de script à part celui qui inclut les fichiers de traits). Cependant, en y réfléchissant davantage, je me demande si l'effort déployé pour créer une telle interface en vaut la peine, par rapport à la simple création d'un script incluant les fichiers de traits.

@LikeLakers2

Comment cela fonctionnera-t-il dans VisualScript, le cas échéant ?

Si c'est fait comme je l'ai suggéré, cela n'arriverait pas du tout pour VisualScript. GDScript uniquement. Tout système de traits implémenté pour VisualScript serait conçu complètement différemment car VisualScript n'est pas un langage analysé. N'empêche pas du tout la possibilité (devrait juste être implémenté différemment). De plus, peut-être devrions-nous d'abord envisager de prendre en charge l'héritage VisualScript ? MDR

En outre, pourrait-il être avantageux d'inclure une interface d'inspecteur pour les traits, si les traits étaient implémentés ?

Il n'y aurait pas grand intérêt. Les traits donnent simplement des détails sur le GDScript, lui transmettant les propriétés, les constantes, les signaux et les méthodes définis par le trait.

J'imagine que certains cas d'utilisation de traits peuvent inclure des cas d'utilisation où il n'y a que des traits et aucun script (du moins, aucun script à part celui qui inclut les fichiers de traits).

Les traits, tels qu'ils sont représentés dans d'autres langues, ne sont jamais utilisables isolément, mais doivent être inclus dans une autre écriture pour être utilisables.

Je me demande si l'effort déployé pour créer une telle interface en vaut la peine

Créer une interface Inspector d'une manière ou d'une autre n'aurait pas vraiment de sens pour GDScript seul. L'ajout ou la suppression d'un trait impliquerait de modifier directement le code source de la propriété source_code de la ressource Script, c'est-à-dire qu'il ne s'agit pas d'une propriété sur le Script lui-même. Donc, soit...

  1. l'éditeur devrait apprendre à gérer spécifiquement l'édition correcte du code source pour les fichiers GDScript pour ce faire (sujet aux erreurs), ou ...
  2. tous les scripts devraient prendre en charge les traits afin que GDScriptLanguage puisse fournir son propre processus interne pour ajouter et supprimer des traits (mais toutes les langues ne prennent pas en charge les traits, de sorte que la propriété ne serait pas significative dans tous les cas).

Quel est le besoin d'une telle fonctionnalité? Y a-t-il quelque chose que cela permet que vous ne puissiez pas faire maintenant ? Ou cela rend-il certaines tâches beaucoup plus rapides à traiter ?

Je préfère garder GDscript un langage simple plutôt que d'ajouter des fonctionnalités complexes presque jamais utilisées.

Il résout le problème Child-Nodes-As-Script-Dependencies avec lequel ce gars avait un problème , mais ne vient pas avec le même type de bagage que MultiScript car il est limité à une seule langue. Le module GDScript peut isoler la logique sur la façon dont les traits sont liés les uns aux autres et au script principal, tandis que résoudre les différences entre les différentes langues serait beaucoup plus compliqué.

En l'absence d'importations multiples/d'héritage multiple, les dépendances de nœuds enfants en tant que script sont le seul moyen d'éviter de répéter BEAUCOUP de code, ce qui résoudrait certainement le problème d'une manière agréable.

@groud @Zireael07 Je veux dire, l'approche inter-langage la plus radicale serait de 1) repenser complètement Object pour utiliser un ScriptStack pour fusionner des scripts empilés en une seule représentation de script, 2) réintroduire MultiScript et créer un support d'éditeur qui convertit automatiquement l'ajout de scripts dans des multiscripts (ou simplement de rendre tous les scripts multiscripts pour des raisons de simplicité, auquel cas l'implémentation de MultiScript serait essentiellement notre ScriptStack), ou 3) implémenter une sorte de système de traits inter-langues pour le type d'objet qui peut fusionner dans Scripts d'extension de référence en tant que traits, incorporant leur contenu comme un script typique. Toutes ces options sont cependant beaucoup plus envahissantes pour le moteur. Cela rend tout plus simple.

Je ne pense pas que le trait soit nécessaire. ce dont nous avons le plus besoin, c'est d'un cycle de libération rapide du moteur. Je veux dire que nous devons rendre le moteur plus flexible pour ajouter de nouvelles fonctionnalités aussi simplement qu'ajouter de nouveaux fichiers dll ou plus et que le moteur s'intègre automatiquement, comme le style des plugins dans la plupart des IDE. par exemple, j'ai vraiment désespérément besoin que Websocket fonctionne, je n'ai pas besoin d'attendre la version 3.1 pour la publier. 3.1 trop cassé en ce moment avec tant de bugs. ce sera génial si nous avons cette fonctionnalité. la nouvelle classe peut s'injecter automatiquement dans GDScript à partir d'un fichier .dll ou .so aléatoire dans un chemin. Je ne sais pas combien d'efforts cela doit faire en C++ mais j'espère que ce n'est pas trop difficile 😁

@ fian46 Eh bien, si quelqu'un avait implémenté des websockets en tant que plugin GDNative téléchargeable, alors oui, ce que vous avez décrit serait le flux de travail. Au lieu de cela, ils ont choisi d'en faire une fonctionnalité intégrée disponible dans le moteur vanilla. Rien n'empêche les gens de créer des fonctionnalités de cette façon, donc votre point n'a vraiment aucun rapport avec le sujet de ce numéro.

oups je ne sais pas que GDNative existe 😂😂😂. Le trait est génial, mais est-il plus facile de créer une fausse classe de traits et de l'instancier, puis d'appeler la fonction comme un script de base ?

Si un script Godot est une classe sans nom, pourquoi ne pas instancier "move_right_trait.gd" dans "my_sprite.gd" ?
Désolé pour mon ignorance si je ne comprends pas le problème.

Je comprends l'utilisation de traits dans des langages plus fortement typés tels que Rust ou (interfaces en) C++, mais dans un langage typé canqué, n'est-ce pas un peu inutile? La simple implémentation des mêmes fonctions devrait vous permettre d'obtenir une interface uniforme entre vos types. Je suppose que je ne sais pas quel est le problème exact avec la façon dont GDScript gère les interfaces ou comment un système de traits serait même vraiment utile.

Ne pourriez-vous pas également utiliser preload("Some-other-behavior.gd") et stocker les résultats dans une variable pour obtenir essentiellement le même effet ?

@fian46 @DriNeo Eh bien, oui et non. Le chargement de scripts et l'utilisation de classes de script s'en occupent déjà, mais le problème va au-delà.

@TheYokai

L'implémentation des mêmes fonctions devrait vous permettre d'obtenir une interface uniforme entre vos types

Le problème n'est pas d'obtenir une interface uniforme, ce que vous avez raison, le duck-typing résout très bien, mais plutôt d'organiser (combiner/échanger) efficacement des groupes d'implémentations connexes.


Dans 3.1, avec les classes de script, vous pouvez définir des fonctions statiques sur un script de référence (ou n'importe quel type en fait), puis utiliser ce script comme espace de noms pour accéder globalement à ces fonctions dans GDScript.

extends Reference
class_name Game
static func print_text(p_text):
    print(p_text)
# can even add inner classes for sub-namespaces

extends Node
func _ready():
    Game.print_text("Hello World!")

Cependant, en ce qui concerne le contenu non statique, en particulier les éléments qui utilisent des fonctions spécifiques à un nœud, il est difficile de diviser sa logique.

Par exemple, que se passe-t-il si j'ai un KinematicBody2D et que je veux avoir un comportement "Jump" et un comportement "Run" ? Chacun de ces comportements aurait besoin d'accéder à la gestion des entrées et aux fonctionnalités move_and_slide de KinematicBody2D. Idéalement, je serais capable d'échanger indépendamment l'implémentation de chaque comportement et de conserver tout le code de chaque comportement dans des scripts séparés.

Actuellement, tous les flux de travail que je connais pour cela ne sont tout simplement pas optimaux.

  1. Si vous conservez toutes les implémentations dans le même script et échangez simplement les fonctions utilisées ...

    1. la modification des "comportements" peut impliquer l'échange de plusieurs fonctions en tant qu'ensemble, de sorte que vous ne pouvez pas regrouper efficacement les modifications d'implémentation.

    2. Toutes les fonctions pour chaque comportement (X * Y) se trouvent dans votre script unique, il peut donc être gonflé très rapidement.

  2. Vous pouvez simplement remplacer l'intégralité du script, mais cela signifie que vous devez créer un nouveau script pour chaque combinaison de comportements, ainsi que toute logique utilisant ces comportements.
  3. Si vous utilisez des nœuds enfants comme dépendances de script, cela signifie que vous auriez ces nœuds Node2D "composants" étranges qui récupèrent leur parent et appellent la méthode move_and_slide POUR cela, ce qui est un peu contre nature, relativement parlant.

    • Vous devez faire l'hypothèse que votre parent va implémenter cette méthode ou faire de la logique pour vérifier qu'il a la méthode. Et si vous effectuez une vérification, vous pouvez soit échouer en silence et potentiellement avoir un bogue silencieux dans votre jeu, soit le transformer inutilement en un script d'outil afin de pouvoir définir un avertissement de configuration sur le nœud pour vous le signaler visuellement dans l'éditeur. qu'il y a un problème.

    • Vous n'obtenez pas non plus une complétion de code appropriée pour les opérations prévues des nœuds, car ils dérivent de Node2D et le but est de piloter le comportement d'un parent KinematicBody2D.

Maintenant, j'admettrai que l'option 3 est le flux de travail le plus efficace actuellement, et que ses problèmes peuvent être largement résolus en utilisant le typage statique pratique pour GDScript dans 3.1. Cependant, il y a un problème plus fondamental en jeu.

Le système nœud-scène de Godot a généralement la forme d'utilisateurs créant des nœuds ou des scènes qui effectuent un travail particulier, dans leur propre système fermé. Vous pouvez instancier ces nœuds/scènes dans une autre scène et leur faire calculer des données qui sont ensuite utilisées par la scène parente (comme c'est le cas avec la relation Area2D et CollisionShape2D).

Cependant, l'utilisation du moteur vanille et la recommandation des meilleures pratiques générales est de garder le comportement de votre scène verrouillé sur le nœud racine et/ou son script. Vous n'avez presque jamais de nœuds "composants de comportement" qui disent réellement à la racine quoi faire (et quand ils sont là, c'est très maladroit). Les nœuds AnimationPlayer/Tween sont les seules exceptions discutables auxquelles je peux penser, mais même leurs opérations sont dirigées par la racine (il leur délègue effectivement le contrôle temporairement). (Edit: Même dans ce cas, l'animation et l'interpolation ne sont pas le travail du KinematicBody2D, il est donc logique que ces tâches soient déléguées. Le mouvement, cependant, comme courir et sauter est sa responsabilité) Il est plus simple et plus naturel de permettre une implémentation de trait pour organiser le code car il maintient les relations entre les nœuds strictement data-up/behavior-down et garde le code plus isolé dans ses propres fichiers de script.

Eh bien, vous marquer comme "implémentant une interface/un trait" devrait également remplir un test * is * , ce qui est pratique pour tester la fonctionnalité de quelque chose.

@ OvermindDL1 Je veux dire, j'ai donné un exemple de test comme celui-là, mais j'ai utilisé in la place car je voulais faire la distinction entre l'héritage et l'utilisation des traits.

Je suppose que je suis entré dans un problème XY ici, mon mauvais. Je venais de sortir de 2 autres numéros (#23052, #15996) qui abordaient ce sujet d'une manière ou d'une autre et je me suis dit que je soumettrais une proposition, mais je n'ai pas vraiment donné tout le contexte.

@groud cette solution résoudra l'un des problèmes soulevés contre #19486.

@willnationsdev super idée, j'ai hâte d'y être !

D'après ma compréhension limitée, ce que ce système Trait veut accomplir est d'activer quelque chose de similaire au flux de travail montré dans cette vidéo : https://www.youtube.com/watch?v=raQ3iHhE_Kk
(Tenez compte, je parle du _workflow_ affiché, pas de la fonctionnalité utilisée)

Dans la vidéo, il est comparé à d'autres types de flux de travail, avec leurs avantages et leurs inconvénients.

Au moins à ma connaissance, ce type de flux de travail est actuellement impossible dans GDScript, à cause du fonctionnement de l'héritage.

@AfterRebelion Les premières minutes de cette vidéo où il isole la modularité, l'éditabilité et la débogabilité de la base de code (et les détails connexes de ces attributs) sont en effet la poursuite de cette fonctionnalité.

Au moins à ma connaissance, ce type de flux de travail est actuellement impossible dans GDScript, en raison du fonctionnement de l'héritage.

Ce n'est pas tout à fait vrai, car Godot le fait très bien en ce qui concerne les hiérarchies de nœuds et la conception de scènes. Les scènes sont modulaires par nature, les propriétés peuvent être exportées (et même animées) directement depuis l'éditeur sans jamais traiter de code, et tout peut être testé/débogué isolément car les scènes peuvent être exécutées indépendamment.

La difficulté survient lorsque la logique qui serait généralement externalisée vers un nœud enfant doit être exécutée par le nœud racine car la logique repose sur les fonctionnalités héritées du nœud racine. Dans ces cas, la seule façon d'utiliser la composition est que les enfants commencent à dire au parent quoi faire plutôt que de les laisser s'occuper de leurs propres affaires pendant que le parent les utilise.

Ce n'est pas un problème dans Unity car GameObject n'a pas de véritable héritage dont les utilisateurs peuvent profiter. Dans Unreal, cela pourrait être un peu (?) Un problème car ils ont des hiérarchies internes similaires basées sur des nœuds / composants pour les acteurs.

D'accord, jouons un peu à Devil's Advocate ici ( @MysteryGM , vous pourriez en profiter). J'ai passé du temps à réfléchir à la façon dont je pourrais écrire un tel système dans Unreal et cela me donne une nouvelle perspective sur celui-ci. Désolé pour les gens qui pensaient que ce serait une bonne idée / qui en étaient ravis :

L'introduction d'un système de traits ajoute une couche de complexité à GDScript en tant que langage qui peut le rendre plus difficile à maintenir.

En plus de cela, les traits en tant que fonctionnalité rendent plus difficile l'identification de l'origine des variables, des constantes, des signaux, des méthodes et même des sous- classes . Si le script de votre nœud a soudainement 12 traits différents, vous ne saurez pas nécessairement d'où tout vient. Si vous voyez une référence à quelque chose, vous devrez parcourir 12 fichiers différents pour même savoir où se trouve une chose dans la base de code.

Cela réduit en fait la capacité de débogage de GDScript en tant que langage, car tout problème donné peut vous obliger à séparer en moyenne 2 ou 3 emplacements différents dans la base de code. Si l'un de ces emplacements est difficile à trouver car il dit qu'il se trouve dans un script mais qu'il se trouve en fait ailleurs - et si la lisibilité du code n'indique pas clairement quelle chose est responsable des données/logique - alors ces 2 ou 3 les étapes sont multipliées en un ensemble d'étapes arbitrairement grand et très stressant.

La taille et la portée croissantes d'un projet amplifient encore plus ces effets négatifs et font de l'utilisation des traits une qualité assez insoutenable.


Mais que faire pour résoudre le problème ? Nous ne voulons pas que les nœuds enfants de la "logique des composants" indiquent aux racines de la scène ce qu'il faut faire, mais nous ne pouvons pas non plus compter sur l'héritage ou sur la modification de scripts entiers pour résoudre notre problème.

Eh bien, que ferait un moteur non ECS dans cette situation ? La composition est toujours la réponse, mais dans ce cas, un nœud complet est déraisonnable lorsqu'il est mis à l'échelle / complique la dynamique de la hiérarchie de propriété. Au lieu de cela, on peut simplement définir des objets d'implémentation non-Node qui résument l'implémentation concrète d'un comportement, mais qui appartiennent toujours au nœud racine. Cela peut être fait avec un script Reference .

# root.gd
extends KinematicBody2D

export(Script) var jump_impl_script = null setget set_jump_impl_script
var jump_impl
func set_jump_impl_script(p_script):
    jump_impl = p_script.new() if p_script else null

export(Script) var move_impl_script = null setget set_move_impl_script
var move_impl
func set_move_impl_script(p_script):
    move_impl = p_script.new() if p_script else null

func _physics_process():
    # use logic involving these...
    move_impl.move(...)
    jump_impl.jump(...)

Si les exportations fonctionnaient de telle manière que nous pouvions les modifier dans l'inspecteur en tant qu'énumérations pour les classes qui dérivent d'un type particulier, comme nous le pouvons pour les nouvelles instances de Resource, alors ce serait cool. La seule façon de le faire maintenant serait de corriger les exportations de scripts de ressources, puis de faire en sorte que votre script d'implémentation étende Resource (sans autre raison). Cependant, les faire étendre Resource serait une bonne idée si l'implémentation elle-même nécessite des paramètres que vous aimeriez pouvoir définir à partir de l'inspecteur. :-)

Maintenant, ce qui faciliterait CECI serait d'avoir un système d'extraits de code ou un système de macros afin que la création de ces sections de code déclaratives réutilisées soit plus facile pour les développeurs.

Quoi qu'il en soit, oui, je pense avoir en quelque sorte identifié des problèmes flagrants avec un système de traits et une meilleure approche pour résoudre le problème. Hourra pour les problèmes de problème XY ! /s

Éditer:

Ainsi, le flux de travail de l'exemple ci-dessus impliquerait de définir le script d'implémentation, puis d'utiliser l'instance du script lors de l'exécution pour définir le comportement. Mais que se passe-t-il si l'implémentation elle-même nécessite des paramètres que vous souhaitez définir de manière statique à partir de l'inspecteur ? Voici une version basée sur une ressource à la place.

# root.gd
extends KinematicBody2D

# if you use a Resource script AND had a way of specifying that the assigned Resource 
# must extend that script, then the editor would automatically assign an instance of 
# that resource script to the var. No separate instancing or setter necessary.

export(Resource) var jump_impl = null # set jump duration, max height, tween easing via Inspector
export(Resource) var move_impl = null # similarly customize movement from Inspector

# can then create different Resources as different implementations. Because they are resources,
# one can edit them even outside of a scene!
func _physics_process():
    move_impl.move(...)
    jump_impl.jump(...)

Connexe : #22660

@AfterRebelion

Tenez en compte, je parle du flux de travail affiché, pas de la fonctionnalité utilisée

Il est ironique qu'après avoir clarifié cela et que je sois d'accord avec le flux de travail optimal, puis en désaccord avec les parties ultérieures du commentaire, je le suive en disant essentiellement comment la "fonctionnalité utilisée" dans cette vidéo est en fait le moyen idéal pour aborder ce problème à Godot de toute façon . Ha ha.

# root.gd
extends KinematicBody2D

export(Script) var jump_impl_script = null setget set_jump_impl_script
var jump_impl
func set_jump_impl_script(p_script):
jump_impl = p_script.new() if p_script else null
...

Wow, je ne savais pas que l'exportation était si puissante. On s'attendait à ce qu'il ne puisse interagir qu'avec des primitives et d'autres structures de données.

Cela rend mon commentaire précédent invalide.
Comme vous l'avez dit, si une sorte de macro est implémentée pour faciliter son implémentation, ce serait le meilleur moyen d'implémenter ce flux de travail sans avoir besoin de MultiScript. Peut-être pas aussi polyvalent que Unity, car vous auriez toujours besoin de déclarer tous les scripts possibles à l'avance, mais cela reste une bonne option.

@AfterRebelion

Comme vous l'avez dit, si une sorte de macro est implémentée pour faciliter son implémentation, ce serait le meilleur moyen d'implémenter ce flux de travail sans avoir besoin de MultiScript.

Eh bien, l'approche basée sur Resource que j'ai mentionnée dans le même commentaire, combinée à un meilleur support d'éditeur de # 22660, rendrait la qualité comparable à ce que Unity peut faire.

Peut-être pas aussi polyvalent que Unity, car vous auriez toujours besoin de déclarer tous les scripts possibles à l'avance, mais cela reste une bonne option.

Eh bien, s'ils corrigent les indications de type de tableau dans la version 3.2, vous pouvez définir un tableau exporté pour les chemins de fichiers qui doivent étendre un script et ajouter efficacement le vôtre. Cela pourrait même être fait via un plugin en 3.1 en utilisant la classe EditorInspectorPlugin pour ajouter du contenu personnalisé à l'inspecteur pour certaines ressources ou nœuds.

Je veux dire, si vous vouliez un système de type "Unity", alors vous AURIEZ vraiment des sous-nœuds qui disent à la racine quoi faire et vous n'auriez qu'à les récupérer en vous référant à leur nom, sans les déclarer ou les ajouter manuellement à partir du script du nœud racine. La méthode Resource est généralement beaucoup plus efficace et maintient une base de code plus propre.

Étant donné que le système de traits mettrait une pression excessive sur la convivialité du code GDScript, sur la base des raisons que j'ai exposées, je vais continuer et clore ce problème. Les pièges de son ajout l'emportent de loin sur les avantages relativement maigres que nous pourrions recevoir d'un tel système, et ces mêmes avantages peuvent être mis en œuvre de différentes manières avec plus de clarté et de convivialité.

Eh bien, j'ai raté cette discussion pendant mon absence. Je n'ai pas encore tout lu, mais j'ai eu l'idée d'ajouter des traits à GDScript pour résoudre le problème de réutilisation du code, que je trouve bien plus élégant et clair que le faux héritage multiple consistant à attacher un script à un type dérivé. Bien que ce que je ferais était de créer des fichiers de traits spécifiques, ne permettant à aucun fichier de classe d'être un trait. Je ne pense pas que ce serait trop mal.

Mais je suis ouvert aux suggestions pour résoudre le problème principal, qui consiste à réutiliser le code dans plusieurs types de nœuds.

@vnen la solution que j'ai trouvée et qui est le dernier élément est d'externaliser les sections réutilisables vers des scripts de ressources.

  • Ils peuvent toujours être exposés et leurs propriétés modifiées dans l'inspecteur, comme s'il s'agissait de variables membres sur un nœud.

  • Ils conservent une trace claire de l'origine des données et de la logique, ce qui pourrait facilement être compromis si de nombreux traits sont inclus dans un seul script.

  • Ils n'ajoutent aucune complexité excessive à GDScript en tant que langage. Par exemple, s'ils existaient, nous aurions à résoudre comment les qualités partagées (propriétés, constantes, signaux) seraient fusionnées dans le script principal (ou, si elles ne sont pas fusionnées, obligeraient les utilisateurs à gérer les conflits de dépendance).

  • Les scripts de ressources sont meilleurs car ils peuvent être affectés et modifiés à partir de l'inspecteur. Les concepteurs, rédacteurs, etc. pourront modifier les objets d'implémentation directement depuis l'éditeur.

@willnationsdev je vois (bien que le nom "scripts de ressources" semble étrange, puisque tous les scripts sont des ressources). Le principal problème avec cette solution est qu'elle ne résout pas quelque chose que les gens attendent avec l'approche d'héritage : ajouter des variables et des signaux exportés au nœud racine de la scène (en particulier lors de l'instanciation de la scène ailleurs). Vous pouvez toujours modifier les variables exportées à partir des sous-ressources, mais cela devient moins pratique (vous ne pouvez pas dire en un coup d'œil quelles propriétés vous pouvez modifier).

L'autre problème est qu'il nécessite de répéter beaucoup de code passe-partout. Vous devez également vous assurer que les scripts de ressources sont réellement définis avant d'appeler les fonctions.

L'avantage est qu'on n'a besoin de rien, c'est déjà disponible. Je suppose que cela devrait être documenté et que les personnes utilisant la méthode actuelle "attacher le script au nœud dérivé" pourraient commenter la solution pour voir à quel point elle est viable.

@vnen

mais cela devient moins pratique (vous ne pouvez pas dire d'un coup d'œil quelles propriétés vous pouvez modifier).

Pouvez-vous préciser ce que vous voulez dire ici ? Je ne vois pas comment il pourrait y avoir un manque de clarté substantiel quant aux propriétés accessibles, surtout si quelque chose comme # 22660 était fusionné.

  1. J'instancie la scène, je veux savoir comment je peux la modifier, et je regarde donc le script du nœud racine de cette scène.
  2. Dans le script, je vois...

    • export(MoveImpl) var move_impl = FourWayMoveImpl.new()

    • use FourWayMoveTrait

  3. En supposant que nous ayons un moyen de tracer l'identifiant (qui devrait vraiment être une fonctionnalité 3.1.1 !) pour ouvrir le script, nous finissons alors par ouvrir le script associé et pouvons afficher ses propriétés.

Cela me semble être le même nombre d'étapes, à moins qu'il ne me manque quelque chose.

De plus, quelles propriétés sont modifiables sont en fait encore plus claires avec les ressources, je dirais puisque, si une implémentation a des propriétés qui lui sont spécifiques, alors le fait que les données sont liées à l'implémentation est plus clair si vous devez y accéder avec la préface instance d'implémentation, c'est-à-dire move_impl.<property> .

L'autre problème est qu'il nécessite de répéter beaucoup de code passe-partout. Vous devez également vous assurer que les scripts de ressources sont réellement définis avant d'appeler les fonctions.

C'est vrai, cependant, je pense toujours que les avantages d'une exportation avec une initialisation l'emportent sur le coût du verbe ajouté:

("High-Level Teammate", HLT, comme les designers, les écrivains, les artistes, etc.)

  • On peut attribuer les valeurs directement à partir de l'inspecteur, plutôt que d'avoir à ouvrir le script, à localiser la ligne correcte à modifier, puis à la modifier (déjà mentionné, mais cela mène à...).

  • On peut spécifier que le contenu exporté a une exigence de type de base. L'inspecteur peut alors fournir automatiquement une liste énumérée des implémentations autorisées. Les HLT peuvent alors attribuer en toute sécurité uniquement les dérivations de ce type. Cela les aide à les isoler de l'alternative d'avoir besoin de connaître les ramifications de tous les différents scripts de traits qui circulent. Nous devrions également modifier la saisie semi-automatique dans GDScript pour prendre en charge la recherche de fichiers de traits nommés et sans nom en réponse à la vue du mot-clé use .

  • On peut sérialiser une configuration d'une implémentation sous forme de fichier *.tres. Les HLT peuvent ensuite les glisser-déposer depuis le dock FileSystem ou même créer leur propre droit dans l'inspecteur. Si l'on souhaitait faire la même chose avec les traits, il faudrait créer un trait dérivé qui fournit un constructeur personnalisé pour remplacer celui par défaut. Ensuite, ils utiliseraient ce trait à la place comme une "préconfiguration" via un constructeur codé de manière impérative.

    1. Plus faible car il est impératif plutôt que déclaratif.
    2. Plus faible car le constructeur doit être explicitement défini dans le script.
    3. Si le trait n'a pas de nom, l' utilisateur devra savoir où se trouve le trait afin de l'utiliser correctement à la place du trait de base par défaut. Si le trait est nommé, il obstrue inutilement l'espace de noms global.
    4. S'ils changent le script pour dire use FourWayMoveTrait au lieu de use MoveTrait , il n'y a plus aucune indication durable que le script est même compatible avec la base MoveTrait . Cela permet à HLT de semer la confusion quant à savoir si le FourWayMoveTrait peut même changer en un autre MoveTrait sans casser les choses.
    5. Si un HLT créait une nouvelle implémentation de trait de cette manière, il ne connaîtrait pas nécessairement toutes les propriétés qui peuvent/doivent être définies à partir du trait de base. Il ne s'agit pas d'un problème lié aux ressources créées dans l'inspecteur.
  • On peut même avoir plusieurs ressources du même type (s'il y a une raison à cela). Un trait ne supporterait pas cela, mais déclencherait à la place des conflits d'analyse.

  • On peut modifier les configurations et/ou leurs valeurs individuelles sans jamais quitter la fenêtre 2D/3D. C'est beaucoup plus confortable pour les HLT (j'en connais beaucoup qui s'énervent carrément quand ils doivent regarder du code).

Avec tout cela dit, je suis d'accord que le passe-partout est ennuyeux. Pour faire face à cela, je préfère ajouter un système de macro ou d'extrait de code pour le simplifier. Un système d'extraits de code serait une bonne idée car il pourrait éventuellement prendre en charge n'importe quel langage pouvant être modifié dans le ScriptEditor, plutôt que simplement GDScript seul.

Sur:

mais cela devient moins pratique (vous ne pouvez pas dire d'un coup d'œil quelles propriétés vous pouvez modifier).

Je parlais de l'inspecteur, donc je veux dire le "HLT" ici. Les gens qui ne regarderont pas dans le code. Avec les traits, nous pourrions ajouter de nouvelles propriétés exportées au script. Avec les scripts de ressources, vous ne pouvez exporter que des variables vers la ressource elle-même, elles ne s'afficheront donc pas dans l'inspecteur, sauf si vous modifiez la sous-ressource.

Je comprends votre argument mais cela va au-delà du problème initial : évitez de répéter du code (sans héritage). Les traits sont pour les programmeurs. Dans de nombreux cas, il n'y a qu'une seule implémentation que vous voulez réutiliser, et vous ne voulez pas l'exposer dans l'inspecteur. Bien sûr, vous pouvez toujours utiliser les scripts de ressources sans exporter simplement en les affectant directement dans le code, mais cela ne résout toujours pas le problème de la réutilisation des variables exportées et des signaux de l'implémentation commune, qui est l'une des principales raisons pour lesquelles les gens essaient de utiliser l'héritage.

Autrement dit, les gens essaient actuellement d'utiliser l'héritage d'un script générique non seulement pour les fonctions, mais aussi pour les propriétés exportées (qui sont généralement liées à ces fonctions) et les signaux (qui peuvent ensuite être connectés à l'interface utilisateur).

C'est le problème difficile à résoudre. Il existe plusieurs façons de le faire avec GDScript seul, mais encore une fois, ils nécessitent de copier du code passe-partout.

Je ne peux qu'imaginer les allers-retours qui devraient avoir lieu pour que les scripts externes se comportent comme s'ils étaient écrits directement dans le script principal lui-même.

Ce serait une bonne chose si le ciel et la terre n'avaient pas à être déplacés pour y parvenir. X)

@vnen Je vois ce que vous dites maintenant. Eh bien, puisqu'il semble qu'il y ait encore un peu de vie dans ce numéro, je le rouvrirai la prochaine fois que j'en aurai l'occasion.

Des nouvelles de la réouverture ? Maintenant que la GodotCon 2019 est annoncée et que le Godot Sprint est une chose, cela vaut peut-être la peine d'en parler.

@AfterRebelion J'avais juste oublié de revenir et de le rouvrir. Merci de me le rappeler. XD

@willnationsdev J'ai aimé ce que j'ai lu concernant EditorInspectorPlugin ! question rapide, cela signifie que je peux créer un inspecteur personnalisé pour un type de données... par exemple... ajouter un bouton à l'inspecteur.
(cela fait un certain temps que je voulais faire cela, pour avoir un moyen de déclencher des événements à des fins de débogage avec un bouton dans l'inspecteur, je pourrais faire en sorte que le bouton exécute un script)

@xDGameStudios Oui , cela et bien plus encore est tout à fait possible. Tout contrôle personnalisé peut être ajouté à l'inspecteur, soit en haut, en bas, au-dessus d'une propriété ou en dessous d'une catégorie.

@willnationsdev Je ne sais pas si je peux te contacter par message privé !! Mais j'aimerais en savoir plus sur EditorInspectorPlugin (comme un exemple de code) .. quelque chose dans les lignes d'un type de ressource personnalisé (par exemple) MyResource qui a une propriété export "name" et un bouton d'inspecteur qui imprime le "name" variable si j'appuie dessus (dans l'éditeur ou pendant le débogage) ! La documentation fait cruellement défaut dans ce domaine... J'écrirais moi-même la documentation si je savais m'en servir ! :D merci

J'aimerais aussi en savoir plus. X)

Est-ce donc la même chose qu'un script de chargement automatique avec des sous-classes contenant des fonctions statiques ?

par exemple, votre cas deviendrait Traits.MoveRightTrait.move_right()

ou encore plus simple, avoir différents scripts de chargement automatique pour différentes classes de traits :

Movement.move_right()

Non, les traits sont une fonctionnalité spécifique à la langue équivalente au copier-coller du code source d'un script dans un autre script (un peu comme une fusion de fichiers). Donc, si j'ai un trait avec move_right() , et que je déclare ensuite que mon deuxième script utilise ce trait, alors il peut aussi utiliser move_right() , même s'il n'est pas statique et même s'il accède propriétés ailleurs dans la classe. Cela entraînerait simplement une erreur d'analyse si la propriété n'existait pas dans le deuxième script.

J'ai découvert que j'avais soit du code en double (fonctions plus petites, par exemple créer un arc) soit des nœuds superflus car je ne peux pas avoir d'héritage multiple.

Ce serait génial, je me retrouve à devoir créer un script avec exactement la même fonctionnalité juste pour être utilisé sur différents types de nœuds, causant différents extend s donc fondamentalement juste pour une ligne de code. Au fait, si quelqu'un sait comment faire cela avec le système actuel, faites-le moi savoir.

Si j'ai des fonctionnalités pour un script avec extends Node , existe-t-il un moyen d'attacher le même comportement à un autre type de nœud sans avoir à dupliquer le fichier source et à le remplacer par extend approprié ?

Des progrès là-dessus ? Je continue à devoir dupliquer du code ou à ajouter des nœuds, comme je l'ai déjà dit. Je sais que ce ne sera pas fait en 3.1, mais peut-être viser la 3.2 ?

Oh, je n'ai pas du tout travaillé là-dessus. En fait, je suis plus avancé sur la mise en œuvre de ma méthode d'extension que sur cela. J'ai besoin de parler avec vnen des deux, car j'aimerais travailler avec lui sur les meilleurs détails de syntaxe/implémentation.

Edit : si quelqu'un d'autre veut essayer d'implémenter des traits, il est le bienvenu. Juste besoin de se coordonner avec les devs.

"mise en œuvre de la méthode d'extension" ?

@Zireael07 #15586
Il permet aux gens d'écrire des scripts qui peuvent ajouter de nouvelles fonctions "intégrées" pour les classes de moteur. Mon interprétation de la syntaxe serait quelque chose comme ceci:

static Array func sum(p_self: Array):
    if not len(p_self):
        return 0
    var value = p_self[0]
    for i in range(1, len(p_self)):
        value += p_self[i]
    return value

Ensuite, ailleurs, je pourrais simplement faire:

var arr = [1, 2, 3]
print(arr.sum()) # prints 6

Il appellerait secrètement le script d'extension constamment chargé et appellerait la fonction statique du même nom qui est liée à la classe Array, en transmettant l'instance comme premier paramètre.

J'ai eu des discussions avec @vnen et @jahd2602 à ce sujet. Une chose qui me vient à l'esprit est la solution de Jai au polymorphisme : importer l'espace de noms d'une propriété.

De cette façon, vous pourriez faire quelque chose comme ceci :

class A:
    var a_prop: String = "Hello"
    func foo():
        print("A's a_prop: ", a_prop)
    func bar():
        print("A's bar()")

class B:
    using var a: A = A.new()
    var a_prop: String = "World" # Overriding A's a_prop

    func bar():  # Overriding A's bar()
        print("B's bar()")

func main():
    var b: B = B.new()
    b.foo() # output: "A's a_prop: World"
    b.bar() # output: "B's bar()"

Le fait est que le mot-clé using importe l'espace de noms d'une propriété, de sorte que b.foo() n'est vraiment que du sucre syntaxique pour b.a.foo() .

Et ensuite, assurez-vous que b is A == true et que B peuvent être utilisés dans des situations typées qui acceptent également A.

Cela a également l'avantage que les choses n'ont pas à être déclarées comme des traits, cela fonctionnerait pour tout ce qui n'a pas de noms de qualité communs.

Un problème est que cela ne cadre pas bien avec le système d'héritage actuel. Si A et B sont Node2D et que nous créons une fonction dans A : func baz(): print(self.position) , quelle position sera imprimée lorsque nous appellerons b.baz() ?
Une solution pourrait être de demander à l'appelant de déterminer self . Appeler b.foo() appellerait foo() avec b comme self et bafoo() appellerait foo() avec a comme self.

Si nous avions des méthodes autonomes telles que Python (où x.f(y) est du sucre pour f(x,y) ), cela pourrait être très facile à implémenter.

Une autre idée sans rapport :

Concentrez-vous uniquement sur les fonctions autonomes, style JavaScript.

Si nous adoptons la convention x.f(y) == f(x,y) pour les fonctions statiques, nous pourrions très facilement avoir ce qui suit :

class Jumper:
    static func jump(_self: KinematicBody2D):
        # jump implementation

class Runner:
    static func run(_self: KinematicBody2D, direction: Vector2):
        # run implementation

class Character:
    extends KinematicBody2D
    func run = Runner.run       # Example syntax
    func jump = Jumper.jump

func main():
    var character = Character.new()
    character.jump()
    character.run(Vector2(1,0))

Cela aurait un impact minimal sur le système de classes, car cela n'affecte vraiment que les méthodes. Si nous voulions vraiment que cela soit flexible, cependant, nous pourrions utiliser entièrement JavaScript et autoriser simplement les définitions de fonction à être des valeurs assignables et appelables.

@jabcross Ça a l'air bien, j'aime le concept d'une sorte d'espacement de noms facultatif, et l'idée de la fonction est intéressante.

En ce qui concerne l'espace de noms, je me demande pourquoi pas simplement using A , les autres choses déclaratives semblent étrangères.

Curieux aussi de savoir comment cela devrait être résolu avec l'héritage multiple. Je suppose que l'option A force les deux scripts à hériter du même type, ils sont donc tous deux étendus au-dessus de la même classe, sans aucune fusion spéciale.

Option B, peut-être des mots-clés GDScript supplémentaires pour spécifier une classe de traits, et de quelle classe vous aimeriez avoir des indices. Ce serait la même idée, mais seulement des étapes supplémentaires pour apparaître plus explicites.

Au sommet d'A.gd :

extends Trait as Node2D
is Trait as Node2D
is Trait extends B
extends B as Trait

Ohhh, j'aime vraiment le concept d'importation d'espace de noms. Cela résout non seulement le problème des traits, mais aussi potentiellement le concept de "méthodes d'extension" pour ajouter du contenu aux types de moteurs.

class_name ArrayExt
static func sum(_self: Array) -> int:
    var sum: int = 0
    for a_value in _self:
        sum += a_value
    return sum

using ArrayExt
func _ready():
    var a = [1, 2, 3]
    print(a.sum())

@jabcross Si nous ajoutions également des lambas et/ou autorisions des objets à implémenter un opérateur d'appel (et avions un type callable pour les valeurs compatibles), alors nous pourrions commencer à ajouter une approche plus fonctionnelle au code GDScript (qui Je pense que ce serait une excellente idée). Certes, marcher davantage sur le territoire # 18698 de @vnen à ce stade, mais …

Nous devons considérer que GDScript est toujours un langage dynamique et certaines de ces suggestions nécessitent une vérification d'exécution pour envoyer correctement les appels, ajoutant une pénalité de performance (même pour les personnes qui n'utilisent pas les fonctionnalités, car il doit vérifier tous les appels de fonction et peut-être aussi la recherche de propriété). C'est aussi pourquoi je ne sais pas si l'ajout d'extensions est une bonne idée (d'autant plus qu'il s'agit essentiellement de sucre de syntaxe).

Je préfère le système des traits purs, où les traits ne sont pas des classes mais une chose en soi. De cette façon, ils peuvent être entièrement résolus au moment de la compilation et fournir des messages d'erreur en cas de conflits de noms. Je pense que cela résoudrait le problème sans frais d'exécution supplémentaires.

@vnen Ahhh, je n'avais pas réalisé cela à propos du coût d'exécution. Et si cela s'applique à toute implémentation de méthode d'extension, alors je suppose que ce ne serait pas idéal non plus.

Si nous faisions alors un système de traits purs, pensiez-vous simplement faire trait TraitName à la place de extends associé à un using TraitName under extend dans d'autres scripts ? Et le mettriez-vous en œuvre vous-même, ou serait-il délégué ?

Si nous faisions alors un système de traits purs, pensiez-vous simplement faire trait TraitName à la place de extends associé à un using TraitName under extend dans d'autres scripts ?

C'est mon idée. Je pense que c'est assez simple et couvre à peu près tous les cas d'utilisation pour la réutilisation du code. J'autoriserais même la classe utilisant le trait à remplacer les méthodes de trait (si c'est possible de le faire au moment de la compilation). Les traits pourraient également étendre d'autres traits.

Et le mettriez-vous en œuvre vous-même, ou serait-il délégué ?

Cela ne me dérangerait pas de confier la tâche à quelqu'un d'autre qui est à la hauteur. Je manque de temps de toute façon. Mais nous devrions nous mettre d'accord sur la conception à l'avance. Je suis assez flexible avec les détails mais cela devrait 1) ne pas entraîner de vérifications d'exécution (croyez que GDScript se prête bien à des choses impossibles à comprendre lors de la compilation), 2) être relativement simple et 3) ne pas trop ajouter à temps de compilation.

@vnen J'aime ces idées. Je me demandais comment vous imaginiez qu'un trait puisse faire des choses comme l'auto-complétion pour les classes qui l'incluraient, ou cela ne serait-il pas possible ?

Je me demandais comment vous imaginiez qu'un trait puisse faire des choses comme l'auto-complétion pour les classes qui l'incluraient, ou cela ne serait-il pas possible ?

Un trait serait essentiellement une "importation" à mon avis. Il devrait être trivial de montrer l'achèvement, en supposant que l'achèvement pour les membres fonctionne.

@vnen J'imagine que vous analyseriez essentiellement un ClassNode avec un indicateur trait défini dessus. Et puis, si vous faites une instruction using , elle tentera de fusionner toutes les propriétés/méthodes/signaux/constantes/sous-classes dans le script actuel.

  1. Si une méthode entre en conflit, l'implémentation du script actuel remplacera la méthode de base, comme si elle redéfinissait une méthode héritée.

    • Mais que faire si une classe de base a déjà la méthode "merged" ?

  2. Si une propriété, un signal et une constante se chevauchent, vérifiez s'il s'agit du même type/signal de signature. S'il n'y a pas de décalage, considérez-le simplement comme une "fusion" de la propriété/du signal/de la constante. Sinon, informez l'utilisateur d'un conflit de type/signature (une erreur d'analyse).

Mauvaise idée? Ou devrions-nous carrément interdire les conflits sans méthode ? Et les sous-classes ? J'ai le sentiment que nous devrions en faire des conflits.

@willnationsdev Cela ressemble au "problème du diamant" (alias "diamant mortel de la mort"), une ambiguïté bien documentée avec différentes solutions déjà appliquées dans divers langages de programmation populaires.

Ce qui me rappelle:
@vnen est-ce que les traits vont pouvoir étendre d'autres traits ?

@ jahd2602 Il a déjà suggéré que comme possibilité

Les traits pourraient également étendre d'autres traits.

@ jahd2602 Sur la base des solutions Perl/Python, il semble qu'elles forment essentiellement une "pile" de couches contenant le contenu de chaque classe afin que les conflits du dernier trait utilisé reposent sur les autres versions et les écrasent. Cela semble être une assez bonne solution pour ce scénario. Sauf si vous ou @vnen avez des pensées alternatives. Merci pour l'aperçu lié des solutions jahd.

Quelques questions.

Premièrement : de quelles manières devrions-nous prendre en charge l'instruction using ?

Je pense que l'instruction using devrait nécessiter un GDScript à valeur constante.

using preload("res://my_trait.gd") # a preloaded expression
using ScriptClass.MyTrait # a const resource
using Autoload.MyTrait # a const resource
using MyTrait # a regular script class

Je pense à tout ce qui précède.

Deuxièmement : quelle devrait être la syntaxe autorisée pour définir un trait et/ou son nom ?

Quelqu'un ne veut pas nécessairement utiliser une classe de script pour son trait, donc je ne pense pas que nous devrions appliquer une exigence trait TraitName . Je pense que trait doit être sur une ligne.

Donc, s'il s'agit d'un script d'outil, il devrait bien sûr avoir tool en haut. Ensuite, si c'est un trait, il doit définir s'il s'agit d'un trait sur la ligne suivante. Autorisez éventuellement quelqu'un à indiquer le nom de la classe de script après la déclaration de trait sur la même ligne et s'il le fait, ne lui permettez pas d'utiliser également class_name . S'ils omettent le nom du trait, alors class_name <name> convient. Ensuite, lors de l'extension d'un autre type, nous pourrions insérer le extends après la déclaration de trait et/ou sur une ligne distincte après la déclaration de trait. Donc, je considérerais chacun de ces valides:

# Global name from trait keyword.
trait MyTrait extends BaseTrait

# Global name from class_name keyword, but is still a trait and also happens to be a tool script.
tool
trait
extends BaseTrait
class_name MyTrait

# A trait with no global name associated with it. Does not extend anything.
trait

Troisièmement : devrions-nous, à des fins d'auto-complétion et/ou de déclaration d'intention/d'exigences, permettre à un trait de définir un type de base qu'il doit étendre ?

Nous avons déjà discuté du fait que les traits devraient prendre en charge l'héritage d'autres traits. Mais devrions-nous autoriser un TraitA à extend Node , autoriser le script TraitA à obtenir l'auto-complétion de Node, mais également déclencher une erreur d'analyse si nous faisons une instruction using TraitA lorsque la classe actuelle n'étend pas Node ou l'un de ses types dérivés?

Quatrième : plutôt que d'avoir des traits qui étendent d'autres traits, ne pouvons-nous pas simplement garder l'instruction extends réservée aux extensions de classe, permettre à un trait de ne pas avoir du tout besoin de cette instruction, mais plutôt que d'étendre un trait de base, autoriser traits pour avoir simplement leurs propres déclarations using qui sous-importent ces traits ?

# base_trait.gd
trait
func my_method():
    print("Hello")

# derived_trait.gd
trait
using preload("base_trait.gd")
func my_method():
   print("World") # overrides previous method, will only print "World".

Bien sûr, l'avantage ici serait que vous seriez en mesure de regrouper plusieurs traits sous un seul nom de trait en utilisant plusieurs instructions using , similaires aux fichiers d'inclusion C++ qui incluent plusieurs autres classes.

Cinquième : si nous avons un trait, et qu'il a un using ou un extends pour une méthode, puis implémente le sien, que faisons-nous quand il appelle, dans cette fonction .<method_name> pour exécuter l'implémentation de base ? Supposons-nous que ces appels s'exécutent toujours dans le contexte d'héritage de classe et que la hiérarchie des traits n'a aucune influence ici ?

cc @vnen

Premièrement : de quelles manières devrions-nous prendre en charge l'instruction using ?

Je pense que l'instruction using devrait nécessiter un GDScript à valeur constante.

using preload("res://my_trait.gd") # a preloaded expression
using ScriptClass.MyTrait # a const resource
using Autoload.MyTrait # a const resource
using MyTrait # a regular script class

Je suis d'accord avec tout ça. Mais pour le chemin, j'utilise directement une chaîne : using "res://my_trait.gd"

Deuxièmement : quelle devrait être la syntaxe autorisée pour définir un trait et/ou son nom ?

Quelqu'un ne veut pas nécessairement utiliser une classe de script pour son trait, donc je ne pense pas que nous devrions appliquer une exigence trait TraitName . Je pense que trait doit être sur une ligne.

Donc, s'il s'agit d'un script d'outil, il devrait bien sûr avoir tool en haut. Ensuite, si c'est un trait, il doit définir s'il s'agit d'un trait sur la ligne suivante. Autorisez éventuellement quelqu'un à indiquer le nom de la classe de script après la déclaration de trait sur la même ligne et s'il le fait, ne lui permettez pas d'utiliser également class_name . S'ils omettent le nom du trait, alors class_name <name> convient. Ensuite, lors de l'extension d'un autre type, nous pourrions insérer le extends après la déclaration de trait et/ou sur une ligne distincte après la déclaration de trait. Donc, je considérerais chacun de ces valides:

# Global name from trait keyword.
trait MyTrait extends BaseTrait

# Global name from class_name keyword, but is still a trait and also happens to be a tool script.
tool
trait
extends BaseTrait
class_name MyTrait

# A trait with no global name associated with it. Does not extend anything.
trait

tool sur un trait ne devrait pas faire de différence, car ils ne sont pas exécutés directement.

Je suis d'accord qu'un trait n'a pas nécessairement un nom global. J'utiliserais trait de la même manière que tool . Ce doit être la première chose dans le fichier de script (sauf pour les commentaires). Le mot-clé doit éventuellement être suivi du nom du trait. Je n'utiliserais pas class_name pour eux, car ce ne sont pas des classes.

Troisièmement : devrions-nous, à des fins d'auto-complétion et/ou de déclaration d'intention/d'exigences, permettre à un trait de définir un type de base qu'il doit étendre ?

Honnêtement, je n'aime pas ajouter des fonctionnalités dans le langage pour le bien de l'éditeur. C'est là que les annotations seraient utiles.

Maintenant, si nous voulons que les traits ne s'appliquent qu'à un certain type (et à ses dérivés), alors ça va. En fait, je pense que c'est mieux pour les vérifications statiques : cela permet à un trait d'utiliser des éléments d'une classe tandis que la compilation peut réellement vérifier s'ils sont utilisés avec les types corrects et ainsi de suite.

Quatrième : plutôt que d'avoir des traits qui étendent d'autres traits, ne pouvons-nous pas simplement garder l'instruction extends réservée aux extensions de classe, permettre à un trait de ne pas avoir du tout besoin de cette instruction, mais plutôt que d'étendre un trait de base, autoriser traits pour avoir simplement leurs propres déclarations using qui sous-importent _ces_ traits ?

Eh bien, c'est surtout une question de sémantique. Lorsque j'ai mentionné que les traits peuvent en étendre un autre, je ne voulais pas vraiment utiliser le mot-clé extends . La principale différence est qu'avec extends vous ne pouvez en étendre qu'un seul, avec using vous pouvez intégrer de nombreux autres traits en un seul. Je suis d'accord avec using , tant qu'il n'y a pas de cycles, ce n'est pas un problème.

Cinquième : si nous avons un trait, et qu'il a un using ou un extends pour une méthode, puis implémente le sien, que faisons-nous quand il appelle, dans cette fonction .<method_name> pour exécuter l'implémentation de base ? Supposons-nous que ces appels s'exécutent toujours dans le contexte d'héritage de classe et que la hiérarchie des traits n'a aucune influence ici ?

C'est une question délicate. Je suppose qu'un trait n'a rien à voir avec l'héritage de classe. Ainsi, la notation par points doit appeler la méthode sur le trait parent, s'il y en a un, sinon générer une erreur. Un trait ne doit pas être conscient des classes dans lesquelles il se trouve.

OTOH, un trait est presque comme un "include", il serait donc appliqué textuellement à la classe, appelant ainsi l'implémentation parente. Mais honnêtement, j'interdirais simplement la notation par points si la méthode ne se trouve pas dans le trait parent.

Qu'en est-il d'un trait qui nécessite que la classe ait un ou plusieurs autres traits ? Par exemple, un trait DoubleJumper qui nécessite à la fois le trait Jumper , un trait Upgradable et une classe qui hérite KinematicBody2D .

Rust, par exemple, vous permet d'utiliser des signatures de type comme celles-ci. Quelque chose comme KinematicBody2D: Jumper, Upgradable . Mais puisque nous utilisons : pour annoter le type, nous pourrions simplement utiliser KinematicBody2D & Jumper & Upgradable ou quelque chose du genre.

Il y a aussi le problème du polymorphisme. Que se passe-t-il si l'implémentation du trait est différente pour chaque classe, mais qu'elle expose la même interface ?

Par exemple, nous voulons une méthode kill() dans le trait Jumper , qui est utilisé à la fois par Enemy et Player . Nous voulons des implémentations différentes pour chaque cas, tout en gardant les deux compatibles avec la même signature de type Jumper . Comment faire cela ?

Pour le polymorphisme, vous créeriez simplement un trait distinct qui inclut le trait avec kill() , puis implémente sa propre version spécifique de la méthode. L'utilisation de traits qui remplacent les méthodes de traits précédemment incluses est la façon dont vous le géreriez.

De plus, je ne pense pas qu'il y ait (encore) de plans pour faire en sorte qu'un indice de type ait des exigences de trait. Est-ce quelque chose que nous voudrions faire?

créer un trait distinct

Cela ne générerait-il pas un tas de fichiers de traits uniques ? Si nous pouvions faire des déclarations de traits imbriquées (similaire au mot-clé class ), cela pourrait être plus pratique. Nous pourrions également remplacer les méthodes directement dans la classe qui utilise le trait.

J'apprécierais vraiment un système de signature de type fort (peut-être avec une composition booléenne et des options/non nulles). Les traits s'intégreraient parfaitement.

Je ne sais pas si cela a été discuté, mais je pense qu'il devrait être possible d'invoquer une version spécifique à un trait d'une fonction. Par example:

trait A
func m():
  print("A")

trait B
func m():
  print("B")

class C
using A
using B

func c():
  A.m()
  B.m()
  m()

qui affiche : A , B , B .


De plus, je ne suis pas tout à fait sûr du "pas de coût d'exécution". Comment seraient gérés les scripts chargés dynamiquement (non disponibles lors de l'exportation) avec des classes utilisant des traits définis avant l'exportation ? Est-ce que j'ai mal compris quelque chose ? Ou ce cas n'est-il pas considéré comme "d'exécution" ?

Je ne sais pas si cela a été discuté, mais je pense qu'il devrait être possible d'invoquer une version spécifique à un trait d'une fonction.

J'envisageais déjà cela, mais je ne suis pas sûr de permettre à une classe d'utiliser des traits conflictuels (c'est-à-dire des traits qui définissent la méthode avec le même nom). L'ordre des instructions using ne devrait faire aucune différence.

De plus, je ne suis pas tout à fait sûr du "pas de coût d'exécution". Comment seraient gérés les scripts chargés dynamiquement (non disponibles lors de l'exportation) avec des classes utilisant des traits définis avant l'exportation ? Est-ce que j'ai mal compris quelque chose ? Ou ce cas n'est-il pas considéré comme "d'exécution" ?

Il ne s'agit pas d'exporter. Cela aura certainement un impact sur le temps de chargement, puisque la compilation se produit au chargement (bien que je ne pense pas que ce soit très important), mais cela ne devrait pas avoir d'impact lorsque le script est en cours d'exécution. Idéalement, les scripts devraient être compilés à l'exportation, mais c'est une autre discussion.

Bonjour à tous.

Je suis nouveau sur Godot et je m'y suis habitué ces derniers jours. Alors que j'essayais de comprendre les meilleures pratiques à utiliser pour fabriquer des composants réutilisables, j'avais décidé d'un modèle. Je ferais toujours en sorte que le nœud racine d'une sous-scène, destiné à être instancié dans une autre scène, exporte toutes les propriétés que j'ai l'intention de définir de l'extérieur. Autant que possible, je voulais rendre la connaissance de la structure interne de la branche instanciée inutile au reste de la scène.

Pour que cela fonctionne, le nœud racine doit "exporter" les propriétés, puis copier les valeurs dans l'enfant approprié dans _ready. Ainsi, par exemple, imaginez un nœud Bomb avec un enfant Timer. Le nœud racine de la bombe dans la sous-scène exporterait "detonation_time", puis il ferait $Timer.wait_time = detonation_time dans _ready. Cela nous permet de bien le définir dans l'interface utilisateur de Godot chaque fois que nous l'instancions sans avoir à rendre les enfants modifiables et à accéder au minuteur.

pourtant
1) C'est une transformation très mécanique, il semble donc que quelque chose de similaire pourrait être pris en charge par le système
2) Cela ajoute probablement une légère inefficacité par rapport à la définition de la valeur appropriée directement dans le nœud enfant.

Avant de continuer, cela peut sembler tangentiel à ce qui est discuté car cela n'implique pas d'autoriser une sorte d'héritage "privé" (en langage C++). Cependant, j'aime vraiment le système de construction de comportements de Godot en composant des éléments de scène au lieu d'une ingénierie plus proche de l'héritage. Ces relations "écrites" sont immuables et statiques. OTOH, la structure de la scène est dynamique, vous pouvez même la modifier au moment de l'exécution. La logique du jeu est tellement sujette à changement au cours du développement que je pense que la conception de Godot correspond très bien au cas d'utilisation.

Il est vrai que les nœuds enfants sont utilisés comme extensions comportementales des nœuds racines, mais cela ne les empêche pas de manquer d'autosuffisance, IMO. Une minuterie est parfaitement autonome et son comportement est prévisible, quelle que soit l'heure à laquelle elle est habituée. Que vous utilisiez une cuillère pour boire de la soupe ou manger de la crème glacée, elle remplit convenablement sa fonction même si elle agit comme une extension de votre main. Je considère les nœuds racine comme des maestros qui coordonnent les comportements des nœuds enfants afin qu'ils n'aient pas à se connaître directement les uns les autres et qu'ils soient DONC capables de rester autonomes. Les nœuds parent/racine sont des gestionnaires liés au bureau qui délèguent des responsabilités mais ne font pas beaucoup de travail direct. Comme ils sont minces, il est facile d'en créer un nouveau pour un comportement légèrement différent.

Cependant, je pense que les nœuds racine devraient également agir comme l'INTERFACE principale de la fonctionnalité de l'ensemble de la branche instanciée. Toutes les propriétés qui peuvent être modifiées dans l'instance doivent être "réglables" dans le nœud racine de la branche même si le propriétaire ultime de la propriété est un nœud enfant. À moins que quelque chose ne me manque, cela doit être arrangé manuellement dans la version actuelle de Godot. Ce serait bien si cela pouvait être automatisé d'une manière ou d'une autre pour combiner les avantages d'un système dynamique avec des scripts plus faciles.

Une chose à laquelle je pense est un système "d'héritage dynamique", si vous voulez, disponible pour les sous-classes de Node. Il y aurait deux sources de propriétés/méthodes dans un tel script, celles du script qu'il étend et celles "remontées" des enfants au sein de la structure de la scène. Ainsi, mon exemple avec la bombe deviendrait quelque chose comme export lifted var $Timer.wait_time [= value?] as detonation_time dans la section des variables membres du script bomb.gd. Le système générerait essentiellement $Timer.wait_time = detonation_time dans le rappel _ready et générerait le getter/setter qui permettra à $Bomb.detonation_time = 5 du parent du nœud Bomb d'entraîner la définition de $Timer.wait_time = 5 .

Dans l'exemple de l'OP avec MoveRightTrait, nous aurions le nœud auquel mysprite.gd est attaché avoir MoveRightTrait comme nœud enfant. Ensuite, dans mysprite.gd, nous aurions quelque chose comme lifted func $MoveRightTrait.move_right [as move_right] (peut-être que 'as' pourrait être facultatif lorsque le nom sera le même). Désormais, appeler move_right sur un objet de script créé à partir de mysprite.gd déléguerait automatiquement au nœud enfant approprié. Peut-être que les signaux pourraient être mis en bulle afin qu'ils puissent être attachés à un nœud enfant à partir de la racine ? Peut-être que des nœuds entiers pourraient être bouillonnés avec seulement lifted $MoveRightTrait [as MvR] sans func, signal ou var. Dans ce cas, toutes les méthodes et propriétés sur MoveRightTrait seraient accessibles depuis mysprite directement en tant que mysprite.move_right ou via mysprite.MvR.move_right si le 'as MvR' est utilisé.

C'est une idée de la façon de simplifier la création d'une INTERFACE à une structure de scène à la racine d'une branche instanciée, en augmentant leur caractéristique de "boîte noire" et en obtenant une commodité de script ainsi que la puissance du système de scène dynamique de Godot. Bien sûr, il y aurait de nombreux détails secondaires à considérer. Par exemple, contrairement aux classes de base, les nœuds enfants peuvent être supprimés au moment de l'exécution. Comment les fonctions et propriétés affichées/levées doivent-elles se comporter si elles sont appelées/accessibles dans ce cas d'erreur ? Si un nœud avec le bon NodePath est rajouté, les propriétés levées fonctionnent-elles à nouveau ? [OUI, IMO] De plus, ce serait une erreur d'utiliser 'lifted' dans des classes non dérivées de Node car il n'y aurait jamais d'enfants à partir desquels bulle/lift dans ce cas. De plus, des conflits de noms sont possibles avec des doublons "as {name}" ou "lifted $Timer1 lifted $Timer2" où les nœuds ont des propriétés/méthodes portant le même nom. L'interpréteur de script devrait idéalement détecter de tels problèmes logiques.

J'ai l'impression que cela nous donnerait beaucoup de ce que nous voulons, même si ce n'est vraiment que du sucre syntaxique qui nous évite d'avoir à écrire des fonctions de transfert et des initialisations. De plus, comme c'est fondamentalement simple sur le plan conceptuel, cela ne devrait pas être si difficile à mettre en œuvre ou à expliquer.

Quoi qu'il en soit, si vous êtes arrivé jusqu'ici, merci d'avoir lu !

J'ai utilisé "levé" partout mais c'est juste illustratif.
Quelque chose comme using var $Timer.wait_time as detonation_time ou using $Timer est évidemment tout aussi bon. Dans tous les cas, vous pouvez facilement hériter de pseudo-nœuds enfants, créant ainsi un point d'accès unique cohérent à la fonctionnalité souhaitée à la racine de la branche à instancier. L'exigence sur les éléments de fonctionnalité réutilisables est qu'ils étendent Node ou une sous-classe de celui-ci afin qu'ils puissent être ajoutés en tant qu'enfants au composant plus grand.

Une autre façon de voir les choses est que le mot-clé "extends" sur un script qui hérite d'un nœud vous donne votre relation "est-un" tout en utilisant le mot-clé "using" ou "lifted" sur un script pour "faire remonter" un Les membres du nœud descendant vous donnent quelque chose qui s'apparente à des "implémentements" [hé, mot-clé possible] qui existe dans les langages avec un héritage unique mais plusieurs "interfaces" (par exemple, Java). Dans l'héritage multiple illimité (comme c++), les classes de base forment un arbre [statique, écrit]. Par analogie, je propose en quelque sorte de superposer une syntaxe pratique et une élimination passe-partout sur les arbres de nœuds existants de Godot.

S'il est un jour déterminé que c'est quelque chose qui mérite d'être exploré, il y a des aspects de l'espace de conception à considérer :
1) Devrions-nous autoriser uniquement les enfants immédiats dans une "utilisation". IOW using $Timer mais pas using $Bomb/Timer'? This would be simpler but would force us to write boilerplate in some cases. I say that a full NodePath ROOTED in the Node to which the script is attached should be legal [but NO references to parents/siblings allowed]. 2) Should there be an option that find_node's the "using"-ed node instead of following a written in NodePath? For example using "Timer" with a string for the pattern would be slower but the forwarding architecture would continue to work if a referenced node's position in the sub-tree changes at run time. This could be used selectively for child nodes that we expect to move around beneath the root. Of course syntax would have to be worked out especially when using a particular member (eg. using var "Timer".wait_time as detonation_time is icky). 3) Should there be a way query for certain functionality [equivalent to asking if an interface is implemented or a child node is present]? Perhaps "using" entire nodes with aliases should allow testing the alias to be a query. So using MoveRightTrait as DirectionalMover in a script would result in node.DirectionalMover returning the child MoveRightTrait. This is logical because node.DirectionalMover.move_right() calls the method on the child MoveRightTrait. Other nodes without that statement would return null. So the statement if node.DirectionalMover:` deviendrait un test pour la fonctionnalité par convention.
4) Le modèle d'état doit être implémentable en remplaçant un nœud "using" par un autre qui a un comportement différent mais la même interface [duck typing] et le même NodePath que celui référencé dans l'instruction "using". Avec la façon dont fonctionne l'arborescence des scènes, cela fonctionnerait presque gratuitement. Cependant, le système devrait suivre les signaux connectés via un parent et restaurer les connexions dans l'enfant remplacé.

Je travaille avec GDScript depuis un certain temps maintenant et je dois admettre qu'une sorte de fonctionnalité trait/mixin et proxy/délégation est absolument nécessaire. Il est assez ennuyeux d'avoir à configurer tout ce passe-partout juste pour connecter des propriétés ou exposer des méthodes d'enfants à la racine de la scène.

Ou ajouter des niveaux de l'arborescence uniquement pour simuler des composants (cela devient assez fastidieux assez rapidement, car vous cassez alors tous les chemins de nœuds avec chaque nouveau composant). Peut-être existe-t-il un meilleur moyen, quelque chose comme un méta/multi script permettant plusieurs scripts sur un nœud ? Si vous avez une solution idiomatique, merci de la partager...

Lancer C++ (GDNative) dans mix rend les choses encore pires, car _ready et _init se comportent différemment là-bas (lire : l'initialisation avec les valeurs par défaut fonctionne à moitié ou ne fonctionne pas du tout).

C'est la principale chose que je dois contourner dans GDScript. J'ai souvent besoin de partager des fonctionnalités entre les nœuds sans structurer toute ma structure d'héritage autour d'elle - par exemple, mon joueur et mes commerçants ont un inventaire, mon joueur + objets + ennemis ont des statistiques, mon joueur et mes ennemis ont des objets équipés, etc.

Actuellement, j'implémente ces "composants" partagés sous forme de classes ou de nœuds chargés dans les "entités" qui en ont besoin, mais c'est désordonné (ajoute beaucoup de recherches de nœuds, rend la frappe de canard presque impossible, etc.) et les approches alternatives ont leurs propres inconvénients donc Je n'ai pas trouvé de meilleur moyen. Les traits/mixins me sauveraient absolument la vie.

Il s'agit de pouvoir partager du code entre des objets sans utiliser l'héritage, ce qui, à mon avis, est à la fois nécessaire et impossible à faire proprement dans Godot pour le moment.

La façon dont je comprends les traits de Rust (https://doc.rust-lang.org/1.8.0/book/traits.html), c'est qu'ils sont comme les classes de types Haskell, où vous avez besoin que certaines fonctions paramétrées soient définies pour le type vous ajoutez un trait, puis vous pouvez utiliser des fonctions génériques définies sur tous les types qui implémentent un trait. Les traits de Rust sont-ils différents de ce qui est proposé ici ?

Celui-ci sera probablement migré en gros, car il a fait l'objet de discussions approfondies ici.

Celui-ci sera probablement migré en gros, car il a fait l'objet de discussions approfondies ici.

_Je trouve le "déplacement" des propositions inutile à mon avis, il vaut mieux les fermer et demander de les rouvrir dans godot-propositions si des personnes manifestent de l'intérêt, et de laisser d'autres propositions réellement mises en œuvre si besoin. De toute façon..._

Je suis tombé sur ce problème il y a un an, mais ce n'est que maintenant que je commence à comprendre l'utilité potentielle du système de traits.

Partager mon flux de travail actuel dans l'espoir d'inspirer quelqu'un à mieux comprendre le problème (et peut-être suggérer une meilleure alternative autre que la mise en œuvre du système de traits).

1. Créez un outil pour générer des modèles de composants pour chaque type de nœud utilisé dans le projet :

@willnationsdev https://github.com/godotengine/godot/issues/23101#issuecomment -431468744

Maintenant, ce qui faciliterait CECI serait d'avoir un système d'extraits de code ou un système de macros afin que la création de ces sections de code déclaratives réutilisées soit plus facile pour les développeurs.

Marcher dans tes pas... 😅

tool
extends EditorScript

const TYPES = [
    'Node',
    'Node2D',
]
const TYPES_PATH = 'types'
const TYPE_BASENAME_TEMPLATE = 'component_%s.gd'

const TEMPLATE = \
"""class_name Component{TYPE} extends {TYPE}

signal host_assigned(node)

export(bool) var enabled = true

export(NodePath) var host_path
var host

func _ready():
    ComponentCommon.init(self, host_path)"""

func _run():
    _update_scripts()


func _update_scripts():

    var base_dir = get_script().resource_path.get_base_dir()
    var dest = base_dir.plus_file(TYPES_PATH)

    for type in TYPES:
        var filename = TYPE_BASENAME_TEMPLATE % [type.to_lower()]
        var code = TEMPLATE.format({"TYPE" : type})
        var path = dest.plus_file(filename)

        print_debug("Writing component code for: " + path)

        var file = File.new()
        file.open(path, File.WRITE)
        file.store_line(code)
        file.close()

2. Créez une méthode statique à réutiliser pour initialiser les composants à héberger (racine par exemple) :

class_name ComponentCommon

static func init(p_component, p_host_path = NodePath()):

    assert(p_component is Node)

    # Try to assign
    if not p_host_path.is_empty():
        p_component.host = p_component.get_node(p_host_path)

    elif is_instance_valid(p_component.owner):
        p_component.host = p_component.owner

    elif is_instance_valid(p_component.get_parent()):
        p_component.host = p_component.get_parent()

    # Check
    if not is_instance_valid(p_component.host):
        push_warning(p_component.name.capitalize() + ": couldn't find a host, disabling.")
        p_component.enabled = false
    else:
        p_component.emit_signal('host_assigned')

Voici à quoi ressemble un composant (trait) une fois généré avec le premier script :

class_name ComponentNode2D extends Node2D

signal host_assigned(node)

export(bool) var enabled = true

export(NodePath) var host_path
var host

func _ready():
    ComponentCommon.init(self, host_path)

(Facultatif) 3. Étendre le composant (trait)

@vnen https://github.com/godotengine/godot/issues/23101#issuecomment -471816901

C'est mon idée. Je pense que c'est assez simple et couvre à peu près tous les cas d'utilisation pour la réutilisation du code. J'autoriserais même la classe utilisant le trait à remplacer les méthodes de trait (si c'est possible de le faire au moment de la compilation). Les traits pourraient également étendre d'autres traits.

Marcher dans tes pas... 😅

class_name ComponentMotion2D extends ComponentNode2D

const MAX_SPEED = 100.0

var linear_velocity = Vector2()
var collision

export(Script) var impl
...

En fait, les Script exportés sont utilisés dans ces composants pour piloter le comportement de types de nœuds hôtes/racines spécifiques par composant. Ici, ComponentMotion2D aura principalement deux scripts :

  • motion_kinematic_body_2d.gd
  • motion_rigid_body_2d.gd

Ainsi, les enfants conduisent toujours le comportement host / root ici. La terminologie host vient du fait que j'utilise des machines à états, et c'est là que les traits ne seraient peut-être pas parfaitement adaptés, car les états sont mieux organisés en nœuds imo.

Les composants eux-mêmes sont "câblés" à la racine en en faisant des membres onready , ce qui réduit efficacement le code passe-partout (au prix d'avoir à les référencer en tant que object.motion )

extends KinematicBody2D

onready var motion = $motion # ComponentMotion2D

Je ne sais pas si cela aiderait à résoudre le problème, mais C # a une chose appelée méthodes d'extension qui étendent les fonctionnalités d'un type de classe.

Fondamentalement, la fonction doit être statique et le premier paramètre est obligatoire et doit être self . Cela ressemblerait à ceci comme définition:

extension.gd

# any script that uses this method must be an instance of `Node2D`
static func distance(self source: Node2D, target: Node2D):
    return source.global_position.distance_to(target.global_position)

# any script that uses this method must be an instance of `Rigidbody2D`
# a `Sprite` instance cannot use this method
static func distance(self source: Rigidbody2D, target: Node2D):
    return source.global_position.distance_to(target.global_position)

Ensuite, lorsque vous souhaitez utiliser la méthode distance , faites simplement ceci :

joueur.gd

func _ready() -> void:
    print(self.distance($Enemy))
    print($BulletPoint.distance($Enemy))

Je le connais, mais cela n'aide pas à résoudre le problème. Héhé, merci quand même.

Des méthodes d'extension @TheColorRed ont déjà été proposées, mais je ne pense pas qu'elles soient réalisables dans un langage dynamique. Je ne pense pas non plus qu'ils résolvent le problème fondamental qui a initialement lancé cette discussion.


Sur une autre note, j'ouvrirai probablement de nombreuses propositions pour GDScript en tant que GIP (ceci inclus, si @willnationsdev n'y voit pas d'inconvénient).

Je crois toujours que les traits ont le plus de sens pour partager du code horizontalement dans un langage OO (sans héritage multiple, mais je ne veux pas aller dans cette direction).

Je ne pense pas qu'ils soient réalisables dans un langage dynamique

Le GDS est-il dynamique ? Les méthodes d'extension pourraient être limitées aux seules instances typées et fonctionneraient exactement de la même manière que dans d'autres langages - juste un sucre syntaxique lors de la compilation remplaçant l'appel de méthode par un appel de méthode statique (fonction). Honnêtement, je préférerais les proxénètes (méthodes externes) avant les prototypes à la manière de JS ou d'autres moyens dynamiques d'attacher des méthodes à des classes ou même à des instances.

Quoi que nous décidions de faire, j'espère que nous ne déciderons pas de l'appeler "proxénètes".

Le GDS est-il dynamique ?

Il existe de nombreuses définitions de "dynamique" dans ce contexte. Pour être clair : le type de variable peut ne pas être connu au moment de la compilation, de sorte que la vérification de l'extension de la méthode doit être effectuée au moment de l'exécution (ce qui nuira aux performances d'une manière ou d'une autre).

Les méthodes d'extension pourraient être limitées aux seules instances typées

Si nous commençons à faire cela, nous pourrions tout aussi bien faire en sorte que GDScript soit uniquement typé. Mais c'est une toute autre discussion que je ne veux pas aborder ici.

Le fait est que les choses ne devraient pas démarrer ou s'arrêter de fonctionner parce que l'utilisateur a ajouté des types à un script. C'est presque toujours déroutant quand ça arrive.

Encore une fois, je ne pense pas que cela résolve le problème de toute façon. Nous essayons d'attacher le même code à plusieurs types, alors qu'une méthode d'extension ne l'ajoutera qu'à un seul type.

Honnêtement, je préférerais les proxénètes (méthodes externes) avant les prototypes à la manière de JS ou d'autres moyens dynamiques d'attacher des méthodes à des classes ou même à des instances.

Personne n'a proposé (encore) d'attacher dynamiquement des méthodes (à l'exécution) et je ne le veux pas non plus. Les traits seraient appliqués statiquement au moment de la compilation.

J'ai initialement fait un commentaire sur Haxe et sa bibliothèque de macros mixin, mais j'ai ensuite réalisé que la plupart des utilisateurs n'utiliseront de toute façon pas de langage tiers.

J'ai récemment rencontré un besoin pour cela.

J'ai des objets avec lesquels l'utilisateur peut interagir mais ne peut pas partager le même parent, mais ils ont besoin de groupes d'API similaires

par exemple, j'ai des classes qui ne peuvent pas hériter du même parent, mais qui utilisent un ensemble similaire d'API :
Entrepôt : Finances, Suppression, MouseInteraction + autres
Véhicule : Finances, Suppression, MouseInteraction + autres
VehicleTerminal : Finances, Suppression, MouseInteraction + autres

Pour les finances, j'ai utilisé la composition, car cela nécessite le moins de code passe-partout car get_finances_component() est une API suffisante car elle ne se soucie pas vraiment de l'objet du jeu.

Les autres:
MouseInteraction and Delection J'ai juste dû copier et coller car il a besoin de connaître les objets du jeu, certaines compositions ne fonctionnent pas ici à moins que j'aie fait une délégation étrange:

Warehouse:
  func delete():
      get_delete_component().delete(self);

mais cela ne me permet pas vraiment de remplacer le fonctionnement de la suppression où, s'il s'agissait d'une classe héritée, je pourrais avoir la possibilité de réécrire une partie du code de suppression si nécessaire.

MouseInteraction and Delection J'ai juste dû copier et coller car il a besoin de connaître les objets du jeu, certaines compositions ne fonctionnent pas ici à moins que j'aie fait une délégation étrange

J'accède actuellement aux composants via des nœuds onready . Je fais quelque chose de similaire:

# character.gd

var input = $input # input component

func _set(property, value):
    if property == "focused": # override
        input.enabled = value
    return true

Donc ça:

character.input.enabled = true

devient ceci :

character.focused = true

Comme @Calinou l' a gentiment souligné, mon problème https://github.com/godotengine/godot-proposals/issues/758 est étroitement lié à cela. Que pensez-vous de la proposition de pouvoir ajouter un trait à un groupe ? Cela pourrait considérablement nécessiter des scripts et d'autres frais généraux.

Ce serait bien d'avoir un moyen d'injecter du code partageable dans les classes, et s'ils ont exporté des valeurs, les faire apparaître dans l'inspecteur, et avoir les méthodes et propriétés disponibles et détectées par l'achèvement du code.

Les propositions de fonctionnalités et d'améliorations pour le moteur Godot sont actuellement discutées et examinées dans un outil de suivi des problèmes dédié aux propositions d'amélioration de Godot (GIP) ( godotengine/godot-proposals ). Le tracker GIP a un modèle de problème détaillé conçu pour que les propositions incluent toutes les informations pertinentes pour démarrer une discussion productive et aider la communauté à évaluer la validité de la proposition pour le moteur.

Le traqueur principal ( godotengine/godot ) est désormais uniquement dédié aux rapports de bogues et aux demandes d'extraction, permettant aux contributeurs de mieux se concentrer sur le travail de correction des bogues. Par conséquent, nous fermons maintenant toutes les propositions de fonctionnalités plus anciennes sur le suivi des problèmes principaux.

Si vous êtes intéressé par cette proposition de fonctionnalité, veuillez ouvrir une nouvelle proposition sur le tracker GIP en suivant le modèle de problème donné (après avoir vérifié qu'il n'existe pas déjà). Assurez-vous de faire référence à cette question fermée si elle comprend une discussion pertinente (que vous êtes également encouragé à résumer dans la nouvelle proposition). Merci d'avance!

Remarque : il s'agit d'une proposition populaire, si quelqu'un la déplace vers les propositions Godot, veuillez également essayer de résumer la discussion.

Cette page vous a été utile?
0 / 5 - 0 notes