Godot 接口
我们常常需要依赖其他对象来获取功能的脚本。这个过程分为两部分:
获取对可能具有这些功能的对象的引用。
从对象访问数据或逻辑。
本教程接下来的部分将介绍各种完成这些操作的方法。
获取对象引用
对所有 Object 来说,获得引用的最基础的方法,是从另一个已获得引用的对象来获得。
GDScriptC#
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.
同样的原则也适用于 Reference 对象。虽然使用者经常以这种方式访问 Node 和 Resource,但还有另一种方法。
除了访问属性和方法,也可以通过加载来获得 Resource。
GDScriptC#
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 "";
}
}
请注意以下几点:
在一种语言中,有许多加载这些资源的方法。
在设计对象如何访问数据时,不要忘记,还可以将资源作为引用传递。
请记住, 加载资源将获取引擎维护的缓存资源实例. 要获取一个新对象, 必须 复制 一个现有引用, 或者使用
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` 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
public void DynamicLookupWithDynamicNodePath()
{
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 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;
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());
}
};
从对象访问数据或逻辑
Godot的脚本API是鸭子类型. 这意味着, 如果脚本执行操作, 则Godot不会通过 类型 验证其是否支持该操作. 相反, 它检查对象是否 实现 了单个方法.
例如, CanvasItem 类有一个 visible
的属性. 实际上, 脚本API公开的所有属性, 都是绑定到名称的 setter
和 getter
对. 如果有人试图访问 CanvasItem.visible, 那么Godot会按照以下顺序进行检查:
如果对象附加了脚本, 它将尝试通过脚本设置属性. 通过覆盖属性的
setter
方法, 这为脚本提供了覆盖基础对象上定义的属性的机会.如果脚本没有该属性, 它在
ClassDB
中对CanvasItem
类及其所有继承的类型执行visible
属性的哈希表查找. 如果找到, 它将调用绑定的setter
或getter
. 有关哈希表的更多信息, 参见 数据偏好 文档.如果没有找到, 它会进行显式检查, 以查看用户是否要访问
script
或meta
属性.如果没有, 它将在
CanvasItem
及其继承的类型中检查_set
/_get
实现(取决于访问类型). 这些方法可以执行逻辑, 从而给人一种对象具有属性的印象._get_property_list
方法也是如此.- 请注意, 即使对于非合法的符号名称, 例如 TileSet 的
1/tile_name
属性, 这种情况也会发生. 这是指ID为1的tile
的名称, 即 TileSet.tile_get_name(1).
- 请注意, 即使对于非合法的符号名称, 例如 TileSet 的
因此, 这个鸭子类型的系统可以在脚本, 对象的类, 或对象继承的任何类, 但只能用于扩展Object的对象中, 定位属性.
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.
将访问权限外包给 FuncRef. 在人们需要最大程度地摆脱依赖的情况下, 这些方法可能会很有用. 在这种情况下, 需要依靠外部上下文来设置此方法.
GDScriptC#
# 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());
}
}
这些策略有助于Godot的灵活设计. 通过它们, 用户可以使用多种工具来满足他们的特定需求.