Work in progress

The content of this page was not yet updated for Godot 4.4 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

使用伺服器進行最佳化

像 Godot 這樣的引擎,因為高階的架構與功能,讓使用上更為方便。大多數功能都是透過 場景系統 來存取與使用。利用節點和資源可簡化大型遊戲的專案結構與資產管理。

當然,也有一些缺點:

  • 會增加一層額外的複雜度。

  • 效能會比直接使用底層 API 來得低。

  • 無法直接利用多執行緒來控制它們。

  • 需要消耗更多記憶體。

在多數情況下這不算太大問題(Godot 本身已高度最佳化,大部分操作也都是透過訊號處理,無需反覆輪詢)。但在某些情境下就會成為瓶頸,例如同時需要在每一影格處理數萬個實例時。

這種情況有時會讓開發者懷念手寫底層程式碼的時光,甚至後悔選擇了遊戲引擎,希望能回到更手工、低階的遊戲實作方式。

不過,Godot 的設計其實能解決這類問題。

也參考

你可以參考 Bullet Shower 範例專案 來觀察如何實際運用底層伺服器 API

伺服器

Godot 最有趣的設計決策之一,就是整個場景系統其實是*可選*的。雖然目前沒辦法在編譯時完全移除,但你完全可以選擇不使用它。

Godot 核心採用『伺服器』的概念。這些伺服器提供非常底層的 API,負責控制算繪、物理、音訊等功能。場景系統就是建立在這些伺服器之上。最常見的伺服器有:

你可以研究這些伺服器的 API,會發現它們所提供的功能,其實就是 Godot 所有高階操作的底層實作。

RID

要使用伺服器,最重要的是理解『Resource ID』(RID)物件。這些 RID 就是對伺服器內部資源的控制碼,需要手動分配與釋放。幾乎所有伺服器函式都需要透過 RID 來存取實際資源。

大多數 Godot 節點與資源內部都持有來自伺服器的 RID,可以透過不同函式取得。其實所有繼承自 Resource 的物件,都可以直接轉換為 RID。但並非所有資源都有 RID,這時 RID 會是空的。不過你仍然可以將這些資源作為 RID 傳給伺服器 API。

警告

資源是引用計數的(參見 RefCounted),但指向資源 RID 的引用*不會*被計入引用數。請務必在伺服器之外保留對資源本身的參考,否則資源與其 RID 都會被釋放。

對於節點,有許多函式可以取得相關 RID:

建議你多嘗試探索熟悉的節點與資源,看看能從哪些函式取得對應的伺服器 RID

不建議直接操作已經關聯節點的物件的 RID。要建立或控制新的資源,或與現有資源互動,應該都直接使用伺服器函式來處理。

建立 Sprite

以下範例展示如何用程式碼建立一個精靈(Sprite),並透過底層 CanvasItem API 來移動它。

extends Node2D


# RenderingServer expects references to be kept around.
var texture


func _ready():
    # Create a canvas item, child of this node.
    var ci_rid = RenderingServer.canvas_item_create()
    # Make this node the parent.
    RenderingServer.canvas_item_set_parent(ci_rid, get_canvas_item())
    # Draw a texture on it.
    # Remember, keep this reference.
    texture = load("res://my_texture.png")
    # Add it, centered.
    RenderingServer.canvas_item_add_texture_rect(ci_rid, Rect2(-texture.get_size() / 2, texture.get_size()), texture)
    # Add the item, rotated 45 degrees and translated.
    var xform = Transform2D().rotated(deg_to_rad(45)).translated(Vector2(20, 30))
    RenderingServer.canvas_item_set_transform(ci_rid, xform)

伺服器中的 CanvasItem API 允許你向其新增繪圖基本圖元。一旦新增後無法修改,如需變更必須先清除再重新新增(但設定 transform 則不受此限制,可任意次數變更)。

清除圖元方式如下:

RenderingServer.canvas_item_clear(ci_rid)

在 3D 空間中產生網格實體

3D 的 API 與 2D 不同,因此必須使用專屬的產生實體 API。

extends Node3D


# RenderingServer expects references to be kept around.
var mesh


func _ready():
    # Create a visual instance (for 3D).
    var instance = RenderingServer.instance_create()
    # Set the scenario from the world, this ensures it
    # appears with the same objects as the scene.
    var scenario = get_world_3d().scenario
    RenderingServer.instance_set_scenario(instance, scenario)
    # Add a mesh to it.
    # Remember, keep the reference.
    mesh = load("res://mymesh.obj")
    RenderingServer.instance_set_base(instance, mesh)
    # Move the mesh around.
    var xform = Transform3D(Basis(), Vector3(20, 100, 0))
    RenderingServer.instance_set_transform(instance, xform)

建立 2D 剛體並用它移動精靈

這會透過 PhysicsServer2D API 建立一個 RigidBody2D,並在剛體移動時同步移動一個 CanvasItem

# Physics2DServer expects references to be kept around.
var body
var shape


func _body_moved(state, index):
    # Created your own canvas item, use it here.
    RenderingServer.canvas_item_set_transform(canvas_item, state.transform)


func _ready():
    # Create the body.
    body = Physics2DServer.body_create()
    Physics2DServer.body_set_mode(body, Physics2DServer.BODY_MODE_RIGID)
    # Add a shape.
    shape = Physics2DServer.rectangle_shape_create()
    # Set rectangle extents.
    Physics2DServer.shape_set_data(shape, Vector2(10, 10))
    # Make sure to keep the shape reference!
    Physics2DServer.body_add_shape(body, shape)
    # Set space, so it collides in the same space as current scene.
    Physics2DServer.body_set_space(body, get_world_2d().space)
    # Move initial position.
    Physics2DServer.body_set_state(body, Physics2DServer.BODY_STATE_TRANSFORM, Transform2D(0, Vector2(10, 20)))
    # Add the transform callback, when body moves
    # The last parameter is optional, can be used as index
    # if you have many bodies and a single callback.
    Physics2DServer.body_set_force_integration_callback(body, self, "_body_moved", 0)

3D 版本也大同小異,因為 2D 與 3D 物理伺服器的用法幾乎一致(分別使用 RigidBody3DPhysicsServer3D)。

從伺服器取得資料

除非你非常確定自己在做什麼,否則**千萬不要**直接呼叫 RenderingServerPhysicsServer2DPhysicsServer3D 來請求資料。這些伺服器通常為了效能是非同步執行的,任何會回傳數值的函式呼叫都會造成伺服器暫停,強制等待所有待處理事項完成再回傳結果。如果你每一幀都這樣呼叫,效能會大幅下降,而且原因可能不明顯。

因此,這類伺服器的大多數 API 設計時,通常根本不允許直接回傳資訊,除非是要取得可儲存的實際資料。