Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

场景组织

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. 初始化 Callable 属性。比调用方法更安全,因为不需要拥有这个方法的所有权。用于启动行为。

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

  • “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.

  • “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.

备注

对于较小的游戏,一个更简单且更少控制的做法是使用一个“Game”单例,简单地调用 SceneTree.change_scene_to_file() 方法,用于置换出主场景的内容。这种结构多少保留了“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. 命令式解决方案:对 CanvasItem 或者 Node3D 节点使用 top_level 属性。这样就会让该节点忽略其继承的变换(transform)。

备注

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.