Godot 接口
脚本常常需要依赖其他对象来获取功能。这个过程分为两部分:
获取对可能具有这些功能的对象的引用。
从对象访问数据或逻辑。
接下来,本教程将介绍多种方法来完成这些操作。
获取对象引用
对所有 Object 来说,获得引用的最基础的方法,是通过另一个已获得引用的对象。
GDScriptC#
var obj = node.object # Property access.
var obj = node.get_object() # Method access.
GodotObject obj = node.Object; // Property access.
GodotObject obj = node.GetObject(); // Method access.
同样的方法也适用于 RefCounted 对象。虽然使用者经常以这种方式访问 Node 和 Resource,但还是有一些其他方法可以使用。
除了访问属性和方法,也可以通过加载来获得 Resource。
GDScriptC#
# If you 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.
# The `@tool` annotation must be placed at the top of the script.
@tool
# Load resource during scene load.
var preres = preload(path)
# Load resource when program reaches statement.
var res = load(path)
# Note that users load scenes and scripts, by convention, with PascalCase
# names (like typenames), often into constants.
const MyScene = preload("my_scene.tscn") # Static load
const MyScript = preload("my_script.gd")
# This type's value varies, i.e. it is a variable, so it uses snake_case.
@export var script_type: Script
# Must configure from the editor, defaults to null.
@export var const_script: Script:
set(value):
if Engine.is_editor_hint():
const_script = value
# Warn users if the value hasn't been set.
func _get_configuration_warnings():
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>("res://Path/To/MyScript.cs");
// Initialize with same value. Value cannot be changed.
public readonly Script MyConstScript = GD.Load<Script>("res://Path/To/MyScript.cs");
// Like 'readonly' due to inaccessible setter.
// But, value can be set during constructor, i.e. MyType().
public Script MyNoSetScript { get; } = GD.Load<Script>("res://Path/To/MyScript.cs");
// 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[] _GetConfigurationWarnings()
{
if (EnemyScn == null)
{
return new string[] { "Must initialize property 'EnemyScn'." };
}
return Array.Empty<string>();
}
}
请注意以下几点:
在一种语言中,有许多加载这些资源的方法。
在设计对象如何访问数据时,不要忘记,还可以将资源作为引用传递。
请记住,加载资源时只会获取引擎维护的缓存资源实例。如果要获取一个新对象,必须 复制 一个现有引用,或者使用
new()
从头实例化一个对象。
节点同样也有另一种访问方式:场景树。
GDScriptC#
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` annotation 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)
# Fastest. Doesn't break if node is moved in the Scene tree dock.
# Node must be selected in the inspector as it's an exported property.
@export var child: Node
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`s.
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())
using Godot;
using System;
using System.Diagnostics;
public class MyNode : Node
{
// Slow
public void DynamicLookupWithDynamicNodePath()
{
GD.Print(GetNode("Child"));
}
// Fastest. Lookup node and cache for future access.
// Doesn't break if node moves later.
private Node _child;
public void _Ready()
{
_child = GetNode("Child");
}
public void LookupAndCacheForFutureAccess()
{
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 { get; set; }
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 with an exception.
if (prop == null)
{
throw new InvalidOperationException("'Prop' wasn't initialized.");
}
// Fail and terminate.
// Note: Scripts run from a release export template don't run `Debug.Assert`s.
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()
{
MyNode globals = GetNode<MyNode>("/root/Globals");
GD.Print(globals);
GD.Print(globals.Prop);
GD.Print(globals.MyGetter());
}
};
从对象访问数据或逻辑
Godot 的脚本 API 是鸭子类型(duck-typed)的。这意味着,当脚本执行某项操作时,Godot 不会通过 类型 来验证其是否支持该操作。相反,它会检查对象是否 实现 了这个被调用的方法。
实际上,脚本 API 公开的所有属性,都是绑定到名称的 setter
和 getter
对。以 CanvasItem 类的 visible
属性为例,如果有人试图访问 CanvasItem.visible,那么 Godot 会按照以下顺序进行检查:
如果对象附加了脚本,它将尝试通过脚本设置属性。这使得脚本有机会通过覆盖属性的
setter
方法来覆盖在基础对象上定义的属性。如果脚本没有该属性, 它在
ClassDB
中对CanvasItem
类及其所有继承的类型执行visible
属性的哈希表查找. 如果找到, 它将调用绑定的setter
或getter
. 有关哈希表的更多信息, 参见 数据偏好 文档.如果没有找到, 它会进行显式检查, 以查看用户是否要访问
script
或meta
属性.如果没有, 它将在
CanvasItem
及其继承的类型中检查_set
/_get
实现(取决于访问类型). 这些方法可以执行逻辑, 从而给人一种对象具有属性的印象._get_property_list
方法也是如此.- 请注意,即使对于不合法的符号名称也会发生这种情况,例如以数字开头或包含斜杠(/)的名称。
因此,这个鸭子类型的系统可以在脚本、对象的类,或对象继承的任何类中定位属性,但仅限于扩展 Object 的对象。
Godot 提供了多种选项,来对这些访问执行运行时检查:
鸭子类型属性的访问。Godot 将像上文所述的那样对它进行属性检查。如果对象不支持该操作,则执行将停止。
GDScriptC#
# 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.
方法检查。在 CanvasItem.visible 的例子中,我们可以像访问任何其他方法一样,访问
set_visible
和is_visible
。GDScriptC#
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.
将访问权限外包给 Callable。 在人们需要最大程度地摆脱依赖的情况下,这些方法可能会很有用。 在这种情况下,需要依赖外部上下文来设置该方法。
GDScriptC#
# child.gd
extends Node
var fn = null
func my_method():
if fn:
fn.call()
# parent.gd
extends Node
@onready var child = $Child
func _ready():
child.fn = print_me
child.my_method()
func print_me():
print(name)
// Child.cs
using Godot;
public partial class Child : Node
{
public Callable? Callable { get; set; }
public void MyMethod()
{
Callable?.Call();
}
}
// Parent.cs
using Godot;
public partial class Parent : Node
{
private Child _child;
public void _Ready()
{
_child = GetNode<Child>("Child");
_child.Callable = Callable.From(PrintMe);
_child.MyMethod();
}
public void PrintMe()
{
GD.Print(Name);
}
}
这些策略有助于Godot的灵活设计. 通过它们, 用户可以使用多种工具来满足他们的特定需求.