Up to date
This page is up to date for Godot 4.2
.
If you still find outdated information, please open an issue.
Godot 介面¶
我們常常會需要為了一些功能而讓腳本仰賴其他物件。這個過程可以分為兩個部分:
取得可能有該功能的物件之參照。
從該物件存取資料或邏輯。
本篇教學接下來的部分將介紹各種達成此目的的方法。
取得物件參照¶
對於所有 Object <class_Object ,取得現有物件參照最基礎的方法,就是從另一個已經取得的實體來取得。
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.
相同的原則也可以用在 Reference 物件上。雖然使用者通常會用這種方式來存取 Node 與 Resource ,但也有另一種方法。
除了存取屬性或方法,也可以通過 load 存取來取得 Resource。
# 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>();
}
}
請注意下列事項:
在一種語言當中,有許多載入這種 Resource 的方法。
在設計物件如何存取資料時,請不要忘記,我們也可用參照來傳遞資源。
請記得,載入資源會載入由引擎快取的資源實體。若要取得新物件,則必須要 複製 現有參照,或用
new()
來重新實體化資源。
Node 也有另一個替代的存取點:SceneTree 場景樹。
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 是鴨子型別的。這代表,當腳本執行某樣操作時,Godot 不會用 型別 來驗證該物件是否支援該操作,而是檢查該物件是否有 實作 個別方法。
舉例來說, CanvasItem 類別有一個 visible
屬性。所有暴露在腳本 API 中的屬性,事實上都是繫結在名稱上的 Setter 與 Getter 配對。當我們嘗試存取 CanvasItem.visible 時,Godot 會照順序進行下列檢查:
若物件有附加腳本,則會嘗試在腳本中設定屬性。這樣就能讓腳本覆寫該屬性的 Setter 方法,讓腳本有覆寫基礎物件中定義的屬性的機會。
若腳本沒有該屬性,則會在 ClassDB 中通過 HashMap 查詢來找到 CanvasItem 類別與其繼承的所有型別中的「visible」屬性。若有找到,則會繫結 Setter 與 Getter。更多有關 HashMaps 的資訊,請參考 資料偏好 文件。
若找不到,則會明確地檢查使用者要存取的是不是「script」或「meta」屬性。
若不是的話,則會檢查 CanvasItem 與其繼承的型別中是否有實作
_set
與_get
(依據存取型別的不同)。這兩個方法可以執行一組邏輯,讓該物件看起來有這個屬性。同樣地,有_get_property_list
方法也是這樣。請注意,即使對於不合法的符號名稱也會發生這種情況,例如以數位開頭或包含斜杠(/)的名稱。
最後,這個鴨子型別系統可以在腳本、物件類別或任何物件繼承的類別中找到屬性,但只適用於繼承了 Object 的物件中。
Godot 提供了各種能在執行階段執行這些檢查的方法:
鴨子型別屬性存取。這種方法會進行屬性檢查 (如上所示)。若物件不支援該操作,則會終止執行。
# 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
方法。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 。這些方法適用於我們需要最大限度地跳脫相依性。這時,便需要仰賴外部脈路來設定方法。
# 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 才有靈活的設計。有了這些策略,使用者便有了豐富的工具來滿足特殊需求。