Godot: Fügen Sie ein GDScript-Merkmalsystem hinzu.

Erstellt am 18. Okt. 2018  ·  93Kommentare  ·  Quelle: godotengine/godot

(Bearbeiten:
So minimieren Sie weitere XY-Probleme:
Das hier angesprochene Problem ist, dass Godots Node-Scene-System / Skriptsprachen noch nicht die Erstellung wiederverwendbarer, gruppierter Implementierungen unterstützen, die 1) spezifisch für die Funktionen des Root-Knotens sind und 2) die ausgetauscht und/oder kombiniert werden können. Skripte mit statischen Methoden oder Unterknoten mit Skripten könnten für das letztere Bit verwendet werden, und in vielen Fällen funktioniert dies. Godot zieht es jedoch im Allgemeinen vor, dass Sie die Logik für das Gesamtverhalten Ihrer Szene im Root-Knoten speichern, während sie Daten verwendet, die von den untergeordneten Knoten berechnet wurden, oder erheblich abweichende Unteraufgaben an sie delegiert, z. B. verwaltet ein KinematicBody2D keine Animationen delegiert das an einen AnimationPlayer.

Einen dünneren Wurzelknoten zu haben, der "Komponenten"-Knoten verwendet, um sein Verhalten zu steuern, ist im Vergleich dazu ein schwaches System. Einen weitgehend leeren Wurzelknoten zu haben, der sein gesamtes Verhalten nur an untergeordnete Knoten delegiert, widerspricht diesem Paradigma. Die untergeordneten Knoten werden zu Verhaltenserweiterungen des Wurzelknotens und nicht zu autarken Objekten, die eigenständig eine Aufgabe erfüllen. Es ist sehr klobig, und das Design könnte vereinfacht/verbessert werden, indem wir die gesamte Logik im Stammknoten konsolidieren, aber auch die Logik in verschiedene, zusammensetzbare Teile aufteilen können.

Ich nehme an, das Thema dieser Ausgabe dreht sich mehr um den Umgang mit dem oben genannten Problem als um GDScript, aber ich glaube, dass GDScript Traits der einfachste und direkteste Ansatz zur Lösung des Problems wäre.
)

Für die Uninformierten sind Merkmale im Wesentlichen eine Möglichkeit, zwei Klassen zu einer zu verschmelzen (so ziemlich ein Kopier-/Einfügemechanismus), nur dass Sie, anstatt den Text der Dateien buchstäblich zu kopieren/einzufügen, lediglich eine Schlüsselwortanweisung verwenden die beiden Dateien verknüpfen. (Bearbeiten: Der Trick ist, dass ein Skript zwar nur eine Klasse erben kann, aber mehrere Merkmale enthalten kann.)

Ich stelle mir etwas vor, bei dem jede GDScript-Datei als Trait für eine andere GDScript-Datei verwendet werden kann, solange der Trait-Typ eine Klasse erweitert, die vom zusammengeführten Skript geerbt wurde, dh ein Sprite-erweiterndes GDScript kann kein Ressourcen-GDScript verwenden als Merkmal, aber es kann ein Node2D GDScript verwenden. Ich würde mir eine ähnliche Syntax vorstellen:

# 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")

Dafür sehe ich zwei Möglichkeiten:

  1. Analysieren Sie das Skript vorab über RegEx für „^trait \Nachladevorgang). Wir müssten die Trait-Verschachtelung nicht unterstützen oder den generierten Quellcode nach jeder Iteration kontinuierlich erneut untersuchen, um zu sehen, ob weitere Trait-Einfügungen vorgenommen wurden.
  2. Analysieren Sie das Skript normal, aber bringen Sie dem Parser bei, das Schlüsselwort zu erkennen, das Skript zu laden, auf das verwiesen wird, DIESES Skript zu parsen und dann den Inhalt seines ClassNode an den generierten ClassNode des aktuellen Skripts anzuhängen (wodurch effektiv die analysierten Ergebnisse eines Skripts genommen und hinzugefügt werden zu den geparsten Ergebnissen des anderen Skripts). Dies würde automatisch das Verschachteln von Trait-Typen unterstützen.

Auf der anderen Seite möchten die Leute vielleicht, dass das Merkmal GDScript einen Namen hat, aber vielleicht NICHT wollen, dass der class_name dieses GDScripts im CreateDialog erscheint (weil es nicht dazu gedacht ist, selbst erstellt zu werden). In diesem Fall ist es möglicherweise KEINE gute Idee, dies von einem Skript unterstützen zu lassen; nur diejenigen, die besonders gekennzeichnet sind (vielleicht durch das Schreiben von 'trait' am Anfang der Datei?). Jedenfalls Stoff zum Nachdenken.

Die Gedanken?

Bearbeiten: Nach einigem Nachdenken glaube ich, dass Option 2 viel besser wäre, da 1) wir wüssten, WELCHES Skript ein Skriptsegment stammt (für eine bessere Fehlerberichterstattung) und 2) wir in der Lage wären, Fehler zu identifizieren, wenn sie seitdem auftreten Eingeschlossene Skripte müssen der Reihe nach geparst werden, anstatt nur am Ende alles zu parsen. Dies würde die Verarbeitungszeit verkürzen, die es zum Parsing-Prozess hinzufügt.

archived discussion feature proposal gdscript

Hilfreichster Kommentar

@aaronfranke Traits , im Grunde dasselbe wie Mixins , haben einen völlig anderen Anwendungsfall als Schnittstellen, gerade weil sie Implementierungen der Methoden enthalten. Wenn eine Schnittstelle eine Standardimplementierung hätte, wäre sie eigentlich keine Schnittstelle mehr.

Traits/Mixins sind in PHP, Ruby, D, Rust, Haxe, Scala und vielen anderen Sprachen (wie in den verlinkten Wikis beschrieben) vorhanden, daher sollten sie bereits mit Personen vertraut sein, die über ein breites Repertoire an Programmiersprachenkenntnissen verfügen.

Wenn wir Schnittstellen implementieren würden (was ich auch nicht ablehne, insbesondere wenn die optionale statische Typisierung kommt), wäre dies effektiv nur eine Möglichkeit, Funktionssignaturen anzugeben und dann zu verlangen, dass die relevanten GDScript-Skripte diese Funktionssignaturen mit Merkmalen implementieren enthalten (falls diese zu diesem Zeitpunkt existierten).

Alle 93 Kommentare

Was ist der Vorteil statt: extends "res://move_right_trait.gd"

@MrJustreborn Weil Sie mehrere Eigenschaften in einer Klasse haben können, aber Sie können nur ein Skript erben.

Wenn ich das richtig verstehe, ist das im Grunde das, was C# " Schnittstellen " nennt, aber mit nicht abstrakten Methoden? Es könnte besser sein, die Feature-Interfaces statt Traits zu nennen, um Programmierern vertraut zu sein.

@aaronfranke Traits , im Grunde dasselbe wie Mixins , haben einen völlig anderen Anwendungsfall als Schnittstellen, gerade weil sie Implementierungen der Methoden enthalten. Wenn eine Schnittstelle eine Standardimplementierung hätte, wäre sie eigentlich keine Schnittstelle mehr.

Traits/Mixins sind in PHP, Ruby, D, Rust, Haxe, Scala und vielen anderen Sprachen (wie in den verlinkten Wikis beschrieben) vorhanden, daher sollten sie bereits mit Personen vertraut sein, die über ein breites Repertoire an Programmiersprachenkenntnissen verfügen.

Wenn wir Schnittstellen implementieren würden (was ich auch nicht ablehne, insbesondere wenn die optionale statische Typisierung kommt), wäre dies effektiv nur eine Möglichkeit, Funktionssignaturen anzugeben und dann zu verlangen, dass die relevanten GDScript-Skripte diese Funktionssignaturen mit Merkmalen implementieren enthalten (falls diese zu diesem Zeitpunkt existierten).

Vielleicht ein Schlüsselwort wie includes ?

extends Node2D
includes TraitClass

Obwohl andere Namen wie Trait, Mixin, Has usw. sicherlich auch in Ordnung sind.

Ich mag auch die Idee, eine Option zum Ausschließen des Typs class_name aus dem Hinzufügen-Menü zu haben. Es kann mit kleinen Typen, die nicht eigenständig als Knoten funktionieren, sehr unübersichtlich werden.

Es kann sogar nur ein eigenes Feature-Thema sein.

(Meinen Kommentar versehentlich gelöscht, hoppla! Außerdem das obligatorische "Warum erlaubst du nicht einfach mehrere Skripte, Unity macht das?" )

Wie wird das in VisualScript funktionieren, wenn überhaupt?

Könnte es auch von Vorteil sein, ein Inspektor-Interface für Traits einzufügen, wenn Traits implementiert würden? Ich kann mir vorstellen, dass einige Anwendungsfälle für Merkmale Anwendungsfälle beinhalten, in denen es nur Merkmale und kein Skript gibt (zumindest kein Skript außer einem, das die Merkmalsdateien enthält). Wenn ich jedoch genauer darüber nachdenke, frage ich mich, ob sich der Aufwand für die Erstellung einer solchen Schnittstelle überhaupt lohnt, verglichen mit der Erstellung eines Skripts, das die Trait-Dateien enthält.

@LikeLakers2

Wie wird das in VisualScript funktionieren, wenn überhaupt?

Wenn es so gemacht wird, wie ich vorgeschlagen habe, würde es für VisualScript überhaupt nicht passieren. Nur GDScript. Jedes für VisualScript implementierte Trait-System würde völlig anders gestaltet, da VisualScript keine geparste Sprache ist. Schließt die Möglichkeit jedoch überhaupt nicht aus (müßte nur anders implementiert werden). Außerdem sollten wir vielleicht zuerst in Betracht ziehen, VisualScript-Vererbungsunterstützung zu erhalten? lol

Könnte es auch von Vorteil sein, ein Inspektor-Interface für Traits einzufügen, wenn Traits implementiert würden?

Es hätte nicht viel Sinn. Die Merkmale verleihen dem GDScript einfach Details, indem sie ihm die Eigenschaften, Konstanten, Signale und Methoden übergeben, die durch das Merkmal definiert sind.

Ich kann mir vorstellen, dass einige Anwendungsfälle für Merkmale Anwendungsfälle beinhalten, in denen es nur Merkmale und kein Skript gibt (zumindest kein Skript außer einem, das die Merkmalsdateien enthält).

Traits, wie sie in anderen Sprachen dargestellt werden, sind niemals isoliert verwendbar, sondern müssen in einem anderen Skript enthalten sein, um verwendbar zu sein.

Ich frage mich, ob sich der Aufwand für die Erstellung einer solchen Schnittstelle überhaupt lohnt

Das Erstellen einer Inspector-Schnittstelle auf irgendeine Weise würde für GDScript allein nicht wirklich viel Sinn machen. Das Hinzufügen oder Entfernen einer Eigenschaft würde das direkte Bearbeiten des Quellcodes der Eigenschaft source_code der Skriptressource beinhalten, dh es ist keine Eigenschaft des Skripts selbst. Also entweder...

  1. dazu müsste dem Redakteur beigebracht werden, wie er speziell mit der korrekten Bearbeitung des Quellcodes für GDScript-Dateien umgeht (fehleranfällig), oder...
  2. Alle Skripte müssten Eigenschaften unterstützen, damit die GDScriptLanguage ihren eigenen internen Prozess zum Hinzufügen und Entfernen von Eigenschaften bereitstellen kann (aber nicht alle Sprachen unterstützen Eigenschaften, daher wäre die Eigenschaft nicht in allen Fällen sinnvoll).

Was ist die Notwendigkeit für eine solche Funktion? Gibt es irgendetwas, was Sie jetzt nicht tun können? Oder macht es einige Aufgaben deutlich schneller zu erledigen?

Ich würde GDscript lieber in einer einfachen Sprache belassen, als komplexe Funktionen hinzuzufügen, die fast nie verwendet werden.

Es löst das Problem Child-Nodes-As-Script-Dependencies, mit dem dieser Typ ein Problem hatte , aber es hat nicht die gleiche Art von Ballast wie MultiScript, weil es auf eine einzige Sprache beschränkt ist. Das GDScript-Modul kann die Logik darüber isolieren, wie die Merkmale miteinander und mit dem Hauptskript zusammenhängen, während das Auflösen der Unterschiede zwischen verschiedenen Sprachen viel komplizierter wäre.

Da es keine mehrfachen Importe/mehrere Vererbung gibt, sind Kind-Knoten-als-Skript-Abhängigkeiten die einzige Möglichkeit, um zu vermeiden, dass der Code VIEL wiederholt wird, und dies würde das Problem definitiv auf nette Weise lösen.

@groud @Zireael07 Ich meine, der radikalere, sprachübergreifende Ansatz wäre, 1) Object vollständig neu zu gestalten, um einen ScriptStack zu verwenden, um gestapelte Skripte zu einer einzigen Skriptdarstellung zusammenzuführen, 2) MultiScript erneut einzuführen und Editorunterstützung zu erstellen, die automatisch konvertiert das Hinzufügen von Skripten zu Multiskripten (oder der Einfachheit halber einfach alle Skripte zu Multiskripten zu machen, in diesem Fall wäre die MultiScript-Implementierung im Wesentlichen unser ScriptStack), oder 3) eine Art sprachübergreifendes Trait-System für den Object-Typ implementieren, der zusammengeführt werden kann Verweiserweiternde Skripte als Merkmale, die ihren Inhalt wie ein typisches Skript einbeziehen. Alle diese Optionen sind jedoch viel invasiver für den Motor. Das hält alles einfacher.

Ich glaube nicht, dass Eigenschaft notwendig ist. Was wir am meisten brauchen, ist ein schneller Motorfreigabezyklus. Ich meine, wir müssen die Engine flexibler machen, um neue Funktionen hinzuzufügen, indem Sie einfach neue DLL- oder so-Dateien hinzufügen und die Engine automatisch in sich selbst integriert, wie der Plugin-Stil in den meisten IDEs. Zum Beispiel brauche ich wirklich dringend, dass Websocket funktioniert, ich muss nicht warten, bis 3.1 veröffentlicht wird. 3.1 ist im Moment zu kaputt mit so vielen Fehlern. Es wird großartig sein, wenn wir diese Funktion haben. Eine neue Klasse kann automatisch in GDScript aus zufälligen .dll- oder .so-Dateien in einem bestimmten Pfad eingefügt werden. Ich weiß nicht, wie viel Aufwand das in C++ macht, aber ich hoffe, das ist nicht zu schwer 😁

@fian46 Nun, wenn jemand Websockets als herunterladbares GDNative-Plugin implementiert hätte, dann ja, das, was Sie beschrieben haben , wäre der Workflow. Stattdessen entschieden sie sich dafür, es als integrierte Funktion in der Vanilla-Engine verfügbar zu machen. Nichts hindert Leute daran, Features auf diese Weise zu erstellen, also hat Ihr Punkt wirklich nichts mit dem Thema dieser Ausgabe zu tun.

Ups, ich weiß nicht, ob es GDNative gibt 😂😂😂. Trait ist großartig, aber ist es einfacher, eine gefälschte Trait-Klasse zu erstellen und zu instanziieren und später die Funktion wie ein einfaches Skript aufzurufen?

Wenn ein Godot-Skript eine unbenannte Klasse ist, warum nicht "move_right_trait.gd" in "my_sprite.gd" instanziieren?
Entschuldigung für meine Unwissenheit, wenn ich das Problem nicht verstehe.

Ich verstehe die Verwendung von Traits in stärker typisierten Sprachen wie Rust oder (Schnittstellen in) C++, aber in einer ducked typed Language ist das nicht ein bisschen unnötig? Durch einfaches Implementieren der gleichen Funktionen können Sie eine einheitliche Schnittstelle zwischen Ihren Typen erreichen. Ich glaube, ich bin mir etwas unsicher, was das genaue Problem mit der Art und Weise ist, wie GDScript mit Schnittstellen umgeht oder wie ein Trait-System überhaupt wirklich helfen würde.

Könnten Sie nicht auch preload ("Some-other-behavior.gd") verwenden und die Ergebnisse in einer Variablen speichern, um im Grunde den gleichen Effekt zu erzielen?

@fian46 @DriNeo Nun, ja und nein. Das Laden von Skripten und die Verwendung von Skriptklassen erledigen das bereits, aber das Problem geht darüber hinaus.

@TheYokai

Die Implementierung derselben Funktionen sollte es Ihnen ermöglichen, eine einheitliche Schnittstelle zwischen Ihren Typen zu erreichen

Das Problem besteht nicht darin, eine einheitliche Schnittstelle zu erreichen, mit der Sie recht haben, Enteneingabe löst sich gut, sondern Gruppen verwandter Implementierungen effizient zu organisieren (kombinieren/austauschen).


In 3.1 können Sie mit Skriptklassen statische Funktionen in einem Referenzskript (oder wirklich jedem Typ) definieren und dieses Skript dann als Namensraum verwenden, um global auf diese Funktionen in GDScript zuzugreifen.

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!")

Wenn es jedoch um nicht statischen Inhalt geht, insbesondere um Inhalte, die knotenspezifische Funktionen verwenden, ist es schwierig, die eigene Logik aufzuteilen.

Was ist zum Beispiel, wenn ich einen KinematicBody2D habe und ein „Sprung“- und ein „Lauf“-Verhalten haben möchte? Jedes dieser Verhalten würde Zugriff auf die Eingabeverarbeitung und move_and_slide -Funktionen von KinematicBody2D benötigen. Im Idealfall wäre ich in der Lage, die Implementierung jedes Verhaltens unabhängig voneinander auszutauschen und den gesamten Code für jedes Verhalten in separaten Skripten aufzubewahren.

Aktuell sind alle mir bekannten Workflows dafür einfach nicht optimal.

  1. Wenn Sie alle Implementierungen im selben Skript behalten und nur die verwendeten Funktionen austauschen ...

    1. Das Ändern von "Verhalten" kann das Austauschen mehrerer Funktionen als Satz beinhalten, sodass Sie die Implementierungsänderungen nicht effektiv gruppieren können.

    2. Alle Funktionen für jedes Verhalten (X * Y) befinden sich in Ihrem einzigen Skript, sodass es sehr schnell aufgebläht werden kann.

  2. Sie können einfach das gesamte Skript ersetzen, aber das bedeutet dann, dass Sie ein neues Skript für jede Kombination von Verhaltensweisen und jede Logik erstellen müssen, die diese Verhaltensweisen verwendet.
  3. Wenn Sie untergeordnete Knoten als Skriptabhängigkeiten verwenden, bedeutet dies, dass Sie diese seltsamen "Komponenten" -Node2D-Knoten haben, die sich ihre übergeordneten Knoten schnappen und die Methode move_and_slide für sie aufrufen, was relativ unnatürlich ist.

    • Sie müssen davon ausgehen, dass Ihr Elternteil diese Methode implementieren wird, oder Logik anwenden, um zu überprüfen, ob er die Methode hat. Und wenn Sie eine Überprüfung durchführen, können Sie entweder unbemerkt fehlschlagen und möglicherweise einen stillen Fehler in Ihrem Spiel haben, oder Sie können es unnötigerweise in ein Tool-Skript umwandeln, nur damit Sie eine Konfigurationswarnung auf dem Knoten setzen können, um Sie im Editor visuell darauf hinzuweisen dass es ein Problem gibt.

    • Sie erhalten auch keine ordnungsgemäße Codevervollständigung für die beabsichtigten Operationen der Knoten, da sie von Node2D abgeleitet sind und der springende Punkt darin besteht, das Verhalten eines KinematicBody2D-Elternteils zu steuern.

Nun, ich gebe zu, dass Option 3 derzeit der effektivste Workflow ist und dass seine Probleme größtenteils durch die Verwendung der handlichen statischen Typisierung für GDScript in 3.1 gelöst werden können. Es gibt jedoch eine grundlegendere Frage im Spiel.

Das Node-Scene-System von Godot hat im Allgemeinen die Form von Benutzern, die Nodes oder Szenen erstellen, die eine bestimmte Aufgabe in ihrem eigenen geschlossenen System erfüllen. Sie können diese Knoten/Szenen in einer anderen Szene instanziieren und sie Daten berechnen lassen, die dann von der übergeordneten Szene verwendet werden (wie dies bei der Area2D- und CollisionShape2D-Beziehung der Fall ist).

Die Verwendung der Vanilla-Engine und allgemeine Best Practices empfehlen jedoch, das Verhalten Ihrer Szene an den Root-Knoten und/oder sein Skript gebunden zu halten. Sie haben kaum jemals "Verhaltenskomponenten"-Knoten, die der Wurzel tatsächlich sagen, was sie tun soll (und wenn sie da ist, fühlt sie sich sehr klobig an). Die AnimationPlayer/Tween-Knoten sind die einzigen bestreitbaren Ausnahmen, die mir einfallen, aber selbst ihre Operationen werden von der Wurzel geleitet (es delegiert effektiv die Kontrolle vorübergehend an sie). (Bearbeiten: Auch in diesem Fall sind Animation und Tweening nicht die Aufgabe von KinematicBody2D, daher ist es sinnvoll, diese Aufgaben zu delegieren. Bewegung, wie Laufen und Springen, liegt jedoch in seiner Verantwortung.) Es ist einfacher und natürlicher zuzulassen eine Trait-Implementierung, um den Code zu organisieren, da sie die Beziehungen zwischen den Knoten streng Daten-oben/Verhalten-unten hält und den Code isolierter in seinen eigenen Skriptdateien hält.

Eh, sich selbst als 'Implementieren einer Schnittstelle/Eigenschaft' zu markieren, sollte jedoch auch einen * is * -Test bestehen, was praktisch ist, um die Funktionalität von etwas zu testen.

@OvermindDL1 Ich meine, ich habe ein Beispiel für einen solchen Test gegeben, aber ich habe stattdessen in verwendet, da ich zwischen Vererbung und Verwendung von Merkmalen unterscheiden wollte.

Ich schätze, ich bin hier irgendwie auf ein XY-Problem gestoßen, mein Fehler. Ich kam gerade von 2 anderen Ausgaben (Nr. 23052, Nr. 15996), die dieses Thema auf die eine oder andere Weise behandelten, und dachte, ich würde einen Vorschlag einreichen, aber ich habe nicht wirklich den gesamten Kontext angegeben.

@groud Diese Lösung wird eines der Probleme lösen, die gegen # 19486 aufgeworfen wurden.

@willnationsdev tolle Idee, ich freue mich drauf!

Nach meinem begrenzten Verständnis möchte dieses Trait-System etwas Ähnliches wie den in diesem Video gezeigten Workflow ermöglichen: https://www.youtube.com/watch?v=raQ3iHhE_Kk
(Berücksichtigen Sie, ich spreche von dem gezeigten _Workflow_, nicht von der verwendeten Funktion.)

Im Video wird es mit anderen Arten von Workflows verglichen, mit ihren Vor- und Nachteilen.

Zumindest meines Wissens nach ist diese Art von Workflow in GDScript derzeit aufgrund der Funktionsweise der Vererbung nicht möglich.

@AfterRebelion Die ersten Minuten dieses Videos, in denen er die Modularität, Bearbeitbarkeit und Debuggbarkeit der Codebasis (und die zugehörigen Details dieser Attribute) isoliert, sind in der Tat das Streben nach dieser Funktion.

Zumindest meines Wissens nach ist diese Art von Workflow in GDScript derzeit aufgrund der Funktionsweise der Vererbung nicht möglich.

Dieses Bit ist nicht ganz richtig, weil Godot dies in Bezug auf Knotenhierarchien und das Entwerfen von Szenen tatsächlich sehr gut macht. Szenen sind von Natur aus modular, Eigenschaften können direkt aus dem Editor exportiert (und sogar animiert) werden, ohne sich jemals mit Code befassen zu müssen, und alles kann isoliert getestet/debuggt werden, da Szenen unabhängig ausgeführt werden können.

Die Schwierigkeit besteht darin, dass die Logik, die normalerweise an einen untergeordneten Knoten ausgelagert wird, vom Wurzelknoten ausgeführt werden muss, da die Logik auf den geerbten Merkmalen des Wurzelknotens beruht. In diesen Fällen besteht die einzige Möglichkeit, Komposition zu verwenden, darin, dass die Kinder anfangen, den Eltern zu sagen, was sie tun sollen, anstatt sie sich um ihre eigenen Angelegenheiten kümmern zu müssen, während die Eltern sie verwenden.

Dies ist in Unity kein Problem, da GameObject keine echte Vererbung hat, die Benutzer nutzen können. In Unreal könnte es ein kleines (?) Problem sein, da sie ähnliche knoten-/komponentenbasierte interne Hierarchien für Akteure haben.

Okay, lass uns hier ein bisschen Devil's Advocate spielen ( @MysteryGM , das könnte dir Spaß machen). Ich habe einige Zeit damit verbracht, darüber nachzudenken, wie ich ein solches System in Unreal schreiben könnte, und das gibt mir eine neue Perspektive darauf. Entschuldigung für die Leute, die das für eine gute Idee hielten / sich darüber aufgeregt haben:

Die Einführung eines Trait-Systems fügt GDScript als Sprache eine Ebene der Komplexität hinzu, die die Pflege erschweren kann.

Darüber hinaus erschweren Merkmale als Merkmal die Isolierung, woher Variablen, Konstanten, Signale, Methoden und sogar Unterklassen tatsächlich kommen. Wenn das Skript Ihres Knotens plötzlich 12 verschiedene Merkmale hat, wissen Sie nicht unbedingt, woher alles kommt. Wenn Sie einen Verweis auf etwas sehen, müssten Sie 12 verschiedene Dateien durchsuchen, um überhaupt zu wissen, wo sich ein Ding in der Codebasis befindet.

Dies beeinträchtigt die Debuggbarkeit von GDScript als Sprache, da Sie bei einem bestimmten Problem möglicherweise durchschnittlich 2 oder 3 verschiedene Stellen in der Codebasis auseinandernehmen müssen. Wenn einer dieser Orte schwer zu finden ist, weil er sagt , dass er sich in einem Skript befindet, sich aber tatsächlich woanders befindet - und wenn die Lesbarkeit des Codes nicht eindeutig angibt, welches Ding für die Daten / Logik verantwortlich ist - dann diese 2 oder 3 Schritte werden zu einer beliebig großen, höchst belastenden Schrittfolge multipliziert.

Die zunehmende Größe und der Umfang eines Projekts verstärken diese negativen Auswirkungen noch weiter und machen die Verwendung von Merkmalen zu einer ziemlich unhaltbaren Eigenschaft.


Aber was kann getan werden, um das Problem zu lösen? Wir wollen keine untergeordneten "Komponentenlogik"-Knoten, die den Szenenwurzeln sagen, was zu tun ist, aber wir können uns auch nicht auf die Vererbung verlassen oder ganze Skripte ändern, um unser Problem zu lösen.

Nun, was würde eine Nicht-ECS-Engine in dieser Situation tun? Die Zusammensetzung ist immer noch die Antwort, aber in diesem Fall ist ein vollständiger Knoten nicht sinnvoll, wenn er skaliert wird / erschwert die Dynamik der Eigentümerhierarchie. Stattdessen kann man einfach Nicht-Knoten-Implementierungsobjekte definieren, die die konkrete Implementierung eines Verhaltens abstrahieren, die aber alle immer noch im Besitz des Wurzelknotens sind. Dies kann mit einem Reference -Skript erfolgen.

# 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(...)

Wenn die Exporte so funktionieren würden, dass wir sie im Inspektor als Aufzählungen für Klassen bearbeiten könnten, die einen bestimmten Typ ableiten, wie wir es für neue Ressourceninstanzen können, dann wäre das cool. Die einzige Möglichkeit, dies jetzt zu tun, besteht darin, Ressourcenskriptexporte zu reparieren und dann Ihr Implementierungsskript dazu zu bringen, Ressource zu erweitern (aus keinem anderen Grund). Es wäre jedoch eine gute Idee, Ressourcen erweitern zu lassen, wenn die Implementierung selbst Parameter erfordert, die Sie im Inspektor definieren möchten. :-)

Nun, was DIES einfacher machen würde, wäre ein Snippet- oder Makrosystem, damit die Erstellung dieser wiederverwendeten deklarativen Codeabschnitte für Entwickler einfacher ist.

Wie auch immer, ja, ich glaube, ich habe offensichtliche Probleme mit einem Eigenschaftssystem und einem besseren Ansatz zur Lösung des Problems identifiziert. Hurra für XY-Probleme! /S

Bearbeiten:

Der Workflow des obigen Beispiels würde also beinhalten, das Implementierungsskript festzulegen und dann die Instanz des Skripts zur Laufzeit zu verwenden, um das Verhalten zu definieren. Was aber, wenn die Implementierung selbst Parameter erfordert, die Sie statisch vom Inspector aus festlegen möchten? Hier ist stattdessen eine Version, die auf einer Ressource basiert.

# 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(...)

Verwandte: #22660

@AfterRebelion

Berücksichtigen Sie, dass ich über den gezeigten Workflow spreche, nicht über die verwendete Funktion

Es ist ironisch, dass ich, nachdem Sie dies klargestellt haben und ich dem optimalen Arbeitsablauf zustimme, und dann späteren Teilen des Kommentars nicht zustimme, im Anschluss daran sage, dass die „verwendete Funktion“ in diesem Video tatsächlich die ideale Vorgehensweise ist dieses Problem in Godot sowieso . Haha.

# 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, wusste nicht, dass der Export so mächtig ist. Erwartete, dass es nur mit Primitiven und anderen Datenstrukturen interagieren kann.

Damit ist mein vorheriger Kommentar hinfällig.
Wie Sie sagten, wenn eine Art Makro implementiert wird, um die Implementierung zu vereinfachen, wäre dies der beste Weg, um diesen Workflow ohne die Notwendigkeit von MultiScript zu implementieren. Vielleicht nicht so vielseitig wie Unity, da man immer noch jedes mögliche Skript vorher deklarieren müsste, aber immer noch eine gute Option.

@AfterRebelion

Wie Sie sagten, wenn eine Art Makro implementiert wird, um die Implementierung zu vereinfachen, wäre dies der beste Weg, um diesen Workflow ohne die Notwendigkeit von MultiScript zu implementieren.

Nun, der Resource basierende Ansatz, den ich im selben Kommentar erwähnt habe, kombiniert mit etwas besserer Editor-Unterstützung von #22660, würde die Qualität vergleichbar mit dem machen, was Unity leisten kann.

Vielleicht nicht so vielseitig wie Unity, da man immer noch jedes mögliche Skript vorher deklarieren müsste, aber immer noch eine gute Option.

Nun, wenn sie die Array-Typ-Hinweise in 3.2 beheben, können Sie ein exportiertes Array für Dateipfade definieren, die ein Skript erweitern und effektiv Ihr eigenes hinzufügen müssen. Dies könnte sogar über ein Plugin in 3.1 erfolgen, indem die Klasse EditorInspectorPlugin verwendet wird, um dem Inspektor benutzerdefinierte Inhalte für bestimmte Ressourcen oder Knoten hinzuzufügen.

Ich meine, wenn Sie ein "Unity"-ähnliches System wollten, dann WÜRDEN Sie wirklich Unterknoten haben, die der Wurzel sagen, was zu tun ist, und Sie müssten sie nur abrufen, indem Sie sich auf ihren Namen beziehen, ohne sie manuell zu deklarieren oder hinzuzufügen aus dem Skript des Root-Knotens. Die Ressourcenmethode ist im Allgemeinen viel effektiver und behält eine sauberere Codebasis bei.

Da das Trait-System die Benutzerfreundlichkeit von GDScript-Code übermäßig belasten würde, werde ich aus den von mir skizzierten Gründen fortfahren und dieses Problem schließen. Die Fallstricke seiner Hinzufügung überwiegen bei weitem alle vergleichsweise mageren Vorteile, die wir von einem solchen System erhalten könnten, und dieselben Vorteile können auf unterschiedliche Weise mit mehr Klarheit und Benutzerfreundlichkeit implementiert werden.

Nun, ich habe diese Diskussion verpasst, während ich weg war. Ich habe noch nicht alles gelesen, aber ich hatte die Idee, Traits zu GDScript hinzuzufügen, um das Problem der Wiederverwendung von Code zu lösen, was ich viel eleganter und klarer finde als die gefälschte Mehrfachvererbung, ein Skript an einen abgeleiteten Typ anzuhängen. Was ich jedoch tun würde, wäre, spezifische Trait-Dateien zu erstellen und nicht zuzulassen, dass eine Klassendatei ein Trait ist. Ich glaube nicht, dass es so schlimm wäre.

Aber ich bin offen für Vorschläge zur Lösung des Hauptproblems, nämlich der Wiederverwendung von Code in mehreren Knotentypen.

@vnen Die Lösung, die ich mir ausgedacht habe, ist das letzte Element, um wiederverwendbare Abschnitte in Ressourcenskripts auszulagern.

  • Sie können weiterhin angezeigt und ihre Eigenschaften im Inspektor bearbeitet werden, als wären sie Elementvariablen auf einem Knoten.

  • Sie bewahren eine klare Spur darüber, woher Daten und Logik stammen, etwas, das leicht gefährdet werden könnte, wenn viele Merkmale in einem Skript enthalten sind.

  • Sie fügen GDScript als Sprache keine übermäßige Komplexität hinzu. Wenn sie beispielsweise vorhanden wären, müssten wir klären, wie gemeinsam genutzte Eigenschaften (Eigenschaften, Konstanten, Signale) in das Hauptskript eingebunden werden (oder, falls nicht eingebunden, Benutzer zwingen, sich mit Abhängigkeitskonflikten zu befassen).

  • Ressourcenskripte sind besser, da sie vom Inspektor aus zugewiesen und geändert werden können. Designer, Autoren usw. können Implementierungsobjekte direkt aus dem Editor heraus ändern.

@willnationsdev Ich verstehe (obwohl der Name "Ressourcenskripte" seltsam klingt, da alle Skripte Ressourcen sind). Das Hauptproblem bei dieser Lösung ist, dass sie etwas nicht löst, was die Leute mit dem Vererbungsansatz erwarten: Hinzufügen exportierter Variablen und Signale zum Wurzelknoten der Szene (insbesondere wenn die Szene an anderer Stelle instanziiert wird). Sie können die exportierten Variablen aus den Unterressourcen immer noch bearbeiten, aber es wird weniger praktisch (Sie können nicht auf einen Blick erkennen, welche Eigenschaften Sie bearbeiten können).

Das andere Problem ist, dass der Boilerplate-Code häufig wiederholt werden muss. Sie müssen auch sicherstellen, dass die Ressourcenskripte tatsächlich gesetzt sind, bevor Sie die Funktionen aufrufen.

Der Vorteil ist, dass wir nichts tun müssen, es ist bereits verfügbar. Ich denke, dies sollte dokumentiert werden, und Personen, die die aktuelle Methode "Skript an abgeleiteten Knoten anhängen" verwenden, könnten die Lösung kommentieren, um zu sehen, wie praktikabel sie ist.

@vnen

aber es wird weniger praktisch (Sie können nicht auf einen Blick erkennen, welche Eigenschaften Sie bearbeiten können).

Können Sie hier erläutern, was Sie meinen? Ich sehe nicht, wie es zu einem wesentlichen Mangel an Klarheit darüber kommen könnte, auf welche Eigenschaften zugegriffen werden kann, insbesondere wenn so etwas wie #22660 zusammengeführt würde.

  1. Ich instanziere die Szene, möchte wissen, wie ich sie bearbeiten kann, und schaue mir das Skript des Wurzelknotens dieser Szene an.
  2. Im Skript sehe ich ...

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

    • use FourWayMoveTrait

  3. Angenommen, wir haben eine Möglichkeit, den Bezeichner (der eigentlich eine 3.1.1-Funktion sein sollte!) zu verfolgen, um das Skript zu öffnen, öffnen wir schließlich das zugehörige Skript und können seine Eigenschaften anzeigen.

Scheint mir die gleiche Anzahl von Schritten zu sein, es sei denn, ich vermisse etwas.

Darüber hinaus ist, welche Eigenschaften bearbeitbar sind, bei Ressourcen sogar noch klarer, würde ich sagen, da, wenn eine Implementierung spezifische Eigenschaften hat, die Tatsache, dass die Daten mit der Implementierung zusammenhängen , klarer ist, wenn Sie den Zugriff darauf mit dem voranstellen müssen Implementierungsinstanz, dh move_impl.<property> .

Das andere Problem ist, dass der Boilerplate-Code häufig wiederholt werden muss. Sie müssen auch sicherstellen, dass die Ressourcenskripte tatsächlich gesetzt sind, bevor Sie die Funktionen aufrufen.

Dies ist wahr, aber ich denke immer noch, dass die Vorteile eines Exports mit einer Initialisierung die Kosten für das hinzugefügte Wort überwiegen:

("Hochrangiger Teamkollege", HLT, wie Designer, Schriftsteller, Künstler usw.)

  • Man kann die Werte direkt aus dem Inspektor zuweisen, anstatt das Skript öffnen zu müssen, die richtige Zeile zum Ändern zu finden und sie dann zu ändern (bereits erwähnt, aber es führt zu ...).

  • Man kann angeben, dass der exportierte Inhalt eine Basistypanforderung hat. Der Inspektor kann dann automatisch eine aufgezählte Liste zulässiger Implementierungen bereitstellen. HLTs können dann sicher nur Ableitungen dieses Typs zuweisen. Dies hilft, sie von der Alternative zu isolieren, die Auswirkungen all der verschiedenen herumschwirrenden Merkmalsskripte kennen zu müssen. Wir müssten auch die automatische Vervollständigung in GDScript ändern, um das Nachschlagen benannter und unbenannter Trait-Dateien als Reaktion auf das Schlüsselwort use zu unterstützen.

  • Man kann eine Konfiguration einer Implementierung als *.tres-Datei serialisieren. HLTs können sie dann per Drag-and-Drop aus dem Dateisystem-Dock ziehen oder sogar ihr eigenes Recht im Inspektor erstellen. Wenn man dasselbe mit Eigenschaften machen möchte, müsste man eine abgeleitete Eigenschaft erstellen, die einen benutzerdefinierten Konstruktor bereitstellt, um den Standardkonstruktor zu überschreiben. Dann würden sie diese Eigenschaft stattdessen als „Vorkonfiguration“ über einen zwingend codierten Konstruktor verwenden.

    1. Schwächer, weil es eher zwingend als deklarativ ist.
    2. Schwächer, da der Konstruktor explizit im Skript definiert werden muss.
    3. Wenn die Eigenschaft unbenannt ist, muss der Benutzer wissen, wo sich die Eigenschaft befindet, um sie anstelle der standardmäßigen Basiseigenschaft richtig verwenden zu können. Wenn das Merkmal benannt wird, verstopft es unnötigerweise den globalen Namensraum.
    4. Wenn sie das Skript so ändern, dass es use FourWayMoveTrait statt use MoveTrait sagt, gibt es keinen dauerhaften Hinweis mehr darauf, dass das Skript überhaupt mit der Basis MoveTrait kompatibel ist. Es lässt HLT-Verwirrung darüber aufkommen, ob das FourWayMoveTrait überhaupt zu einem anderen MoveTrait wechseln kann, ohne Dinge zu beschädigen.
    5. Wenn ein HLT auf diese Weise eine neue Eigenschaftsimplementierung erstellen würde, würde er nicht unbedingt alle Eigenschaften kennen, die von der Basiseigenschaft festgelegt werden können/müssen. Dies ist kein Problem mit Ressourcen, die im Inspektor erstellt wurden.
  • Man kann sogar mehrere Ressourcen des gleichen Typs haben (wenn es dafür einen Grund gibt). Ein Trait würde dies nicht unterstützen, sondern stattdessen Parsing-Konflikte auslösen.

  • Man kann Konfigurationen und/oder ihre individuellen Werte ändern, ohne jemals das 2D/3D-Ansichtsfenster zu verlassen. Dies ist für HLTs viel bequemer (ich kenne viele, die sich sofort ärgern, wenn sie sich Code ansehen müssen ).

Bei allem, was gesagt wurde, stimme ich zu, dass die Boilerplate nervig ist. Um damit fertig zu werden, würde ich jedoch lieber ein Makro- oder Snippet-System hinzufügen, um es zu vereinfachen. Ein Snippet-System wäre eine gute Idee, da es möglicherweise jede Sprache unterstützen könnte, die im ScriptEditor bearbeitet werden kann, anstatt nur GDScript allein.

Etwa:

aber es wird weniger praktisch (Sie können nicht auf einen Blick erkennen, welche Eigenschaften Sie bearbeiten können).

Ich habe über den Inspektor gesprochen, also meine ich hier das "HLT". Leute, die nicht in den Code schauen. Mit Eigenschaften könnten wir dem Skript neue exportierte Eigenschaften hinzufügen. Mit Ressourcenskripten können Sie Variablen nur in die Ressource selbst exportieren, sodass sie im Inspektor nicht angezeigt werden, es sei denn, Sie bearbeiten die Unterressource.

Ich verstehe Ihr Argument, aber es geht über das ursprüngliche Problem hinaus: Vermeiden Sie die Wiederholung von Code (ohne Vererbung). Traits sind für Programmierer. In vielen Fällen gibt es nur eine Implementierung, die Sie wiederverwenden möchten, und Sie möchten diese nicht im Inspektor verfügbar machen. Natürlich können Sie die Ressourcenskripte immer noch ohne Exportieren verwenden, indem Sie sie direkt im Code zuweisen, aber das Problem der Wiederverwendung der exportierten Variablen und Signale aus der gemeinsamen Implementierung wird immer noch nicht gelöst, was einer der Hauptgründe dafür ist, warum dies versucht wird Vererbung verwenden.

Das heißt, derzeit wird versucht, die Vererbung von einem generischen Skript nicht nur für Funktionen, sondern auch für exportierte Eigenschaften (die normalerweise mit diesen Funktionen zusammenhängen) und Signale (die dann mit der Benutzeroberfläche verbunden werden können) zu verwenden.

Das ist das schwer zu lösende Problem. Es gibt einige Möglichkeiten, dies allein mit GDScript zu tun, aber auch hier muss Boilerplate-Code kopiert werden.

Ich kann mir das Hin und Her vorstellen, das stattfinden müsste, damit sich externe Skripte so verhalten, als wären sie direkt im Hauptskript selbst geschrieben.

Wäre eine schöne Sache, wenn dafür nicht Himmel und Erde in Bewegung gesetzt werden müssten. X)

@vnen Ich verstehe, was du jetzt sagst. Nun, da es so aussieht, als ob noch etwas Leben in dieser Ausgabe sein könnte, werde ich sie bei nächster Gelegenheit wieder aufschlagen.

Gibt es Neuigkeiten zur Wiedereröffnung? Jetzt, da die GodotCon 2019 angekündigt ist und der Godot Sprint eine Sache ist, lohnt es sich vielleicht, dort darüber zu sprechen.

@AfterRebelion Ich hatte gerade vergessen, zurückzukommen und es erneut zu öffnen. Danke, dass du mich erinnert hast. XD

@willnationsdev Mir hat gefallen, was ich über das EditorInspectorPlugin gelesen habe! kurze Frage, das bedeutet, dass ich einen benutzerdefinierten Inspektor für einen Datentyp erstellen kann ... zum Beispiel ... dem Inspektor eine Schaltfläche hinzufügen.
(Es ist schon eine ganze Weile her, seit ich dies tun wollte, um eine Möglichkeit zu haben, Ereignisse zu Debugging-Zwecken mit einer Schaltfläche im Inspektor auszulösen. Ich könnte den Knopfdruck dazu bringen, ein Skript auszuführen.)

@xDGameStudios Ja , das und noch viel mehr ist möglich. Jedes benutzerdefinierte Steuerelement kann dem Inspektor hinzugefügt werden, entweder oben, unten, über einer Eigenschaft oder unter einer Kategorie.

@willnationsdev Ich weiß nicht, ob ich dich per Privatnachricht kontaktieren darf!! Aber ich würde gerne mehr über das EditorInspectorPlugin wissen (wie einen Beispielcode). Etwas in den Zeilen eines benutzerdefinierten Ressourcentyps (z. B.) MyResource, der einen Eigenschaftsexport "Name" und eine Inspektor-Schaltfläche hat, die den "Namen" druckt. Variable, wenn ich darauf drücke (im Editor oder während des Debuggens)! Dokumentation fehlt in dieser Angelegenheit sehr ... Ich würde die Dokumentation selbst schreiben, wenn ich wüsste, wie man sie benutzt! :D danke

Ich würde auch gerne mehr darüber erfahren. X)

Ist dies also dasselbe wie ein Autoload-Skript mit Unterklassen, die statische Funktionen enthalten?

zB würde Ihr Fall zu Traits.MoveRightTrait.move_right() werden

oder noch einfacher, mit unterschiedlichen Autoload-Skripten für verschiedene Trait-Klassen:

Movement.move_right()

Nein, Traits sind eine sprachspezifische Funktion, die dem Kopieren und Einfügen des Quellcodes eines Skripts in ein anderes Skript entspricht (ähnlich einer Dateizusammenführung). Wenn ich also ein Merkmal mit move_right() habe und dann erkläre, dass mein zweites Skript dieses Merkmal verwendet, dann kann es auch move_right() verwenden, selbst wenn es nicht statisch ist und selbst wenn es darauf zugreift Eigenschaften an anderer Stelle in der Klasse. Es würde nur zu einem Analysefehler führen, wenn die Eigenschaft im zweiten Skript nicht vorhanden wäre.

Ich habe festgestellt, dass ich entweder doppelten Code habe (kleinere Funktionen, zB einen Bogen machen) oder überflüssige Knoten, weil ich keine Mehrfachvererbung haben kann.

Das wäre großartig, ich muss ein Skript mit genau der gleichen Funktionalität erstellen, das nur auf verschiedenen Knotentypen verwendet werden kann, was unterschiedliche extend s verursacht, also im Grunde nur für eine Codezeile. Übrigens, wenn jemand weiß, wie man das mit dem aktuellen System macht, lass es mich wissen.

Wenn ich eine Funktionalität für ein Skript mit extends Node habe, gibt es eine Möglichkeit, das gleiche Verhalten an einen anderen Knotentyp anzuhängen, ohne die Quelldatei duplizieren und durch entsprechende extend ersetzen zu müssen?

Irgendwelche Fortschritte dabei? Wie ich bereits sagte, muss ich immer wieder Code duplizieren oder Knoten hinzufügen. Ich weiß, dass es in 3.1 nicht gemacht wird, aber vielleicht strebst du 3.2 an?

Oh, ich habe überhaupt nicht daran gearbeitet. Tatsächlich bin ich bei der Implementierung meiner Erweiterungsmethode weiter als bei dieser. Ich muss jedoch mit vnen über beide sprechen, da ich mit ihm die besten Syntax-/Implementierungsdetails ausarbeiten möchte.

Bearbeiten: Wenn jemand anderes versuchen möchte, Eigenschaften zu implementieren, kann er gerne hineinspringen. Man muss sich nur mit den Entwicklern abstimmen.

"Implementierung der Erweiterungsmethode"?

@Zirael07 #15586
Es erlaubt Leuten, Skripte zu schreiben, die neue "eingebaute" Funktionen für Engine-Klassen hinzufügen können. Meine Interpretation der Syntax wäre ungefähr so:

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

Dann könnte ich woanders einfach tun:

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

Es würde heimlich das ständig geladene Erweiterungsskript aufrufen und die gleichnamige statische Funktion aufrufen, die an die Array-Klasse gebunden ist, und die Instanz als ersten Parameter übergeben.

Ich habe einige Gespräche mit @vnen und @jahd2602 darüber geführt. Eine Sache, die mir in den Sinn kommt, ist Jais Lösung für Polymorphismus: Importieren des Namensraums einer Eigenschaft.

Auf diese Weise könnten Sie etwa Folgendes tun:

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()"

Der Punkt ist, dass das Schlüsselwort using den Namensraum einer Eigenschaft importiert, so dass b.foo() wirklich nur syntaktischer Zucker für b.a.foo() ist.

Und stellen Sie dann sicher, dass b is A == true und B in getippten Situationen verwendet werden können, die auch A akzeptieren.

Dies hat auch den Vorteil, dass Dinge nicht als Merkmale deklariert werden müssen, dies würde für alles funktionieren, was keine gemeinsamen Qualitätsnamen hat.

Ein Problem ist, dass dies nicht gut mit dem aktuellen Vererbungssystem zusammenpasst. Wenn sowohl A als auch B Node2D sind und wir eine Funktion in A erstellen: func baz(): print(self.position) , welche Position wird gedruckt, wenn wir b.baz() aufrufen?
Eine Lösung könnte darin bestehen, den Anrufer self bestimmen zu lassen. Der Aufruf von b.foo() würde foo() mit b als self aufrufen und bafoo() würde foo() mit a als self aufrufen.

Wenn wir freistehende Methoden wie Python hätten (wobei x.f(y) Zucker für f(x,y) ist), könnte dies wirklich einfach zu implementieren sein.

Eine andere, nicht verwandte Idee:

Konzentrieren Sie sich nur auf freistehende Funktionen im JavaScript-Stil.

Wenn wir die Konvention x.f(y) == f(x,y) für statische Funktionen übernehmen, könnten wir sehr leicht Folgendes haben:

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))

Dies hätte minimale Auswirkungen auf das Klassensystem, da es wirklich nur Methoden betrifft. Wenn wir dies jedoch wirklich flexibel haben wollten, könnten wir volles JavaScript verwenden und einfach zulassen, dass Funktionsdefinitionen zuweisbare, aufrufbare Werte sind.

@jabcross Klingt gut, ich mag das Konzept einer Art optionalem Namespace und die Funktionsidee ist interessant.

In Bezug auf den Namespace frage ich mich, warum nicht einfach using A , die anderen deklarativen Dinge scheinen irrelevant zu sein.

Neugierig auch, wie es mit der Mehrfachvererbung gelöst werden müsste. Ich nehme an, Option A erzwingt, dass beide Skripte denselben Typ erben, sodass sie beide nur über derselben Klasse erweitert werden, ohne dass eine spezielle Zusammenführung erfolgt.

Option B, vielleicht zusätzliche GDScript-Schlüsselwörter, um eine Eigenschaftsklasse anzugeben und von welcher Klasse Sie Hinweise haben möchten. Es wäre die gleiche Idee, aber nur zusätzliche Schritte, um expliziter zu erscheinen.

An der Spitze von A.gd:

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

Ohhh, ich mag das Konzept des Namespace-Imports wirklich. Es löst nicht nur das Trait-Problem, sondern möglicherweise auch das Konzept der „Erweiterungsmethoden“ zum Hinzufügen von Inhalten zu Engine-Typen.

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 Wenn wir dann auch Lambas hinzufügen und/oder Objekten erlauben würden, einen Aufrufoperator zu implementieren (und einen callable -Typ für kompatible Werte hätten), könnten wir damit beginnen, dem GDScript-Code einen stärker funktionsorientierten Ansatz hinzuzufügen (der Ich denke, es wäre eine gute Idee). Zugegeben, an diesem Punkt ging ich mehr in @vnens #18698-Territorium, aber …

Wir müssen berücksichtigen, dass GDScript immer noch eine dynamische Sprache ist und einige dieser Vorschläge eine Laufzeitprüfung erfordern, um die Aufrufe korrekt zu verteilen, was zu einer Leistungseinbuße führt (selbst für Personen, die die Funktionen nicht verwenden, da alle Funktionsaufrufe überprüft werden müssen). und vielleicht auch die Eigenschaftssuche). Aus diesem Grund bin ich mir auch nicht sicher, ob das Hinzufügen von Erweiterungen eine gute Idee ist (zumal sie im Wesentlichen Syntaxzucker sind).

Ich bevorzuge das reine Trait-System, wo Traits keine Klassen sind, sondern eine eigene Sache. Auf diese Weise können sie während der Kompilierzeit vollständig aufgelöst werden und bei Namenskonflikten Fehlermeldungen liefern. Ich glaube, dies würde das Problem ohne zusätzlichen Laufzeitaufwand lösen.

@vnen Ahhh, das mit den Laufzeitkosten war mir nicht klar. Und wenn dies für die Implementierung einer Erweiterungsmethode gilt, dann wäre das meiner Meinung nach auch nicht ideal.

Wenn wir damals ein reines Trait-System erstellt haben, dachten Sie dann daran, einfach trait TraitName anstelle von extends zu machen, gepaart mit einem using TraitName unter Erweiterungen in anderen Skripten? Und würden Sie das selbst umsetzen oder delegieren?

Wenn wir damals ein reines Trait-System erstellt haben, dachten Sie dann daran, einfach trait TraitName anstelle von extends zu machen, gepaart mit einem using TraitName unter Erweiterungen in anderen Skripten?

Das ist meine Idee. Ich glaube, es ist einfach genug und deckt so ziemlich alle Anwendungsfälle für die Wiederverwendung von Code ab. Ich würde der Klasse, die das Merkmal verwendet, sogar erlauben, die Merkmalsmethoden zu überschreiben (wenn dies zur Kompilierzeit möglich ist). Merkmale können auch andere Merkmale erweitern.

Und würden Sie das selbst umsetzen oder delegieren?

Ich hätte nichts dagegen, die Aufgabe jemand anderem zu übertragen, der dazu in der Lage ist. Ich habe sowieso wenig Zeit. Aber wir sollten uns vorher auf das Design einigen. Ich bin ziemlich flexibel mit den Details, aber es sollte 1) keine Laufzeitprüfungen verursachen (glauben, dass sich GDScript gut für Dinge eignet, die bei der Kompilierung nicht herauszufinden sind), 2) relativ einfach sein und 3) nicht zu viel hinzufügen Kompilierungszeit.

@vnen Ich mag diese Ideen. Hast du dich gefragt, wie du dir vorstellst, wie ein Merkmal Dinge wie die automatische Vervollständigung für die Klassen tun könnte, die es enthalten würden, oder wäre das nicht möglich?

Hast du dich gefragt, wie du dir vorstellst, wie ein Merkmal Dinge wie die automatische Vervollständigung für die Klassen tun könnte, die es enthalten würden, oder wäre das nicht möglich?

Ein Merkmal wäre meiner Ansicht nach im Wesentlichen ein "Import". Es sollte trivial sein, die Vervollständigung anzuzeigen, vorausgesetzt, dass die Vervollständigung für Mitglieder funktioniert.

@vnen Ich würde mir vorstellen, dass Sie im Wesentlichen in einen ClassNode mit einem darauf gesetzten trait -Flag parsen würden. Und wenn Sie dann eine using -Anweisung machen, würde es versuchen, alle Eigenschaften/Methoden/Signale/Konstanten/Unterklassen in das aktuelle Skript zusammenzuführen.

  1. Wenn eine Methode kollidiert, würde die Implementierung des aktuellen Skripts die Basismethode überschreiben, als würde sie eine geerbte Methode überschreiben.

    • Aber was tun, wenn eine Basisklasse bereits die Methode "merged" hat?

  2. Wenn sich eine Eigenschaft, ein Signal und eine Konstante überschneiden, prüfen Sie, ob es sich um denselben Typ/dieselbe Signalsignatur handelt. Wenn es keine Nichtübereinstimmung gibt, betrachten Sie es einfach als "Zusammenführung" der Eigenschaft / des Signals / der Konstante. Benachrichtigen Sie andernfalls den Benutzer über einen Typ-/Signaturkonflikt (ein Analysefehler).

Schlechte Idee? Oder sollten wir nicht methodische Konflikte schlichtweg verbieten? Was ist mit Unterklassen? Ich habe das Gefühl, wir sollten das zu Konflikten machen.

@willnationsdev Klingt wie das "Diamantproblem" (auch bekannt als "tödlicher Diamant des Todes"), eine gut dokumentierte Mehrdeutigkeit mit verschiedenen Lösungen, die bereits in verschiedenen gängigen Programmiersprachen angewendet werden.

Was mich erinnert:
@vnen können Eigenschaften andere Eigenschaften erweitern?

@ jahd2602 Er hat das bereits als Möglichkeit vorgeschlagen

Merkmale können auch andere Merkmale erweitern.

@ jahd2602 Basierend auf den Perl/Python-Lösungen scheint es, als würden sie im Grunde genommen einen "Stapel" von Ebenen bilden, die den Inhalt für jede Klasse enthalten, sodass Konflikte aus dem zuletzt verwendeten Merkmal auf den anderen Versionen liegen und diese überschreiben. Das klingt nach einer ziemlich guten Lösung für dieses Szenario. Es sei denn, Sie oder @vnen haben alternative Gedanken. Danke für die verlinkte Lösungsübersicht jahd.

Ein paar Fragen.

Erstens: Auf welche Weise sollten wir die using-Anweisung unterstützen?

Ich denke, dass die using -Anweisung ein GDScript mit konstantem Wert erfordern sollte.

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

Ich denke an all das oben Genannte.

Zweitens: Wie sollten wir sagen, dass die zulässige Syntax zum Definieren eines Merkmals und/oder seines Namens sein sollte?

Jemand möchte vielleicht nicht unbedingt eine Skriptklasse für seine Eigenschaft verwenden, also denke ich nicht, dass wir eine trait TraitName -Anforderung erzwingen sollten. Ich denke, dass trait in einer Zeile stehen muss.

Wenn es sich also um ein Tool-Skript handelt, sollte es natürlich Tool an der Spitze haben. Wenn es sich um ein Merkmal handelt, muss in der folgenden Zeile definiert werden, ob es sich um ein Merkmal handelt. Gestatten Sie optional jemandem, den Namen der Skriptklasse nach der Eigenschaftsdeklaration in derselben Zeile anzugeben, und gestatten Sie ihm in diesem Fall nicht, auch class_name zu verwenden. Wenn sie den Eigenschaftsnamen weglassen, ist class_name <name> in Ordnung. Wenn wir dann einen anderen Typ erweitern, könnten wir das extends nach der Trait-Deklaration und/oder in einer eigenen Zeile nach der Trait-Deklaration einfügen. Also würde ich jedes davon für gültig halten:

# 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

Drittens: Sollten wir für Autovervollständigungszwecke und/oder Absichtserklärungen/Anforderungen zulassen, dass ein Merkmal einen Basistyp definiert, den es erweitern muss?

Wir haben bereits besprochen, dass Eigenschaften das Vererben anderer Eigenschaften unterstützen sollten. Aber sollten wir ein TraitA auf extend Node zulassen, erlauben wir dem TraitA-Skript, die automatische Vervollständigung des Knotens zu erhalten, aber dann auch einen Parsing-Fehler auszulösen, wenn wir eine using TraitA -Anweisung ausführen, wenn die aktuelle Klasse den Knoten nicht erweitert oder einer seiner abgeleiteten Typen?

Viertens: Anstatt Traits andere Traits erweitern zu lassen, können wir nicht stattdessen einfach die Anweisung extends für Klassenerweiterungen reserviert lassen, zulassen, dass eine Eigenschaft diese Anweisung überhaupt nicht benötigt, anstatt eine Basiseigenschaft zu erweitern, erlauben Merkmale, um einfach ihre eigenen using -Aussagen zu haben, die diese Merkmale sub-importieren?

# 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".

Der Vorteil hier wäre natürlich, dass Sie mehrere Traits unter einem einzigen Trait-Namen bündeln könnten, indem Sie mehrere using -Anweisungen verwenden, ähnlich wie C++-Include-Dateien, die mehrere andere Klassen enthalten.

Fünftens: Wenn wir ein Merkmal haben und es using oder extends für eine Methode hat und dann seine eigene implementiert, was machen wir, wenn es innerhalb dieser Funktion .<method_name> aufruft

cc @vnen

Erstens: Auf welche Weise sollten wir die using-Anweisung unterstützen?

Ich denke, dass die using -Anweisung ein GDScript mit konstantem Wert erfordern sollte.

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

Ich bin mit all dem einverstanden. Aber für den Pfad verwende ich direkt einen String: using "res://my_trait.gd"

Zweitens: Wie sollten wir sagen, dass die zulässige Syntax zum Definieren eines Merkmals und/oder seines Namens sein sollte?

Jemand möchte vielleicht nicht unbedingt eine Skriptklasse für seine Eigenschaft verwenden, daher denke ich nicht, dass wir eine trait TraitName -Anforderung erzwingen sollten. Ich denke, dass trait in einer Zeile stehen muss.

Wenn es sich also um ein Tool-Skript handelt, sollte es natürlich Tool an der Spitze haben. Wenn es sich um ein Merkmal handelt, muss in der folgenden Zeile definiert werden, ob es sich um ein Merkmal handelt. Gestatten Sie optional jemandem, den Namen der Skriptklasse nach der Eigenschaftsdeklaration in derselben Zeile anzugeben, und gestatten Sie ihm in diesem Fall nicht, auch class_name zu verwenden. Wenn sie den Eigenschaftsnamen weglassen, ist class_name <name> in Ordnung. Wenn wir dann einen anderen Typ erweitern, könnten wir das extends nach der Trait-Deklaration und/oder in einer eigenen Zeile nach der Trait-Deklaration einfügen. Also würde ich jedes davon für gültig halten:

# 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 auf einem Trait sollten keinen Unterschied machen, da sie nicht direkt ausgeführt werden.

Ich stimme zu, dass ein Merkmal nicht unbedingt einen globalen Namen hat. Ich würde trait ähnlich wie tool verwenden. Es muss das Erste in der Skriptdatei sein (mit Ausnahme von Kommentaren). Dem Schlüsselwort sollte optional der Eigenschaftsname folgen. Ich würde class_name nicht dafür verwenden, da es sich nicht um Klassen handelt.

Drittens: Sollten wir für Autovervollständigungszwecke und/oder Absichtserklärungen/Anforderungen zulassen, dass ein Merkmal einen Basistyp definiert, den es erweitern muss?

Ich mag es ehrlich gesagt nicht, Features in der Sprache um des Editors willen hinzuzufügen. Hier wären Anmerkungen hilfreich.

Wenn wir Eigenschaften nun nur auf einen bestimmten Typ (und seine Ableitungen) anwenden wollen, dann ist das in Ordnung. Tatsächlich denke ich, dass dies wegen der statischen Prüfungen besser ist: Es erlaubt einer Eigenschaft, Dinge aus einer Klasse zu verwenden, während die Kompilierung tatsächlich prüfen kann, ob sie mit den richtigen Typen und so weiter verwendet werden.

Viertens: Anstatt Traits andere Traits erweitern zu lassen, können wir nicht stattdessen einfach die Anweisung extends für Klassenerweiterungen reserviert lassen, zulassen, dass eine Eigenschaft diese Anweisung überhaupt nicht benötigt, anstatt eine Basiseigenschaft zu erweitern, erlauben Eigenschaften, um einfach ihre eigenen using -Aussagen zu haben, die _diese_ Eigenschaften sub-importieren?

Nun, das ist vor allem eine Frage der Semantik. Als ich erwähnte, dass Eigenschaften andere erweitern können, wollte ich nicht wirklich das Schlüsselwort extends verwenden. Der Hauptunterschied besteht darin, dass Sie mit extends nur eine erweitern können, mit using können Sie viele andere Eigenschaften in eine einbetten. Ich bin mit using , solange es keine Zyklen gibt, ist es kein Problem.

Fünftens: Wenn wir ein Merkmal haben und es ein using oder ein extends für eine Methode hat und dann seine eigene implementiert, was machen wir, wenn es innerhalb dieser Funktion .<method_name> aufruft

Das ist eine knifflige Frage. Ich würde davon ausgehen, dass ein Merkmal nichts mit der Klassenvererbung zu tun hat. Die Punktnotation sollte also die Methode für das übergeordnete Merkmal aufrufen, falls vorhanden, andernfalls einen Fehler auslösen. Ein Merkmal sollte sich nicht bewusst sein, in welchen Klassen es sich befindet.

OTOH, ein Merkmal ist fast wie ein "include", also würde es wörtlich auf die Klasse angewendet und daher die übergeordnete Implementierung aufgerufen. Aber ehrlich gesagt würde ich die Punktnotation einfach verbieten, wenn die Methode nicht im übergeordneten Merkmal enthalten ist.

Was ist mit einer Eigenschaft, die erfordert, dass die Klasse eine oder mehrere andere Eigenschaften hat? Zum Beispiel eine Eigenschaft DoubleJumper , die sowohl die Eigenschaft Jumper erfordert, als auch eine Eigenschaft Upgradable und eine Klasse, die KinematicBody2D erbt.

Rust zum Beispiel ermöglicht es Ihnen, Typsignaturen wie diese zu verwenden. Etwas wie KinematicBody2D: Jumper, Upgradable . Aber da wir : verwenden, um den Typ zu kommentieren, könnten wir einfach KinematicBody2D & Jumper & Upgradable oder so etwas verwenden.

Es gibt auch das Problem der Polymorphie. Was ist, wenn die Implementierung des Merkmals für jede Klasse unterschiedlich ist, aber dieselbe Schnittstelle verfügbar macht?

Zum Beispiel wollen wir eine Methode kill() in der Eigenschaft Jumper , die sowohl von Enemy als auch Player verwendet wird. Wir möchten für jeden Fall unterschiedliche Implementierungen, wobei beide weiterhin mit der gleichen Signatur des Typs Jumper kompatibel bleiben. Wie macht man das?

Für den Polymorphismus würden Sie einfach ein separates Merkmal erstellen, das das Merkmal mit kill() enthält und dann eine eigene spezifische Version der Methode implementiert. Die Verwendung von Merkmalen, die die Methoden zuvor eingeschlossener Merkmale überschreiben, ist die Art und Weise, wie Sie damit umgehen würden.

Außerdem glaube ich nicht, dass es (noch) Pläne gab, einen Typenhinweis mit Eigenschaftenanforderungen zu versehen. Ist das etwas, was wir tun möchten?

Erstellen Sie ein separates Merkmal

Würde das nicht eine Menge einmaliger Trait-Dateien erzeugen? Wenn wir verschachtelte Trait-Deklarationen machen könnten (ähnlich dem Schlüsselwort class ), könnte das bequemer sein. Wir könnten die Methoden auch direkt in der Klasse überschreiben, die das Merkmal verwendet.

Ich würde ein starkes Typsignatursystem wirklich schätzen (vielleicht mit boolescher Zusammensetzung und optionalen/Nicht-Nullwerten). Traits würden genau passen.

Ich bin mir nicht sicher, ob es diskutiert wurde, aber ich denke, es sollte möglich sein, eine merkmalsspezifische Version einer Funktion aufzurufen. Beispielsweise:

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()

was druckt: A , B , B .


Auch bin ich mir über die "keine Laufzeitkosten" nicht ganz sicher. Wie würden dynamisch geladene Skripte (während des Exports nicht verfügbar) mit Klassen gehandhabt, die Eigenschaften verwenden, die vor dem Export definiert wurden? Verstehe ich etwas falsch? Oder wird dieser Fall nicht als "Laufzeit" betrachtet?

Ich bin mir nicht sicher, ob es diskutiert wurde, aber ich denke, es sollte möglich sein, eine merkmalsspezifische Version einer Funktion aufzurufen.

Ich habe bereits darüber nachgedacht, bin mir aber nicht sicher, ob ich einer Klasse erlauben soll, widersprüchliche Eigenschaften zu verwenden (dh Eigenschaften, die Methoden mit demselben Namen definieren). Die Reihenfolge der using -Anweisungen sollte keinen Unterschied machen.

Auch bin ich mir über die "keine Laufzeitkosten" nicht ganz sicher. Wie würden dynamisch geladene Skripte (während des Exports nicht verfügbar) mit Klassen gehandhabt, die Eigenschaften verwenden, die vor dem Export definiert wurden? Verstehe ich etwas falsch? Oder wird dieser Fall nicht als "Laufzeit" betrachtet?

Es geht nicht ums Exportieren. Es wird sich definitiv auf die Ladezeit auswirken, da die Kompilierung beim Laden erfolgt (obwohl ich nicht glaube, dass es sehr wichtig sein wird), aber es sollte sich nicht auf die Ausführung des Skripts auswirken. Idealerweise sollten Skripte beim Export kompiliert werden, aber das ist eine andere Diskussion.

Hallo an alle.

Ich bin neu bei Godot und habe mich in den letzten Tagen daran gewöhnt. Als ich versuchte, die besten Methoden zur Herstellung wiederverwendbarer Komponenten herauszufinden, hatte ich mich für ein Muster entschieden. Ich würde immer den Wurzelknoten einer Unterszene, die in einer anderen Szene instanziiert werden soll, alle Eigenschaften exportieren, die ich von außen festlegen möchte. Ich wollte so weit wie möglich die Kenntnis der internen Struktur des instanzierten Zweigs für den Rest der Szene überflüssig machen.

Damit dies funktioniert, muss der Stammknoten Eigenschaften "exportieren" und dann die Werte in das entsprechende untergeordnete Element in _ready kopieren. Stellen Sie sich beispielsweise einen Bombenknoten mit einem untergeordneten Timer vor. Der Root-Knoten Bombe in der Unterszene würde "detonation_time" exportieren und dann $Timer.wait_time = detonation_time in _ready ausführen. Dies ermöglicht es uns, es in Godots Benutzeroberfläche zu setzen, wann immer wir es instanziieren, ohne Kinder bearbeitbar machen und zum Timer gehen zu müssen.

jedoch
1) Es ist eine sehr mechanische Transformation, also scheint es, als könnte etwas Ähnliches vom System unterstützt werden
2) Es fügt wahrscheinlich eine leichte Ineffizienz hinzu, wenn der entsprechende Wert direkt im untergeordneten Knoten festgelegt wird.

Bevor ich fortfahre, scheint dies nebensächlich zu dem zu sein, was besprochen wird, da es nicht darum geht, eine Art "private" Vererbung (im C++-Sprachgebrauch) zuzulassen. Ich mag jedoch Godots System, Verhalten zu erstellen, indem Szenenelemente komponiert werden, anstatt mehr vererbungsähnliche Technik. Diese „eingeschriebenen“ Beziehungen sind unveränderlich und statisch. OTOH, die Szenenstruktur ist dynamisch, Sie können sie sogar zur Laufzeit ändern. Die Spiellogik kann sich während der Entwicklung so sehr ändern, dass Godots Design meiner Meinung nach sehr gut zum Anwendungsfall passt.

Es ist wahr, dass untergeordnete Knoten als Verhaltenserweiterungen von Stammknoten verwendet werden, aber das führt nicht dazu, dass es ihnen an Autarkie mangelt, IMO. Ein Timer ist vollkommen in sich geschlossen und im Verhalten vorhersagbar, unabhängig davon, was er an Zeit gewöhnt ist. Egal, ob Sie einen Löffel verwenden, um Suppe zu trinken oder Eiscreme zu essen, er erfüllt seine Funktion angemessen, obwohl er als Verlängerung Ihrer Hand fungiert. Ich betrachte Wurzelknoten als Maestros, die das Verhalten von untergeordneten Knoten koordinieren, sodass sie nicht direkt voneinander wissen müssen und daher in der Lage sind, in sich geschlossen zu bleiben. Übergeordnete/Root-Knoten sind an den Schreibtisch gebundene Manager, die Verantwortlichkeiten delegieren, aber nicht viel direkte Arbeit leisten. Da sie dünn sind, ist es einfach, eine neue für etwas anderes Verhalten zu erstellen.

Ich denke jedoch, dass Root-Knoten auch als primäre SCHNITTSTELLE für die Funktionalität des gesamten instanzierten Zweigs fungieren sollten. Alle Eigenschaften, die in der Instanz angepasst werden können, sollten im Stammknoten des Zweigs "einstellbar" sein, selbst wenn der endgültige Eigentümer der Eigenschaft ein untergeordneter Knoten ist. Sofern ich nichts vermisse, muss dies in der aktuellen Version von Godot manuell arrangiert werden. Es wäre schön, wenn dies irgendwie automatisiert werden könnte, um die Vorteile eines dynamischen Systems mit einfacherer Skripterstellung zu kombinieren.

Eine Sache, an die ich denke, ist ein System der "dynamischen Vererbung", wenn Sie so wollen, das für Unterklassen von Node verfügbar ist. Es würde zwei Quellen von Eigenschaften/Methoden in einem solchen Skript geben, die des Skripts, das es erweitert, und die, die von Kindern innerhalb der Szenenstruktur "heraufgesprudelt" werden. Mein Beispiel mit der Bombe würde also so etwas wie export lifted var $Timer.wait_time [= value?] as detonation_time innerhalb des Abschnitts mit den Mitgliedsvariablen des bomb.gd-Skripts werden. Das System würde im Wesentlichen $Timer.wait_time = detonation_time im _ready-Callback generieren und den Getter/Setter generieren, der es $Bomb.detonation_time = 5 vom übergeordneten Knoten von Bomb ermöglicht, dass $Timer.wait_time = 5 gesetzt wird.

Im OP-Beispiel mit MoveRightTrait hätten wir den Knoten, an den mysprite.gd angehängt ist, MoveRightTrait als untergeordneten Knoten. Dann hätten wir in mysprite.gd so etwas wie lifted func $MoveRightTrait.move_right [as move_right] (vielleicht könnte 'as' optional sein, wenn der Name derselbe ist). Jetzt würde der Aufruf von move_right für ein Skriptobjekt, das aus mysprite.gd erstellt wurde, automatisch an den entsprechenden untergeordneten Knoten delegieren. Vielleicht könnten Signale gesprudelt werden, damit sie von der Wurzel aus an einen untergeordneten Knoten angehängt werden können? Vielleicht könnten ganze Nodes mit nur lifted $MoveRightTrait [as MvR] ohne func, signal oder var gesprudelt werden. In diesem Fall wären alle Methoden und Eigenschaften auf MoveRightTrait von mysprite direkt als mysprite.move_right oder über mysprite.MvR.move_right zugänglich, wenn „as MvR“ verwendet wird.

Das ist eine Idee, wie man die Erstellung einer SCHNITTSTELLE zu einer Szenenstruktur im Stamm eines instanzierten Zweigs vereinfacht, ihre "Black Box"-Charakteristik erhöht und Skripterstellungskomfort zusammen mit der Leistungsfähigkeit von Godots dynamischem Szenensystem erhält. Natürlich gibt es viele Nebendetails zu beachten. Beispielsweise können im Gegensatz zu Basisklassen untergeordnete Knoten zur Laufzeit entfernt werden. Wie sollten sich die Bubbled/Lifted-Funktionen und -Eigenschaften verhalten, wenn sie in diesem Fehlerfall aufgerufen/zugegriffen werden? Wenn ein Knoten mit dem richtigen NodePath wieder hinzugefügt wird, funktionieren die aufgehobenen Eigenschaften wieder? [JA, IMO] Es wäre auch ein Fehler, "lifted" in Klassen zu verwenden, die nicht von Node abgeleitet sind, da es in diesem Fall niemals Kinder geben würde, aus denen Bubble/Lift gemacht werden könnte. Außerdem sind Namenskonflikte mit dupliziertem "as {name}" oder "lifted $Timer1 lifted $Timer2" möglich, wenn Knoten Eigenschaften/Methoden mit demselben Namen haben. Der Skriptinterpreter würde solche logischen Probleme idealerweise erkennen.

Ich denke, das würde uns viel von dem geben, was wir wollen, obwohl es wirklich nur syntaktischer Zucker ist, der uns das Schreiben von Weiterleitungsfunktionen und Initialisierungen erspart. Da es konzeptionell im Grunde einfach ist, sollte es auch nicht so schwer zu implementieren oder zu erklären sein.

Wie auch immer, wenn Sie so weit gekommen sind, danke fürs Lesen!

Ich habe überall "gehoben" verwendet, aber das ist nur illustrativ.
Etwas wie using var $Timer.wait_time as detonation_time oder using $Timer ist offensichtlich genauso gut. In jedem Fall können Sie bequem von untergeordneten Knoten pseudoerben und so einen kohärenten einzelnen Zugriffspunkt auf die gewünschte Funktionalität im Stamm des zu instanziierenden Zweigs erstellen. Die Anforderung an die wiederverwendbaren Teile der Funktionalität besteht darin, dass sie Node oder eine Unterklasse davon erweitern, sodass sie der größeren Komponente als untergeordnete Elemente hinzugefügt werden können.

Eine andere Sichtweise ist, dass das Schlüsselwort „extends“ in einem Skript, das von einem Knoten erbt, Ihnen Ihre „is-a“-Beziehung gibt, während Sie das Schlüsselwort „using“ oder „lifted“ in einem Skript verwenden, um a zu „blasen“. Die Mitglieder des Nachkommenknotens geben Ihnen so etwas wie "Implementierungen" [hey, mögliches Schlüsselwort], das in Sprachen mit einfacher Vererbung, aber mehreren "Schnittstellen" (z. B. Java) existiert. Bei uneingeschränkter Mehrfachvererbung (wie c++) bilden Basisklassen einen [statischen, eingeschriebenen] Baum. In Analogie dazu schlage ich vor, Godots vorhandene Node-Bäume mit bequemer Syntax und Boilerplate-Eliminierung zu überlagern.

Wenn sich jemals herausstellt, dass es sich lohnt, dies zu untersuchen, müssen Aspekte des Designraums berücksichtigt werden:
1) Sollten wir nur unmittelbare Kinder in einem "Using" zulassen. IOW using $Timer aber nicht 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:` wäre per Konvention ein Test für die Funktionalität.
4) Das Zustandsmuster sollte implementierbar sein, indem ein "using"-ed-Knoten durch einen anderen ersetzt wird, der ein abweichendes Verhalten, aber dieselbe Schnittstelle [Ententypisierung] und denselben NodePath hat, auf den in der "using"-Anweisung verwiesen wird. Mit der Funktionsweise des Szenenbaums würde das fast umsonst gehen. Das System müsste jedoch Signale verfolgen, die durch einen Elternteil verbunden sind, und Verbindungen in dem ersetzten Kind wiederherstellen.

Ich arbeite jetzt schon seit einiger Zeit mit GDScript und muss zugeben, dass eine Art Trait/Mixin und Proxy/Delegation Feature dringend benötigt wird. Es ist ziemlich lästig, all diese Boilerplates einrichten zu müssen, nur um Eigenschaften zu verbinden oder Methoden von Kindern im Stammverzeichnis der Szene aufzudecken.

Oder das Hinzufügen von Ebenen des Baums nur, um Komponenten zu simulieren (es wird ziemlich schnell ziemlich umständlich, weil Sie dann mit jeder neuen Komponente alle Knotenpfade aufbrechen). Vielleicht gibt es einen besseren Weg, so etwas wie Meta/Multi-Skript, das mehrere Skripte auf einem Knoten erlaubt? Wenn Sie eine idiomatische Lösung haben, teilen Sie sie bitte mit ...

C++ (GDNative) in den Mix zu werfen, macht die Sache noch schlimmer, weil sich _ready und _init dort anders verhalten (sprich: Initialisierung mit Standardwerten funktioniert nur halb oder gar nicht).

Dies ist die Hauptsache, die ich in GDScript umgehen muss. Ich muss oft Funktionen über Knoten hinweg teilen, ohne meine gesamte Vererbungsstruktur darum herum zu strukturieren – zum Beispiel haben mein Spieler und meine Ladenbesitzer ein Inventar, meine Spieler + Gegenstände + Feinde haben Statistiken, mein Spieler und meine Feinde haben Gegenstände ausgerüstet usw.

Derzeit implementiere ich diese gemeinsam genutzten „Komponenten“ als Klassen oder Knoten, die in die „Entitäten“ geladen werden, die sie benötigen, aber es ist chaotisch (fügt viele Suchen nach Knoten hinzu, macht Enteneingabe fast unmöglich usw.) und alternative Ansätze haben daher ihre eigenen Nachteile Ich habe keinen besseren Weg gefunden. Traits/Mixins würden mir absolut das Leben retten.

Worauf es ankommt, ist die Möglichkeit, Code über Objekte hinweg zu teilen, ohne Vererbung zu verwenden, was meiner Meinung nach in Godot im Moment sowohl notwendig als auch nicht sauber möglich ist.

Ich verstehe Rust-Traits (https://doc.rust-lang.org/1.8.0/book/traits.html) so, dass sie wie Haskell-Typklassen sind, bei denen einige parametrisierte Funktionen für den Typ definiert werden müssen Sie fügen ein Merkmal hinzu, und dann können Sie einige generische Funktionen verwenden, die über allen Typen definiert sind, die ein Merkmal implementieren. Sind Rust-Merkmale etwas anderes als das, was hier vorgeschlagen wird?

Dieser wird wahrscheinlich im großen Stil migriert, da er hier ausführlich diskutiert wurde.

Dieser wird wahrscheinlich im großen Stil migriert, da er hier ausführlich diskutiert wurde.

_Ich finde das "Verschieben" von Vorschlägen imo sinnlos, sie werden besser geschlossen und gebeten, in Godot-Vorschlägen wieder zu öffnen, wenn Leute Interesse bekunden, und andere Vorschläge bei Bedarf tatsächlich umsetzen zu lassen. Sowieso..._

Ich bin vor einem Jahr auf dieses Problem gestoßen, aber erst jetzt beginne ich, die potenzielle Nützlichkeit des Merkmalssystems zu verstehen.

Ich teile meinen aktuellen Arbeitsablauf in der Hoffnung, jemanden dazu zu inspirieren, das Problem besser zu verstehen (und vielleicht eine bessere Alternative vorzuschlagen, als das Traits-System zu implementieren).

1. Erstellen Sie ein Tool zum Generieren von Komponentenvorlagen für jeden verwendeten Knotentyp im Projekt:

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

Nun, was DIES einfacher machen würde, wäre ein Snippet- oder Makrosystem, damit die Erstellung dieser wiederverwendeten deklarativen Codeabschnitte für Entwickler einfacher ist.

Auf deinen Spuren wandeln... 😅

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. Erstellen Sie eine statische Methode, die zum Initialisieren von Komponenten für den Host wiederverwendet werden soll (z. B. root):

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')

So sieht eine Komponente (Trait) aus, wenn sie mit dem ersten Skript erstellt wurde:

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)

(Optional) 3. Erweitern Sie die Komponente (Merkmal)

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

Das ist meine Idee. Ich glaube, es ist einfach genug und deckt so ziemlich alle Anwendungsfälle für die Wiederverwendung von Code ab. Ich würde der Klasse, die das Merkmal verwendet, sogar erlauben, die Merkmalsmethoden zu überschreiben (wenn dies zur Kompilierzeit möglich ist). Merkmale können auch andere Merkmale erweitern.

Auf deinen Spuren wandeln... 😅

class_name ComponentMotion2D extends ComponentNode2D

const MAX_SPEED = 100.0

var linear_velocity = Vector2()
var collision

export(Script) var impl
...

Tatsächlich werden die exportierten Script s in diesen Komponenten verwendet, um das Verhalten bestimmter Host-/Root-Knotentypen pro Komponente zu steuern. Hier hat ComponentMotion2D hauptsächlich zwei Skripte:

  • motion_kinematic_body_2d.gd
  • motion_rigid_body_2d.gd

Die Kinder fahren hier also immer noch das host / root Verhalten. Die host -Terminologie stammt von mir, wenn ich Zustandsmaschinen verwende, und hier würden Eigenschaften vielleicht nicht perfekt passen, weil die Zustände imo besser als Knoten organisiert sind.

Die Komponenten selbst sind in der Wurzel "fest verdrahtet", indem sie zu onready Mitgliedern gemacht werden, wodurch der Boilerplate-Code effektiv verringert wird (mit dem Aufwand, sie tatsächlich als object.motion referenzieren zu müssen).

extends KinematicBody2D

onready var motion = $motion # ComponentMotion2D

Ich bin mir nicht sicher, ob dies zur Lösung des Problems beitragen würde, aber C# hat eine Sache namens Erweiterungsmethoden , die die Funktionalität eines Klassentyps erweitern.

Grundsätzlich muss die Funktion statisch sein, und der erste Parameter ist erforderlich und muss self sein. Als Definition sähe das so aus:

Erweiterung.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)

Wenn Sie dann die Methode distance verwenden möchten, tun Sie dies einfach:

Spieler.gd

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

Ich kenne es, aber das hilft nicht, das Problem zu lösen. Hihi, danke trotzdem.

@TheColorRed- Erweiterungsmethoden wurden bereits vorgeschlagen, aber ich glaube nicht, dass sie in einer dynamischen Sprache machbar sind. Ich glaube auch nicht, dass sie das Grundproblem lösen, das diese Diskussion ursprünglich ausgelöst hat.


Außerdem werde ich wahrscheinlich viele der Vorschläge für GDScript als GIPs öffnen (dies eingeschlossen, wenn @willnationsdev nichts dagegen hat).

Ich glaube immer noch, dass Traits am sinnvollsten sind, um Code horizontal in einer OO-Sprache zu teilen (ohne Mehrfachvererbung, aber ich möchte diesen Weg nicht gehen).

Ich glaube nicht, dass sie in einer dynamischen Sprache machbar sind

Ist GDS aber dynamisch? Erweiterungsmethoden könnten auf nur typisierte Instanzen beschränkt werden und würden genauso funktionieren wie in anderen Sprachen - nur ein syntaktischer Zucker während der Kompilierung, der den Methodenaufruf durch den statischen Methoden- (Funktions-) Aufruf ersetzt. Ehrlich gesagt würde ich Pimps (ext. Methoden) vor Prototypen ala JS oder anderen dynamischen Methoden zum Anhängen von Methoden an Klassen oder sogar nur Instanzen bevorzugen.

Wofür wir uns auch entscheiden, ich hoffe, wir entscheiden uns nicht dafür, es "Zuhälter" zu nennen.

Ist GDS aber dynamisch?

In diesem Zusammenhang gibt es viele Definitionen von "dynamisch". Um es klar zu sagen: Der Variablentyp ist zum Zeitpunkt der Kompilierung möglicherweise nicht bekannt, daher muss die Überprüfung der Methodenerweiterung zur Laufzeit durchgeführt werden (was die Leistung auf die eine oder andere Weise beeinträchtigt).

Erweiterungsmethoden könnten nur auf typisierte Instanzen beschränkt werden

Wenn wir damit beginnen, können wir genauso gut GDScript nur typisiert erstellen. Aber das ist eine ganz andere Diskussion, die ich hier nicht führen möchte.

Punkt ist: Dinge sollten nicht anfangen oder aufhören zu funktionieren, weil der Benutzer Typen zu einem Skript hinzugefügt hat. Es ist fast immer verwirrend, wenn es passiert.

Auch hier glaube ich nicht, dass es das Problem sowieso löst. Wir versuchen, denselben Code an mehrere Typen anzuhängen, während eine Erweiterungsmethode ihn nur einem Typ hinzufügt.

Ehrlich gesagt würde ich Pimps (ext. Methoden) vor Prototypen ala JS oder anderen dynamischen Methoden zum Anhängen von Methoden an Klassen oder sogar nur Instanzen bevorzugen.

Niemand hat (noch) vorgeschlagen, Methoden dynamisch (zur Laufzeit) anzuhängen, und das möchte ich auch nicht. Traits würden zur Kompilierzeit statisch angewendet.

Ich habe ursprünglich einen Kommentar zu Haxe und seiner Mixin-Makrobibliothek abgegeben, aber dann wurde mir klar, dass die meisten Benutzer sowieso keine Sprache von Drittanbietern verwenden werden.

Ich bin vor kurzem auf einen Bedarf dafür gestoßen.

Ich habe einige Objekte, mit denen der Benutzer interagieren kann, aber nicht denselben Elternteil teilen kann, aber sie benötigen ähnliche Gruppen von APIs

Zum Beispiel habe ich einige Klassen, die nicht von demselben Elternteil erben können, aber einen ähnlichen Satz von APIs verwenden:
Lager: Finanzen, Löschen, Mausinteraktion + andere
Fahrzeug: Finanzen, Löschen, Mausinteraktion + andere
VehicleTerminal: Finanzen, Löschen, Mausinteraktion + andere

Für die Finanzen habe ich die Komposition verwendet, da dies den wenigsten Boilerplate-Code erfordert, da get_finances_component() eine ausreichende API ist, da es sich überhaupt nicht um die Spielobjekte kümmert.

Die Anderen:
MouseInteraction and Delection Ich musste nur kopieren und einfügen, da es etwas über die Spielobjekte wissen muss, einige Kompositionen funktionieren hier nicht, es sei denn, ich habe eine seltsame Delegierung vorgenommen:

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

aber das erlaubt mir nicht wirklich zu überschreiben, wie delete funktioniert, wo ich, wenn es eine geerbte Klasse wäre, die Möglichkeit hätte, einen Teil des Löschcodes bei Bedarf neu zu schreiben.

Mausinteraktion und Löschung Ich musste nur kopieren und einfügen, da es etwas über die Spielobjekte wissen muss, einige Kompositionen funktionieren hier nicht, es sei denn, ich habe eine seltsame Delegierung vorgenommen

Ich greife derzeit über onready -Knoten auf Komponenten zu. Ich mache etwas ähnliches:

# character.gd

var input = $input # input component

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

Also das:

character.input.enabled = true

wird das:

character.focused = true

Wie @Calinou freundlicherweise darauf hingewiesen hat, hängt mein Problem https://github.com/godotengine/godot-proposals/issues/758 eng damit zusammen. Was hältst du von dem Vorschlag, einer Gruppe ein Merkmal hinzufügen zu können? Dies könnte den Bedarf an Skripten und anderem Overhead drastisch verringern.

Es wäre einfach großartig, eine Möglichkeit zu haben, gemeinsam nutzbaren Code in Klassen einzufügen, und wenn sie exportierte Werte haben, diese im Inspektor erscheinen zu lassen und die Methoden und Eigenschaften verfügbar zu haben und durch die Codevervollständigung erkannt zu werden.

Funktions- und Verbesserungsvorschläge für die Godot Engine werden nun in einem speziellen Godot Improvement Proposals (GIP) ( godotengine/godot-proposals ) Issue Tracker diskutiert und überprüft. Der GIP-Tracker verfügt über eine detaillierte Problemvorlage, die so konzipiert ist, dass Vorschläge alle relevanten Informationen enthalten, um eine produktive Diskussion zu beginnen und der Community zu helfen, die Gültigkeit des Vorschlags für die Engine zu bewerten.

Der Haupt-Tracker ( godotengine/godot ) ist jetzt ausschließlich für Fehlerberichte und Pull-Requests bestimmt, sodass sich die Mitwirkenden besser auf die Fehlerbehebungsarbeit konzentrieren können. Daher schließen wir jetzt alle älteren Feature-Vorschläge im Hauptproblem-Tracker.

Wenn Sie an diesem Funktionsvorschlag interessiert sind, öffnen Sie bitte einen neuen Vorschlag im GIP-Tracker , indem Sie der angegebenen Problemvorlage folgen (nachdem Sie überprüft haben, dass sie noch nicht existiert). Stellen Sie sicher, dass Sie auf dieses geschlossene Thema verweisen, wenn es relevante Diskussionen enthält (die Sie auch im neuen Vorschlag zusammenfassen sollten). Vielen Dank im Voraus!

Hinweis: Dies ist ein beliebter Vorschlag, wenn jemand ihn zu Godot-Vorschlägen verschiebt, versuchen Sie bitte auch, die Diskussion zusammenzufassen.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen