Godot 4 中的场景组织
场景、脚本或资源
- 场景 (
.tscn):一个可复用的节点树,包含自身资源:玩家、敌人、拾取物、UI 面板、关卡。任何需要放置多次或单独运行 (F6) 的内容都应作为场景。 - 脚本 (
.gd):绑定到一个节点的行为。将其附加到所控制场景的根节点,而不是附加到需要深入访问的父节点。 - 资源脚本(
extends Resource配合class_name):没有节点的数据,如物品属性、波次定义或存档数据,可在 Inspector 中编辑并保存为.tres。
场景应能独立运行。如果场景需要来自外部的数据(目标、生成父节点),请使用 @export 或信号,而不是通过 get_parent() 链获取。
实例化场景
extends Node2D
@export var enemy_scene: PackedScene
@export var spawn_parent: Node2D
func spawn_enemy(at: Vector2) -> void:
var enemy := enemy_scene.instantiate() as Node2D
enemy.position = at
spawn_parent.add_child(enemy)
func load_level(path: String) -> void:
var packed := load(path) as PackedScene
if packed == null:
push_error("Level not found: " + path)
return
get_tree().change_scene_to_packed(packed)
@export var x: PackedScene在 Inspector 中设置,并在文件移动后仍然有效,因为场景存储了引用。const BULLET := preload("res://bullet.tscn")在解析时确定:文件缺失会导致解析错误(Preload file "res://bullet.tscn" does not exist.)。适用于固定资源。load()在运行时解析,如果路径错误则返回null并报错。适用于运行时构建的路径。instantiate()(不是 Godot 3 的instance())返回一个Node;将其转换为所需类型。使用queue_free()释放实例。
查找节点
extends Control
@onready var title: Label = %Title
@onready var start_button: Button = %StartButton
@onready var music: AudioStreamPlayer = $Music
@onready var optional_hint: Label = get_node_or_null("Hint")
func _ready() -> void:
title.text = "Main menu"
start_button.grab_focus()
if optional_hint:
optional_hint.visible = false
music.play()
$Path是get_node("Path")的简写,相对于当前节点。如果节点不存在会报错;对于可选节点使用get_node_or_null()。%Name可访问在同一场景中标记为“Access as Unique Name”(在场景停靠栏中右键单击)的节点,即使节点在场景内重新父级化也能找到。但不能访问实例化的子场景内部的节点:请使用$Child/%Name,或者当实例本身是唯一节点时使用%Child/%Name。@onready变量在_ready()之前立即赋值。它们在_init()和_enter_tree()中为null。_ready()在子节点上先于父节点执行,所以父节点的_ready()可以使用其子节点,但子节点的_ready()不能依赖父节点已就绪。- 为
@onready变量指定类型(: Label),以便解析器检查成员名称;未指定类型的路径上的$Label.txt只能在运行时捕获错误。
组
组用于跨场景标记节点。在节点停靠栏(组标签)或代码中添加组,然后查询场景树:
extends Node
func _ready() -> void:
add_to_group("enemies")
func alert_all_enemies() -> void:
get_tree().call_group("enemies", "set_alerted", true)
for enemy in get_tree().get_nodes_in_group("enemies"):
print(enemy.name)
var player := get_tree().get_first_node_in_group("player") as Node2D
if player:
print("Player at ", player.global_position)
call_group() 在 Godot 4 中立即执行。使用 get_tree().call_group_flags(SceneTree.GROUP_CALL_DEFERRED, ...) 来延迟执行。
自动加载
自动加载是一个在主场景之前创建并跨场景切换保持存在的节点。用于管理跨越单个场景的状态(设置、存档数据、场景切换器)以及仅用于信号的全局事件总线。在“项目设置 > 全局 > 自动加载”中注册;project.godot 中将包含:
[autoload]
Events="*res://autoload/events.gd"
* 表示“已启用”复选框被勾选,因此脚本可以直接使用其名称(Events.player_died.emit())。不要对自动加载使用 free() 或 queue_free()。尽量少用自动加载:任何属于单个关卡的内容都应放在该关卡的场景中。
工具注意事项:godot --check-only --script 不会加载自动加载,因此使用自动加载名称的脚本会在该命令下报告 Identifier not found: Events,即使它正常运行。请通过运行场景来验证此类脚本(请参考 godot-verify 技能)。
class_name
class_name Player 注册一个全局类型,这样其他脚本可以写 var player: Player 并启用成员检查。编辑器会在 .godot/ 中维护一个全局类缓存;如果在编辑器外部添加了 class_name,请先打开编辑器或运行 godot --headless --path . --import,再检查使用它的脚本。
UID、.uid 文件和移动文件
Godot 4 同时通过 UID 和路径引用资源。场景和资源在文件头中存储自己的 UID,导入的资源存储在 .import 文件中,从 Godot 4.4 开始,脚本和着色器会生成一个配套文件(player.gd.uid、water.gdshader.uid)。编辑器中保存的引用如下所示:
[ext_resource type="Script" uid="uid://dxxoiuob4eoum" path="res://game_manager.gd" id="1_feb5d"]
规则:
- 将
*.uid文件提交到版本控制。不要将它们添加到.gitignore。 - 尽量在 Godot 文件系统停靠栏中移动和重命名文件;它会自动更新引用。
- 如果在 Godot 外部(终端、IDE、代理)移动脚本或着色器,请同时移动其
.uid文件,然后打开编辑器或运行godot --headless --path . --import,以便 UID 缓存学习新路径。在完成扫描之前,仍然使用旧路径并且加载会失败。 - 如果
.uid文件被遗留在原地,Godot 会为移动的脚本生成一个新的 UID,之前引用旧 UID 的场景会回退到旧的路径:WARNING: ... invalid UID: uid://... - using text path instead: res://old/path.gd,然后ERROR: ... referenced non-existent resource。 - 手动编写
.tscn时,不要随意编造uid=值。省略该属性并使用正确的path;编辑器会在下次保存场景时添加 UID。 - 手动编写的 Godot 4 场景以
[gd_scene format=3]开头,并通过字符串 id 引用资源:[ext_resource type="Script" path="res://main.gd" id="1_main"],然后在节点上使用script = ExtResource("1_main")。子节点需要parent="."或从根节点开始的路径。Godot 4.7 还会在节点上写入unique_id=;手动编写时省略它。 load("uid://...")和preload("uid://...")接受 UID 字符串。从文件系统停靠栏右键菜单复制 UID;ResourceUID.id_to_text(ResourceLoader.get_resource_uid(path))可从代码中返回它。
常见错误
- 使用
get_parent().get_parent()或绝对路径如/root/Main/Player向上查找。应导出引用、使用组或发射信号。 - 在
_init()中或节点进入场景树之前使用节点。 - 在许多脚本中硬编码
res://路径,而不是导出PackedScene。 - 移动
.gd文件时未移动其.uid文件,或在 git 中忽略*.uid。 - 为了方便而将所有内容都设为自动加载。