Godot 介面

我們常常會需要為了一些功能而讓腳本仰賴其他物件。這個過程可以分為兩個部分:

  1. 取得可能有該功能的物件之參照。

  2. 從該物件存取資料或邏輯。

本篇教學接下來的部分將介紹各種達成此目的的方法。

取得物件參照

對於所有 Object <class_Object ,取得現有物件參照最基礎的方法,就是從另一個已經取得的實體來取得。

var obj = node.object # Property access.
var obj = node.get_object() # Method access.

相同的原則也可以用在 Reference 物件上。雖然使用者通常會用這種方式來存取 NodeResource ,但也有另一種方法。

除了存取屬性或方法,也可以通過 load 存取來取得 Resource。

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

請注意下列事項:

  1. 在一種語言當中,有許多載入這種 Resource 的方法。

  2. 在設計物件如何存取資料時,請不要忘記,我們也可用參照來傳遞資源。

  3. 請記得,載入資源會載入由引擎快取的資源實體。若要取得新物件,則必須要 複製 現有參照,或用 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` 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())

從物件中存取資料或邏輯

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 方法也是這樣。

    • 請注意,在非法符號名稱時也會發生這個檢查,像是在 TileSet 的「1/tile_name」屬性。這個屬性代表名稱為 ID 1 的圖塊,即 TileSet.tile_get_name(1)

最後,這個鴨子型別系統可以在腳本、物件類別或任何物件繼承的類別中找到屬性,但只適用於繼承了 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.
    
  • 方法檢查。在 CanvasItem.visible 的情況下,我們可以像其他方法一樣存取 set_visibleis_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.
    
  • 將存取外包給 FuncRef 。這些方法適用於我們需要最大限度地跳脫相依性。這時,便需要仰賴外部脈絡來設定方法。

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

多虧這些策略,Godot 才有靈活的設計。有了這些策略,使用者便有了豐富的工具來滿足特殊需求。