现场组织

本文涉及与有效组织场景内容相关的主题。应该使用哪个节点?他们应该放在哪里?他们应该如何互动?

如何有效地建立关系

当Godot用户开始制作自己的场景时,他们经常遇到以下问题:

他们创造了他们的第一个场景,并充满了内容,在他们感到他们需要把它分割成可重复使用的片段困扰着他们之前。他们把场景的分支保存到自己的场景中。然而,他们随后注意到,他们以前能够依赖的硬引用不再可能。在多个位置重新使用场景会产生问题,因为节点路径找不到目标。在编辑器中断中建立的信号连接。

要解决这些问题,必须实例化子场景,而不需要它们的环境细节。人们需要能够相信,子场景将创建自己,而不挑剔如何使用它。

OOP中要考虑的一个最大的问题是,使用 loose coupling 到代码库的其他部分。这使对象的大小保持较小(为了可维护性),并提高了它们的可重用性,因此不需要重新编写已完成的逻辑。

这些OOP最佳实践 几个 场景结构和脚本使用中最佳实践的结果。

如果可能的话,应该将场景设计为没有依赖关系。 也就是说,我们应该创造一个场景,把他们需要的一切都保存在自己的内部。

如果场景必须与外部上下文交互,经验丰富的开发人员建议使用 Dependency Injection . 这项技术包括让高级API提供低级API的依赖关系。为什么要这样做?因为依赖外部环境的类可能会无意中触发错误和意外行为。

为此,必须公开数据,然后依赖父上下文来初始化它:

  1. 连接到信号。非常安全,但应该只用于“响应”行为,而不是启动它。注意,信号名通常是过去式动词,如“entered”、“skill_activated”或“item_collected”。

    # Parent
    $Child.connect("signal_name", object_with_method, "method_on_the_object")
    
    # Child
    emit_signal("signal_name") # Triggers parent-defined behavior.
    
    // Parent
    GetNode("Child").Connect("SignalName", ObjectWithMethod, "MethodOnTheObject");
    
    // Child
    EmitSignal("SignalName"); // 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).
    
    // Parent
    GetNode("Child").Set("MethodName", "Do");
    
    // Child
    Call(MethodName); // Call parent-defined method (which child must own).
    
  3. 初始化 FuncRef 属性。比方法更安全,因为方法的所有权是不必要的。用于启动行为。

    # Parent
    $Child.func_property = funcref(object_with_method, "method_on_the_object")
    
    # Child
    func_property.call_func() # Call parent-defined method (can come from anywhere).
    
    // Parent
    GetNode("Child").Set("FuncProperty", GD.FuncRef(ObjectWithMethod, "MethodOnTheObject"));
    
    // Child
    FuncProperty.CallFunc(); // Call parent-defined method (can come from anywhere).
    
  4. 初始化节点或其他对象引用。

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
    // Parent
    GetNode("Child").Set("Target", this);
    
    // Child
    GD.Print(Target); // Use parent-defined node.
    
  5. 初始化节点路径。

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    
    // Parent
    GetNode("Child").Set("TargetPath", NodePath(".."));
    
    // Child
    GetNode(TargetPath); // Use parent-defined NodePath.
    

这些选项隐藏子节点的访问源。这反过来又让孩子 松散耦合 它的环境。可以在另一个上下文中重用它,而不需要对其API进行任何额外的更改。

注解

尽管上面的例子说明了父子关系,但同样的原则也适用于所有对象关系。作为兄弟节点的节点应该只知道它们的层次结构,而祖先则中介它们的通信和引用。

# 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)
// Parent
GetNode<Left>("Left").Target = GetNode("Right/Receiver");

public class Left : Node
{
    public Node Target = null;

    public void Execute()
    {
        // Do something with 'Target'.
    }
}

public class Right : Node
{
    public Node Receiver = null;

    public Right()
    {
        Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
        AddChild(Receiver);
    }
}

同样的原则也适用于维护对其他对象依赖性的非节点对象。无论哪个对象真正拥有这些对象,都应该管理它们之间的关系。

警告

我们应该倾向于将数据保持在内部(场景内部),尽管将依赖关系放在外部上下文上,即使是松散耦合的上下文,仍然意味着节点在其环境中的某些内容将是真实的。项目的设计理念应该可以防止这种情况的发生。如果不是这样,代码的固有责任将迫使开发人员使用文档在微观的尺度上跟踪对象关系;这也被称为开发地狱。默认情况下,编写依赖外部文档安全使用的代码容易出错。

为了避免创建和维护这样的文档,可以将依赖节点(“上面的子节点”)转换为实现 _get_configuration_warning() . 从中返回非空字符串将使场景停靠生成一个警告图标,该字符串作为节点的工具提示。这与节点(如 Area2D 没有子节点时的节点 CollisionShape2D 节点已定义。然后,编辑器通过脚本代码自我记录场景。不需要通过文档进行内容复制。

这样的图形用户界面可以更好地通知项目用户有关节点的关键信息。它有外部依赖关系吗?是否满足了这些依赖性?其他程序员,特别是设计者和作者,需要在消息中有明确的说明,告诉他们如何配置它。

那么,为什么所有这些复杂的开关都能工作呢?嗯,因为场景在单独操作时运行得最好。如果无法单独工作,则匿名与其他人合作(具有最小的硬依赖性,即松耦合)。如果一个类不可避免的变化导致它以不可预见的方式与其他场景交互,那么事情就会崩溃。更改一个类可能会对其他类造成破坏性影响。

脚本和场景,作为引擎类的扩展,应该遵守 all OOP原则。示例包括…

选择节点树结构

因此,开发人员开始开发一款游戏,只是为了在它之前停止存在巨大的可能性。他们可能知道自己想做什么,想拥有什么样的系统,但是 在哪里? 把他们都放进去?好吧,一个人如何做他们的游戏总是由他们决定的。一个人可以用多种方法构造节点树。但是,对于那些不确定的人来说,这个有用的指南可以给他们一个体面的结构样本。

一个游戏应该总是有一种“入口点”;在某个地方,开发人员可以明确地跟踪事情的开始位置,这样他们就可以在其他地方继续遵循逻辑。这个地方还可以作为鸟瞰程序中所有其他数据和逻辑的鸟瞰图。对于传统应用程序,这将是“主要”功能。在这种情况下,它将是一个主节点。

  • 节点“main”(main.gd)

这个 main.gd 然后,脚本将作为一个人游戏的主控制器。

然后有一个游戏中实际的“世界”(一个二维或三维的)。这可以是main的子级。此外,游戏还需要一个主图形用户界面来管理项目所需的各种菜单和小部件。

  • 节点“main”(main.gd)
    • node2d/空间“世界”(game_world.gd)

    • 控制“gui”(gui.gd)

更改级别时,可以交换“世界”节点的子节点。 Changing scenes manually 让用户完全控制他们的游戏世界如何过渡。

下一步是考虑一个人的项目需要什么样的游戏系统。如果有一个系统…

  1. 在内部跟踪所有数据

  2. 应该是全局可访问的

  3. 应该孤立存在

…然后应该创建一个 autoload 'singleton' node .

注解

对于较小的游戏,一个更简单的选择是拥有一个“游戏”单例,简单地称为 SceneTree.change_scene() 方法交换主场景的内容。这种结构或多或少地保持了“世界”作为主要游戏节点的地位。

任何GUI也需要是单例的,是“世界”的过渡部分,或者手动添加为根目录的直接子目录。否则,在场景转换期间,GUI节点也会删除自己。

如果一个系统修改了其他系统的数据,那么应该将它们定义为自己的脚本或场景,而不是自动加载。有关原因的详细信息,请参阅 'Autoloads vs. Internal Nodes' 文档。

游戏中的每个子系统都应该在场景中有自己的部分。只有当节点是其父节点的有效元素时,才应该使用父子关系。删除父项是否合理地意味着应该同时删除子项?如果不是,那么它应该作为兄弟姐妹或其他关系在层次结构中有自己的位置。

注解

在某些情况下,需要这些分离的节点来 also 彼此相对。一个人可以使用 RemoteTransform / RemoteTransform2D 用于此目的的节点。它们将允许目标节点有条件地从远程继承选定的转换元素 * 节点。分配 target NodePath ,请使用以下选项之一:

  1. 中介分配的可靠第三方(可能是父节点)。

  2. 一个组,可以轻松地将引用拉到所需的节点(假设只有一个目标)。

什么时候应该这样做?好吧,由他们来决定。当一个节点必须在场景中移动以保护自身时,就必须进行微管理,这就产生了一个难题。例如。。。

  • 将“播放器”节点添加到“房间”。

  • 需要更改房间,因此必须删除当前房间。

  • 在删除房间之前,必须保留和/或移动播放器。

    记忆是一个问题吗?

    • 如果没有,可以创建两个房间,移动播放器并删除旧的房间。没问题。

    如果是这样,就需要…

    • 将播放器移到树中的其他位置。

    • 删除房间。

    • 实例化并添加新房间。

    • 重新添加播放器。

问题是这里的玩家是一个“特殊情况”,开发者必须这样做。 know 他们需要以这种方式处理项目中的玩家。因此,作为一个团队可靠地共享这些信息的唯一方法是 文件 它。然而,在文档中保留实施细节是危险的。这是一个维护负担,增加了代码的可读性,并且不必要地增加了项目的知识内容。

在一个拥有更大资产的更复杂的游戏中,最好是将玩家完全保留在场景中的其他地方。这涉及到…

  1. 更加一致。

  2. 没有“特殊情况”必须记录并保存在某个地方。

  3. 由于这些细节没有说明,因此没有发生错误的机会。

相反,如果需要一个子节点, not 继承其父级的转换,其中一个具有以下选项:

  1. 这个 声明的 解决方案:放置A Node 在他们之间。作为没有转换的节点,节点不会将这些信息传递给其子节点。

  2. 这个 命令 解决方案:使用 set_as_toplevelCanvasItemSpatial 节点。这将使节点忽略其继承的转换。

注解

如果构建网络游戏,请记住哪些节点和游戏系统与所有玩家相关,而哪些节点和游戏系统与权威服务器相关。例如,用户并不都需要每个玩家的“PlayerController”逻辑的副本。相反,他们只需要自己的。因此,将它们与“世界”分开,可以帮助简化对游戏连接等的管理。

场景组织的关键是从关系的角度而不是空间的角度来考虑场景。节点是否需要依赖于其父节点的存在?如果不是这样,他们就可以在其他地方自力更生。如果是这样的话,那就说明他们应该是那个父母的孩子(如果他们还没有成为父母的一部分的话,很可能是父母的一部分)。

这是否意味着节点本身就是组件?一点也不。Godot的节点树形成了聚合关系,而不是组合关系。但是,尽管仍然可以灵活地移动节点,但在默认情况下,当不需要这样的移动时,仍然是最好的选择。