Up to date

This page is up to date for Godot 4.3. If you still find outdated information, please open an issue.

場景組織

This article covers topics related to the effective organization of scene content. Which nodes should you use? Where should you place them? How should they interact?

如何有效地建立關係

當 Godot 使用者開始製作自己的場景時,通常會遇到下列問題:

建立好第一個場景並往場景中加上內容,最後會開始覺得場景逐漸變得複雜,應該要把東西都切割成小塊,所以就將場景中的各個分支都保存為獨立的場景。但是,接著就會發現之前在腳本裡直接引用其他節點的方法不能用了。在多個地方重複使用場景也會出問題,因為沒辦法用節點路徑找到目標,然後在編輯器中建的訊號連接也會斷開。

To fix these problems, you must instantiate the sub-scenes without them requiring details about their environment. You need to be able to trust that the sub-scene will create itself without being picky about how it's used.

OOP 中要考慮的最重要的一點就是要維持一個專注且單一目的的類別,並與程式碼中其他部分 鬆散耦合 。這樣可以讓物件的大小維持得比較小 (為了可維護性) 並提高類別的可用性。

這些 OOP 最佳實踐在場景結構與腳本使用上實際是 數種 最佳實踐。

If at all possible, you should design scenes to have no dependencies. That is, you should create scenes that keep everything they need within themselves.

若場景必須要與外部的東西互動,則有經驗的工程師推薦使用 依賴注入 。這個技術是使用高等 API 來提動低等 API 的相依性。為什麼要這麼做?因為仰賴外部環境的類別可能會不小心觸發 Bug 或未預期的行為。

To do this, you must expose data and then rely on a parent context to initialize it:

  1. 連接訊號。非常安全,但訊號應該只用來「回覆」一個行為,而不是執行行為。請注意,訊號名稱通常使用過去式動詞,如「entered」、「skill_activated」或「item_collected」。

    # Parent
    $Child.signal_name.connect(method_on_the_object)
    
    # Child
    signal_name.emit() # Triggers parent-defined behavior.
    
  2. 呼叫方法。用於開始一個行為。

    # Parent
    $Child.method_name = "do"
    
    # Child, assuming it has String property 'method_name' and method 'do'.
    call(method_name) # Call parent-defined method (which child must own).
    
  3. 初始化 FuncRef 屬性。比使用方法來得安全,因為不需在意方法的所有權。用來開始行為。

    # Parent
    $Child.func_property = object_with_method.method_on_the_object
    
    # Child
    func_property.call() # Call parent-defined method (can come from anywhere).
    
  4. 初始化 Node 或其他 Object 參照。

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
  5. 初始化 NodePath。

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    

These options hide the points of access from the child node. This in turn keeps the child loosely coupled to its environment. You can reuse it in another context without any extra changes to its API.

備註

Although the examples above illustrate parent-child relationships, the same principles apply towards all object relations. Nodes which are siblings should only be aware of their own hierarchies while an ancestor mediates their communications and references.

# Parent
$Left.target = $Right.get_node("Receiver")

# Left
var target: Node
func execute():
    # Do something with 'target'.

# Right
func _init():
    var receiver = Receiver.new()
    add_child(receiver)

The same principles also apply to non-Node objects that maintain dependencies on other objects. Whichever object owns the other objects should manage the relationships between them.

警告

You should favor keeping data in-house (internal to a scene), though, as placing a dependency on an external context, even a loosely coupled one, still means that the node will expect something in its environment to be true. The project's design philosophies should prevent this from happening. If not, the code's inherent liabilities will force developers to use documentation to keep track of object relations on a microscopic scale; this is otherwise known as development hell. Writing code that relies on external documentation to use it safely is error-prone by default.

To avoid creating and maintaining such documentation, you convert the dependent node ("child" above) into a tool script that implements _get_configuration_warnings(). Returning a non-empty PackedStringArray from it will make the Scene dock generate a warning icon with the string(s) as a tooltip by the node. This is the same icon that appears for nodes such as the Area2D node when it has no child CollisionShape2D nodes defined. The editor then self-documents the scene through the script code. No content duplication via documentation is necessary.

像這樣的 GUI 可以更好的通知專案使用者有關節點的重要訊息。節點有外部相依性嗎?相依性有滿足嗎?其他的程式設計師,尤其是設計師與作家,會需要更清楚的指示來瞭解如何正確設定這些節點。

So, why does all this complex switcheroo work? Well, because scenes operate best when they operate alone. If unable to work alone, then working with others anonymously (with minimal hard dependencies, i.e. loose coupling) is the next best thing. Inevitably, changes may need to be made to a class, and if these changes cause it to interact with other scenes in unforeseen ways, then things will start to break down. The whole point of all this indirection is to avoid ending up in a situation where changing one class results in adversely affecting other classes dependent on it.

節點與場景,作為引擎類別的延伸,應該要能套用 所有 OOP 原則。這些原則的例子包含...

選擇一種節點樹架構

You might start to work on a game but get overwhelmed by the vast possibilities before you. You might know what you want to do, what systems you want to have, but where do you put them all? How you go about making your game is always up to you. You can construct node trees in countless ways. If you are unsure, this guide can give you a sample of a decent structure to start with.

A game should always have an "entry point"; somewhere you can definitively track where things begin so that you can follow the logic as it continues elsewhere. It also serves as a bird's eye view of all other data and logic in the program. For traditional applications, this is normally a "main" function. In Godot, it's a Main node.

  • Node「Main」(main.gd)

The main.gd script will serve as the primary controller of your game.

Then you have an in-game "World" (a 2D or 3D one). This can be a child of Main. In addition, you will need a primary GUI for your game that manages the various menus and widgets the project needs.

  • Node「Main」(main.gd)
    • Node2D/Node3D “世界”(game_world.gd)

    • Control「GUI」(gui.gd)

When changing levels, you can then swap out the children of the "World" node. Changing scenes manually gives you full control over how your game world transitions.

The next step is to consider what gameplay systems your project requires. If you have a system that...

  1. 能在內部追蹤所有的資料

  2. 可以在所有地方存取

  3. 獨立存在

... then you should create an autoload 'singleton' node.

備註

對於比較小的遊戲,有另一種控制比較少東西的替代方案為用一個只呼叫 SceneTree.change_scene() 的「Game」單例來切換主要場景的內容。這種結構或多或少將「World」場景作為主要遊戲節點保留。

Any GUI would also need to be either a singleton, a transitory part of the "World", or manually added as a direct child of the root. Otherwise, the GUI nodes would also delete themselves during scene transitions.

If you have systems that modify other systems' data, you should define those as their own scripts or scenes, rather than autoloads. For more information, see Autoloads versus regular nodes.

Each subsystem within your game should have its own section within the SceneTree. You should use parent-child relationships only in cases where nodes are effectively elements of their parents. Does removing the parent reasonably mean that the children should also be removed? If not, then it should have its own place in the hierarchy as a sibling or some other relation.

備註

In some cases, you need these separated nodes to also position themselves relative to each other. You can use the RemoteTransform / RemoteTransform2D nodes for this purpose. They will allow a target node to conditionally inherit selected transform elements from the Remote* node. To assign the target NodePath, use one of the following:

  1. 可信的第三方,可能是母節點,來居中分配。

  2. A group, to pull a reference to the desired node (assuming there will only ever be one of the targets).

When you should do this is subjective. The dilemma arises when you must micro-manage when a node must move around the SceneTree to preserve itself. For example...

  • 將「玩家」節點加到「房間」中。

  • Need to change rooms, so you must delete the current room.

  • Before the room can be deleted, you must preserve and/or move the player.

    If memory is not a concern, you can...

    • Create the new room.

    • Move the player to the new room.

    • Delete the old room.

    If memory is a concern, instead you will need to...

    • 將玩家移動到樹狀結構中其他地方。

    • 刪除房間。

    • 實體化並建立新的房間。

    • Re-add the player to the new room.

The issue is that the player here is a "special case" where the developers must know that they need to handle the player this way for the project. The only way to reliably share this information as a team is to document it. Keeping implementation details in documentation is dangerous. It's a maintenance burden, strains code readability, and unnecessarily bloats the intellectual content of a project.

In a more complex game with larger assets, it can be a better idea to keep the player somewhere else in the SceneTree entirely. This results in:

  1. 一致性更高。

  2. 沒有必須要寫成文件並在其他地方維護的「特例」。

  3. 不會因為沒有注意到細節而有發生錯誤的機會。

In contrast, if you ever need a child node that does not inherit the transform of its parent, you have the following options:

  1. The declarative solution: place a Node in between them. Since it doesn't have a transform, they won't pass this information to its children.

  2. 指令式 解法:在 CanvasItemSpatial 上使用 set_as_toplover Setter。這樣一來便可讓節點忽略其繼承的變換。

備註

If building a networked game, keep in mind which nodes and gameplay systems are relevant to all players versus those just pertinent to the authoritative server. For example, users do not all need to have a copy of every players' "PlayerController" logic - they only need their own. Keeping them in a separate branch from the "world" can help simplify the management of game connections and the like.

場景組織的關鍵就是要以關聯的方式來考量 SceneTree 而不是用空間性來考量。節點是否會仰賴母節點的存在?若不仰賴,則節點可以就完全可以在別的地方活得好好的。若有依賴母節點,則這種依賴關係就是子節點要是母節點的子節點的原因 (而且,顯然必須是母節點的場景中的一部分,如果還不是母節點的場景中的一部分的話)。

Does this mean nodes themselves are components? Not at all. Godot's node trees form an aggregation relationship, not one of composition. But while you still have the flexibility to move nodes around, it is still best when such moves are unnecessary by default.