Godot Schnittstellen

Oft benötigt man Skripte, deren Funktionen auf anderen Objekten basieren. Dieser Prozess besteht aus 2 Teilen:

  1. Erfassen einer Referenz auf das Objekt, das vermutlich die Merkmale aufweist.

  2. Zugriff auf die Daten oder die Logik des Objekts.

Der Rest dieser Anleitung beschreibt die verschiedenen Möglichkeiten, all dies zu tun.

Objektreferenzen erfassen

Für alle Objekte besteht die grundlegendste Art der Referenzierung darin, einen Verweis auf ein vorhandenes Objekt von einer anderen erfassten Instanz abzurufen.

var obj = node.object # Property access.
var obj = node.get_object() # Method access.
Object obj = node.Object; // Property access.
Object obj = node.GetObject(); // Method access.

Das gleiche Prinzip gilt für Referenz Objekte. Während Benutzer häufig auf diese Weise auf Node und Ressource zugreifen, stehen alternative Maßnahmen zur Verfügung.

Anstelle von Eigenschafts- oder Methodenzugriff können Ressourcen durch Ladezugriff abgerufen werden.

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

# Note that users load scenes and scripts, by convention, with PascalCase
# names (like typenames), often into constants.
const MyScene : = preload("my_scene.tscn") as PackedScene # Static load
const MyScript : = preload("my_script.gd") as Script

# This type's value varies, i.e. it is a variable, so it uses snake_case.
export(Script) var script_type: Script

# If need an "export const var" (which doesn't exist), use a conditional
# setter for a tool script that checks if it's executing in the editor.
tool # Must place at top of file.

# Must configure from the editor, defaults to null.
export(Script) var const_script setget set_const_script
func set_const_script(value):
    if Engine.is_editor_hint():
        const_script = value

# Warn users if the value hasn't been set.
func _get_configuration_warning():
    if not const_script:
        return "Must initialize property 'const_script'."
    return ""
// Tool script added for the sake of the "const [Export]" example.
[Tool]
public MyType
{
    // Property initializations load during Script instancing, i.e. .new().
    // No "preload" loads during scene load exists in C#.

    // Initialize with a value. Editable at runtime.
    public Script MyScript = GD.Load<Script>("MyScript.cs");

    // Initialize with same value. Value cannot be changed.
    public readonly Script MyConstScript = GD.Load<Script>("MyScript.cs");

    // Like 'readonly' due to inaccessible setter.
    // But, value can be set during constructor, i.e. MyType().
    public Script Library { get; } = GD.Load<Script>("res://addons/plugin/library.gd");

    // If need a "const [Export]" (which doesn't exist), use a
    // conditional setter for a tool script that checks if it's executing
    // in the editor.
    private PackedScene _enemyScn;

    [Export]
    public PackedScene EnemyScn
    {
        get { return _enemyScn; }
        set
        {
            if (Engine.IsEditorHint())
            {
                _enemyScn = value;
            }
        }
    };

    // Warn users if the value hasn't been set.
    public String _GetConfigurationWarning()
    {
        if (EnemyScn == null)
            return "Must initialize property 'EnemyScn'.";
        return "";
    }
}

Beachten Sie folgendes:

  1. Es gibt viele Möglichkeiten, wie eine Sprache solche Ressourcen laden kann.

  2. Vergessen Sie nicht beim Entwerfen, wie Objekte auf Daten zugreifen, dass Ressourcen auch als Referenz weitergegeben werden können.

  3. Beachten Sie, dass beim Laden einer Ressource die von der Engine verwaltete zwischengespeicherte Ressourceninstanz abgerufen wird. Um ein neues Objekt zu erhalten, muss man eine vorhandene Referenz duplizieren oder eine von Grund auf mit new () instanziieren.

Nodes haben ebenfalls einen alternativen Zugangspunkt: den Szenenbaum.

extends Node

# Slow.
func dynamic_lookup_with_dynamic_nodepath():
    print(get_node("Child"))

# Faster. GDScript only.
func dynamic_lookup_with_cached_nodepath():
    print($Child)

# Fastest. Doesn't break if node moves later.
# Note that `onready` keyword is GDScript only.
# Other languages must do...
#     var child
#     func _ready():
#         child = get_node("Child")
onready var child = $Child
func lookup_and_cache_for_future_access():
    print(child)

# Delegate reference assignment to an external source.
# Con: need to perform a validation check.
# Pro: node makes no requirements of its external structure.
#      'prop' can come from anywhere.
var prop
func call_me_after_prop_is_initialized_by_parent():
    # Validate prop in one of three ways.

    # Fail with no notification.
    if not prop:
        return

    # Fail with an error message.
    if not prop:
        printerr("'prop' wasn't initialized")
        return

    # Fail and terminate.
    # Note: Scripts run from a release export template don't
    # run `assert` statements.
    assert(prop, "'prop' wasn't initialized")

# Use an autoload.
# Dangerous for typical nodes, but useful for true singleton nodes
# that manage their own data and don't interfere with other objects.
func reference_a_global_autoloaded_variable():
    print(globals)
    print(globals.prop)
    print(globals.my_getter())
public class MyNode
{
    // Slow, dynamic lookup with dynamic NodePath.
    public void Method1()
    {
        GD.Print(GetNode(NodePath("Child")));
    }

    // Fastest. Lookup node and cache for future access.
    // Doesn't break if node moves later.
    public Node Child;
    public void _Ready()
    {
        Child = GetNode(NodePath("Child"));
    }
    public void Method2()
    {
        GD.Print(Child);
    }

    // Delegate reference assignment to an external source.
    // Con: need to perform a validation check.
    // Pro: node makes no requirements of its external structure.
    //      'prop' can come from anywhere.
    public object Prop;
    public void CallMeAfterPropIsInitializedByParent()
    {
        // Validate prop in one of three ways.

        // Fail with no notification.
        if (prop == null)
        {
            return;
        }

        // Fail with an error message.
        if (prop == null)
        {
            GD.PrintErr("'Prop' wasn't initialized");
            return;
        }

        // Fail and terminate.
        Debug.Assert(Prop, "'Prop' wasn't initialized");
    }

    // Use an autoload.
    // Dangerous for typical nodes, but useful for true singleton nodes
    // that manage their own data and don't interfere with other objects.
    public void ReferenceAGlobalAutoloadedVariable()
    {
        Node globals = GetNode(NodePath("/root/Globals"));
        GD.Print(globals);
        GD.Print(globals.prop);
        GD.Print(globals.my_getter());
    }
};

Zugriff auf Daten oder Logik von einem Objekt aus

Die Skript-API von Godot ist "duck-typed". Dies bedeutet, dass Godot, wenn ein Skript eine Operation ausführt, nicht überprüft, ob es die Operation nach Typ unterstützt. Stattdessen wird überprüft, ob das Objekt die einzelne Methode implementiert.

Beispielsweise hat die Klasse CanvasItem eine Eigenschaft visible. Alle Eigenschaften, die für die Skript-API verfügbar gemacht werden, sind tatsächlich ein Setter- und Getter-Paar, das an einen Namen gebunden ist. Wenn man versuchen würde, auf CanvasItem.visible zuzugreifen, würde Godot die folgenden Überprüfungen der Reihe nach durchführen:

  • Wenn an das Objekt ein Skript angehängt ist, wird versucht, die Eigenschaft über das Skript festzulegen. Dies eröffnet Skripten die Möglichkeit, eine für ein Basisobjekt definierte Eigenschaft zu überschreiben, indem die Setter-Methode für die Eigenschaft überschrieben wird.

  • Wenn das Skript nicht über die Eigenschaft verfügt, führt es in der ClassDB eine HashMap-Suche nach der "sichtbaren" Eigenschaft für die CanvasItem-Klasse und alle ihre geerbten Typen durch. Wenn es gefunden wird, wird der gebundene Setter oder Getter aufgerufen. Weitere Informationen zu HashMaps finden Sie in der Dokumentation Dateneinstellungen.

  • Wenn es nicht gefunden wird, wird explizit überprüft, ob der Benutzer auf die Eigenschaften "script" oder "meta" zugreifen möchte.

  • Wenn nicht, wird im CanvasItem und seinen geerbten Typen nach einer _set / _get-Implementierung (abhängig von der Art des Zugriffs) gesucht. Diese Methoden können Logik ausführen, die den Eindruck erweckt, dass das Objekt eine Eigenschaft hat. Dies ist auch bei der Methode _get_property_list der Fall.

    • Beachten Sie, dass dies auch für nicht legale Symbolnamen geschieht, wie im Fall von: ref:TileSet <class_TileSet>s Eigenschaft "1/tile_name". Dies bezieht sich auf den Namen der Kachel mit der ID 1, z. B. TileSet.tile_get_name(1).

Infolgedessen kann dieses "duck-typed"-System eine Eigenschaft entweder im Skript, in der Klasse des Objekts oder in einer beliebigen Klasse, die das Objekt erbt, finden, jedoch nur für Dinge, die "Objekt" erweitern.

Godot bietet eine Vielzahl von Optionen für die Durchführung von Laufzeitprüfungen für diese Zugriffe:

  • Ein duck-typed Eigenschaftszugriff. Diese werden die Eigenschaften überprüfen (wie oben beschrieben). Wenn der Vorgang vom Objekt nicht unterstützt wird, wird die Ausführung angehalten.

    # All Objects have duck-typed get, set, and call wrapper methods.
    get_parent().set("visible", false)
    
    # Using a symbol accessor, rather than a string in the method call,
    # will implicitly call the `set` method which, in turn, calls the
    # setter method bound to the property through the property lookup
    # sequence.
    get_parent().visible = false
    
    # Note that if one defines a _set and _get that describe a property's
    # existence, but the property isn't recognized in any _get_property_list
    # method, then the set() and get() methods will work, but the symbol
    # access will claim it can't find the property.
    
    // All Objects have duck-typed Get, Set, and Call wrapper methods.
    GetParent().Set("visible", false);
    
    // C# is a static language, so it has no dynamic symbol access, e.g.
    // `GetParent().Visible = false` won't work.
    
  • Eine Methodenprüfung. Im Fall von CanvasItem.visible kann man wie bei jeder anderen Methode auf die Methoden set_visible und is_visible zugreifen.

    var child = get_child(0)
    
    # Dynamic lookup.
    child.call("set_visible", false)
    
    # Symbol-based dynamic lookup.
    # GDScript aliases this into a 'call' method behind the scenes.
    child.set_visible(false)
    
    # Dynamic lookup, checks for method existence first.
    if child.has_method("set_visible"):
        child.set_visible(false)
    
    # Cast check, followed by dynamic lookup.
    # Useful when you make multiple "safe" calls knowing that the class
    # implements them all. No need for repeated checks.
    # Tricky if one executes a cast check for a user-defined type as it
    # forces more dependencies.
    if child is CanvasItem:
        child.set_visible(false)
        child.show_on_top = true
    
    # If one does not wish to fail these checks without notifying users,
    # one can use an assert instead. These will trigger runtime errors
    # immediately if not true.
    assert(child.has_method("set_visible"))
    assert(child.is_in_group("offer"))
    assert(child is CanvasItem)
    
    # Can also use object labels to imply an interface, i.e. assume it
    # implements certain methods.
    # There are two types, both of which only exist for Nodes: Names and
    # Groups.
    
    # Assuming...
    # A "Quest" object exists and 1) that it can "complete" or "fail" and
    # that it will have text available before and after each state...
    
    # 1. Use a name.
    var quest = $Quest
    print(quest.text)
    quest.complete() # or quest.fail()
    print(quest.text) # implied new text content
    
    # 2. Use a group.
    for a_child in get_children():
        if a_child.is_in_group("quest"):
            print(quest.text)
            quest.complete() # or quest.fail()
            print(quest.text) # implied new text content
    
    # Note that these interfaces are project-specific conventions the team
    # defines (which means documentation! But maybe worth it?).
    # Any script that conforms to the documented "interface" of the name or
    # group can fill in for it.
    
    Node child = GetChild(0);
    
    // Dynamic lookup.
    child.Call("SetVisible", false);
    
    // Dynamic lookup, checks for method existence first.
    if (child.HasMethod("SetVisible"))
    {
        child.Call("SetVisible", false);
    }
    
    // Use a group as if it were an "interface", i.e. assume it implements
    // certain methods.
    // Requires good documentation for the project to keep it reliable
    // (unless you make editor tools to enforce it at editor time).
    // Note, this is generally not as good as using an actual interface in
    // C#, but you can't set C# interfaces from the editor since they are
    // language-level features.
    if (child.IsInGroup("Offer"))
    {
        child.Call("Accept");
        child.Call("Reject");
    }
    
    // Cast check, followed by static lookup.
    CanvasItem ci = GetParent() as CanvasItem;
    if (ci != null)
    {
        ci.SetVisible(false);
    
        // useful when you need to make multiple safe calls to the class
        ci.ShowOnTop = true;
    }
    
    // If one does not wish to fail these checks without notifying users,
    // one can use an assert instead. These will trigger runtime errors
    // immediately if not true.
    Debug.Assert(child.HasMethod("set_visible"));
    Debug.Assert(child.IsInGroup("offer"));
    Debug.Assert(CanvasItem.InstanceHas(child));
    
    // Can also use object labels to imply an interface, i.e. assume it
    // implements certain methods.
    // There are two types, both of which only exist for Nodes: Names and
    // Groups.
    
    // Assuming...
    // A "Quest" object exists and 1) that it can "Complete" or "Fail" and
    // that it will have Text available before and after each state...
    
    // 1. Use a name.
    Node quest = GetNode("Quest");
    GD.Print(quest.Get("Text"));
    quest.Call("Complete"); // or "Fail".
    GD.Print(quest.Get("Text")); // Implied new text content.
    
    // 2. Use a group.
    foreach (Node AChild in GetChildren())
    {
        if (AChild.IsInGroup("quest"))
        {
          GD.Print(quest.Get("Text"));
          quest.Call("Complete"); // or "Fail".
          GD.Print(quest.Get("Text")); // Implied new text content.
        }
    }
    
    // Note that these interfaces are project-specific conventions the team
    // defines (which means documentation! But maybe worth it?).
    // Any script that conforms to the documented "interface" of the
    // name or group can fill in for it. Also note that in C#, these methods
    // will be slower than static accesses with traditional interfaces.
    
  • Auslagern des Zugriffs auf FuncRef. Dies können in Fällen nützlich sein, in denen ein Höchstmaß an Freiheit von Abhängigkeiten erforderlich ist. In diesem Fall stützt man sich auf einen externen Kontext, um die Methode einzurichten.

# child.gd
extends Node
var fn = null

func my_method():
    if fn:
        fn.call_func()

# parent.gd
extends Node

onready var child = $Child

func _ready():
    child.fn = funcref(self, "print_me")
    child.my_method()

func print_me():
    print(name)
// Child.cs
public class Child : Node
{
    public FuncRef FN = null;

    public void MyMethod()
    {
        Debug.Assert(FN != null);
        FN.CallFunc();
    }
}

// Parent.cs
public class Parent : Node
{
    public Node Child;

    public void _Ready()
    {
        Child = GetNode("Child");
        Child.Set("FN", GD.FuncRef(this, "PrintMe"));
        Child.MyMethod();
    }

    public void PrintMe() {
    {
        GD.Print(GetClass());
    }
}

Diese Strategien tragen zu Godots flexiblem Design bei. Die Benutzer verfügen über eine Vielzahl von Werkzeugen, um ihre spezifischen Anforderungen zu erfüllen.