Up to date

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

何时使用场景与脚本

我们已经介绍了场景和脚本的不同之处. 脚本使用命令性代码定义引擎类扩展, 而场景使用声明性代码.

因此,每个系统的功能都不尽相同。场景可以定义扩展类的初始化方式,但不能定义其实际行为。场景通常与脚本结合使用,场景声明节点的组成,而脚本则用命令式代码添加行为。

匿名类型

单独使用脚本 可以 完全定义场景的内容. 从本质上讲,Godot编辑器所做的, 仅在其对象的C++构造函数中.

但是, 选择哪个来使用, 可能是一个两难问题. 创建脚本实例与创建引擎类相同, 而处理场景需要更改API:

const MyNode = preload("my_node.gd")
const MyScene = preload("my_scene.tscn")
var node = Node.new()
var my_node = MyNode.new() # Same method call.
var my_scene = MyScene.instantiate() # Different method call.
var my_inherited_scene = MyScene.instantiate(PackedScene.GEN_EDIT_STATE_MAIN) # Create scene inheriting from MyScene.

此外, 由于引擎和脚本代码之间的速度差异, 脚本的运行速度将比场景慢一些. 节点越大和越复杂, 将它构建为场景的理由就越多.

命名的类型

脚本可以在编辑器中被注册为一个新类型。这样做之后,在节点或资源创建对话框中,它就会被显示为一个新类型,并带有可选图标。这样,用户就可以更加便捷地使用脚本,而不是必须…

  1. 了解他们想要使用的脚本的基本类型.

  2. 创建一个该基本类型的实例.

  3. 将脚本添加到节点.

通过注册一个脚本,该脚本类型将像系统中的其他节点和资源一样成为一个可以被创建的选项。创建对话框甚至还有一个搜索栏,可以按名称查找类型。

用于注册类型的系统有两种:

  • 自定义类型

    • 仅限编辑器. 类型名称在运行时中不可访问.

    • 不支持继承的自定义类型.

    • 一个初始化工具. 使用脚本创建节点. 仅此而已.

    • 编辑器没有对该脚本的类型感知, 或其与其他引擎类型或脚本的关系.

    • 允许用户定义一个图标.

    • 适用于所有脚本语言, 因为它抽象处理脚本资源.

    • 设置使用 EditorPlugin.add_custom_type.

  • Script 类

    • 编辑器和运行时均可访问.

    • 显示全部继承关系.

    • 使用脚本创建节点, 但也可以从编辑器更改或扩展类型.

    • 编辑器知道脚本, 脚本类和引擎c++类之间的继承关系.

    • 允许用户定义一个图标.

    • 引擎开发人员必须手动添加对语言的支持(名称公开和运行时可访问性两者).

    • 仅适用于Godot 3.1+版本.

    • 编辑器扫描项目文件夹, 并为所有脚本语言注册任何公开的名称. 为公开此信息, 每种脚本语言都必须实现自己的支持.

这两种方法都向创建对话框添加名称, 特别是脚本类, 还允许用户在不加载脚本资源的情况下访问类别名称. 在任何地方都可以创建实例, 和访问常量或静态方法.

有了这些功能, 由于它赋予用户易用性, 人们可能希望它们的类型是没有场景的脚本. 那些正在开发的插件或创建供设计人员使用的内部工具, 将以这种方式使事情变得更轻松.

不足之处在于, 这也意味着很大程度上必须使用命令式编程.

Script 与 PackedScene 的性能

在选择场景和脚本时, 最后一个需要考虑的方面是执行速度.

随着对象内容的增加, 脚本创建和初始化所需的内容也会大大增加. 创建节点层次结构就说明了这一点. 每个Node的逻辑可能有几百行代码.

下面的代码示例创建一个新的 Node, 更改名称, 分配脚本, 将其未来的父级设置为其所有者, 以便保存到磁盘中, 最后将其添加为 "主" 节点的子级:

# main.gd
extends Node

func _init():
    var child = Node.new()
    child.name = "Child"
    child.script = preload("child.gd")
    add_child(child)
    child.owner = self

这样的脚本代码比引擎端的C++代码要慢很多. 每条指令都要调用脚本API, 导致后端要进行多次 "查找", 以找到要执行的逻辑.

场景有助于避免这个性能问题。PackedScene (场景包)是场景继承的基础类型,定义了使用序列化数据创建对象的资源。引擎可以在后端批量处理场景,并提供比脚本好得多的性能。

总结

最后, 最好的方法是考虑以下几点:

  • 如果希望创建一个基本工具, 它将在几个不同的项目中重用, 以及可能提供给不同技能水平的人使用.(包括那些不认为自己是个程序员的用户), 它很可能是一个脚本, 有一个自定义名称/图标.

  • 如果有人想创造一个特定于他们的游戏的概念, 那么它应该是一个场景. 场景比脚本更容易跟踪/编辑, 并提供更多的安全性.

  • 如果你想命名一个场景,那么你可以通过声明一个脚本类并给它一个场景作为常量来实现这一点。实际上,该脚本变成了一个命名空间:

    # game.gd
    class_name Game # extends RefCounted, so it won't show up in the node creation dialog.
    extends RefCounted
    
    const MyScene = preload("my_scene.tscn")
    
    # main.gd
    extends Node
    func _ready():
        add_child(Game.MyScene.instantiate())