Godot 4 GDScript 教程 / 节点与场景树
节点与场景树
节点(Node)和场景树(Scene Tree)是 Godot 引擎的核心架构。每个游戏都由一棵节点树构成,理解节点体系、生命周期和通信模式是开发 Godot 游戏的基础。
1. 节点类型体系
1.1 核心节点分类
| 类别 | 常用节点 | 用途 |
|---|---|---|
| 基础节点 | Node, Node2D, Node3D | 通用容器和空间变换 |
| 2D 节点 | Sprite2D, AnimatedSprite2D, TileMapLayer | 2D 可视化 |
| 3D 节点 | MeshInstance3D, Camera3D, Light3D | 3D 可视化 |
| 物理 2D | CharacterBody2D, RigidBody2D, Area2D | 2D 物理 |
| 物理 3D | CharacterBody3D, RigidBody3D, Area3D | 3D 物理 |
| UI 节点 | Control, Button, Label, TextureRect | 用户界面 |
| 音频节点 | AudioStreamPlayer, AudioStreamPlayer2D/3D | 音频播放 |
| 相机节点 | Camera2D, Camera3D | 视图控制 |
| 导航节点 | NavigationAgent2D/3D, NavigationRegion2D/3D | 寻路导航 |
| 动画节点 | AnimationPlayer, AnimationTree | 动画系统 |
| 粒子节点 | GPUParticles2D, GPUParticles3D | 粒子效果 |
1.2 节点继承链
Node (所有节点的基类)
├── CanvasItem (2D 可绘制基类)
│ ├── Node2D (2D 空间变换)
│ │ ├── Sprite2D
│ │ ├── AnimatedSprite2D
│ │ ├── CharacterBody2D
│ │ ├── RigidBody2D
│ │ ├── Area2D
│ │ ├── TileMapLayer
│ │ ├── Camera2D
│ │ ├── Parallax2D
│ │ └── ...
│ └── Control (UI 基类)
│ ├── Button
│ ├── Label
│ ├── TextureRect
│ ├── VBoxContainer
│ └── ...
├── Node3D (3D 空间变换)
│ ├── MeshInstance3D
│ ├── CharacterBody3D
│ ├── RigidBody3D
│ ├── Camera3D
│ ├── Light3D
│ └── ...
├── AudioStreamPlayer
├── Timer
└── ...
2. 场景树管理
2.1 访问场景树
extends Node
func _ready() -> void:
# 获取场景树
var tree: SceneTree = get_tree()
# 获取根节点
var root: Window = tree.root
# 获取当前场景
var current: Node = tree.current_scene
# 获取节点数量
var node_count: int = tree.node_count
# 场景树信息
print(f"当前场景: {current.name}")
print(f"节点数量: {node_count}")
print(f"帧数: {tree.get_frame()}")
print(f"FPS: {Engine.get_frames_per_second()}")
2.2 节点查找
extends Node
func _ready() -> void:
# $ 简写 get_node()
var sprite: Sprite2D = $Sprite2D
var child: Node = $Child/Grandchild
# 路径查找
var node: Node = get_node("Path/To/Node")
var node_from_root: Node = get_node("/root/MainScene/Player")
# 安全查找(不存在返回 null)
var maybe: Node = get_node_or_null("MaybeExists")
if maybe:
print("节点存在")
# 查找子节点(非递归)
var child_a: Node = get_child(0) # 按索引
var child_b: Node = find_child("Enemy") # 按名称
var all_enemies: Array[Node] = find_children("*Enemy*", "", true, false)
# 按组查找
var enemies: Array[Node] = get_tree().get_nodes_in_group("enemies")
var boss: Node = get_tree().get_first_node_in_group("boss")
# 按路径模式查找
var global_target: Node = get_node("/root/GlobalNode")
3. 节点生命周期
3.1 生命周期顺序
初始化顺序:
1. _init() ← GDScript 构造函数
2. _enter_tree() ← 节点进入场景树
3. _ready() ← 所有子节点已就绪
4. _process() ← 每帧调用(开始)
5. _physics_process()← 物理帧调用(开始)
退出顺序:
1. _exit_tree() ← 节点离开场景树
2. _notification(NOTIFICATION_PREDELETE) ← 即将销毁
3.2 生命周期详解
extends Node2D
func _init() -> void:
"""构造函数 - 节点创建时调用,此时还没有进入场景树"""
print("1. _init: 构造")
# 此时无法访问子节点或场景树
func _enter_tree() -> void:
"""节点进入场景树 - 但子节点可能还未就绪"""
print("2. _enter_tree: 进入场景树")
# parent 和 owner 可用,但子节点可能尚未初始化
func _ready() -> void:
"""所有子节点就绪后调用 - 初始化逻辑放在这里"""
print("3. _ready: 就绪")
# 所有子节点已可用,可以安全获取引用
func _process(delta: float) -> void:
"""每帧调用 - 逻辑更新"""
# 不应在这里做重量级计算
func _physics_process(delta: float) -> void:
"""物理帧调用 - 物理相关逻辑"""
# 固定频率(默认 60 FPS)
func _exit_tree() -> void:
"""节点离开场景树 - 清理逻辑"""
print("离开场景树")
func _notification(what: int) -> void:
"""通知系统 - 可以捕获各种引擎事件"""
match what:
NOTIFICATION_PREDELETE:
print("即将销毁")
NOTIFICATION_PAUSED:
print("游戏暂停")
NOTIFICATION_UNPAUSED:
print("游戏恢复")
3.3 子节点初始化顺序
# 父节点的 _ready() 在所有子节点的 _ready() 之后调用
extends Node
# 子节点 A
func _ready() -> void:
print("A: _ready") # 先执行
# 子节点 B
func _ready() -> void:
print("B: _ready") # 其次执行
# 父节点
func _ready() -> void:
print("Parent: _ready") # 最后执行
# 此时所有子节点都已就绪
⚠️ 注意: _ready() 只在节点首次进入场景树时调用。如果节点被移除后再次添加,需要使用 _enter_tree()。
4. 场景实例化与释放
4.1 PackedScene 实例化
extends Node2D
# 在检查器中设置敌人场景
@export var enemy_scene: PackedScene
@export var explosion_scene: PackedScene
@export var pickup_scene: PackedScene
func spawn_enemy(position: Vector2, level: int = 1) -> Node2D:
"""实例化敌人"""
if not enemy_scene:
push_error("未设置 enemy_scene")
return null
var enemy: Node2D = enemy_scene.instantiate()
enemy.position = position
enemy.set("level", level) # 动态设置属性
add_child(enemy)
return enemy
func spawn_explosion(pos: Vector2) -> void:
"""实例化爆炸效果"""
var explosion: Node2D = explosion_scene.instantiate()
explosion.position = pos
add_child(explosion)
# 爆炸完成后自动释放
if explosion.has_method("play"):
explosion.play()
await explosion.finished
explosion.queue_free()
func spawn_wave() -> void:
"""生成一波敌人"""
for i in range(5):
var angle: float = (TAU / 5) * i
var pos: Vector2 = global_position + Vector2(cos(angle), sin(angle)) * 200
spawn_enemy(pos, 1)
4.2 场景释放
extends Node
func cleanup_example() -> void:
# 方法 1: queue_free() - 延迟到帧末安全删除
var enemy: Node2D = $Enemy
enemy.queue_free()
# 方法 2: 立即删除(不推荐,可能影响遍历)
# enemy.free()
# 方法 3: 批量删除子节点
for child in get_children():
child.queue_free()
# 方法 4: 从父节点移除
var child: Node = $SomeChild
remove_child(child) # 移除但不销毁
# 需要手动管理内存或重新添加
# 安全的延迟释放
func safe_destroy() -> void:
# 标记为即将销毁
set_process(false)
set_physics_process(false)
# 播放死亡动画
if has_node("AnimationPlayer"):
$AnimationPlayer.play("death")
await $AnimationPlayer.animation_finished
queue_free()
5. 组 Groups
5.1 组的基本用法
extends Node
func _ready() -> void:
# 在编辑器中通过检查器的 "节点" > "组" 标签添加组
# 代码中添加到组
add_to_group("players")
add_to_group("allies")
# 从组中移除
remove_from_group("allies")
# 检查是否在组中
if is_in_group("players"):
print("是玩家")
# 检查是否在组中(简写)
if "players" in get_groups():
print("是玩家")
5.2 通过组操作节点
extends Node
func _ready() -> void:
# 获取组中所有节点
var enemies: Array[Node] = get_tree().get_nodes_in_group("enemies")
print(f"敌人数量: {enemies.size()}")
# 获取组中第一个节点
var player: Node = get_tree().get_first_node_in_group("players")
# 统计组中节点数量
var enemy_count: int = get_tree().get_node_count_in_group("enemies")
# 向组中所有节点发送调用
get_tree().call_group("enemies", "take_damage", 10)
get_tree().call_group_flags(
SceneTree.GROUP_CALL_DEFERRED, # 延迟调用
"enemies",
"update_target",
player
)
# 更安全的方式:遍历并检查
for enemy in get_tree().get_nodes_in_group("enemies"):
if is_instance_valid(enemy) and enemy.has_method("take_damage"):
enemy.take_damage(10)
5.3 组的实战模式
# 敌人管理器
extends Node
func _process(_delta: float) -> void:
var enemies := get_tree().get_nodes_in_group("enemies")
var player := get_tree().get_first_node_in_group("players")
if not player or enemies.is_empty():
return
# 更新每个敌人的目标
for enemy in enemies:
if is_instance_valid(enemy):
enemy.set("target", player)
# 范围伤害
func area_damage(center: Vector2, radius: float, damage: int) -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
if not is_instance_valid(enemy):
continue
var dist: float = enemy.global_position.distance_to(center)
if dist <= radius:
var falloff: float = 1.0 - (dist / radius)
enemy.take_damage(int(damage * falloff))
6. 节点通信模式
6.1 直接引用
# 适用于紧耦合的父子关系
extends CharacterBody2D
@onready var sprite: Sprite2D = $Sprite2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var health_bar: ProgressBar = $UI/HealthBar
func update_health_bar(current: int, max_val: int) -> void:
health_bar.max_value = max_val
health_bar.value = current
6.2 信号通信
# 适用于松耦合的通信
extends CharacterBody2D
signal died
signal health_changed(value: int)
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health)
if health <= 0:
died.emit()
6.3 调用链(向下传递)
# 父节点通过 call 调用子节点
extends Node
func notify_children(message: String) -> void:
for child in get_children():
if child.has_method("on_notify"):
child.on_notify(message)
# 子节点实现响应
# extends Node2D
# func on_notify(message: String) -> void:
# print(f"收到通知: {message}")
6.4 调用链(向上传递)
# 子节点通过 owner 或 group 向父节点通信
extends Area2D
func _on_body_entered(body: Node2D) -> void:
# 向上冒泡事件
var parent := get_parent()
if parent.has_method("on_item_collected"):
parent.on_item_collected(self)
7. Autoload 全局脚本
7.1 配置 Autoload
项目 > 项目设置 > 自动加载
名称: GameManager
路径: res://scripts/autoload/game_manager.gd
启用: ✅
7.2 Autoload 实现
# scripts/autoload/game_manager.gd
extends Node
## 全局游戏状态管理器
signal game_started
signal game_over(score: int)
signal score_changed(new_score: int)
enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }
var state: GameState = GameState.MENU
var score: int = 0
var high_score: int = 0
var current_level: int = 1
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS # 暂停时也能运行
_load_high_score()
func start_game() -> void:
state = GameState.PLAYING
score = 0
current_level = 1
get_tree().paused = false
game_started.emit()
func pause_game() -> void:
if state == GameState.PLAYING:
state = GameState.PAUSED
get_tree().paused = true
func resume_game() -> void:
if state == GameState.PAUSED:
state = GameState.PLAYING
get_tree().paused = false
func end_game() -> void:
state = GameState.GAME_OVER
if score > high_score:
high_score = score
_save_high_score()
game_over.emit(score)
func add_score(points: int) -> void:
score += points
score_changed.emit(score)
func change_scene(scene_path: String) -> void:
get_tree().change_scene_to_file(scene_path)
func _save_high_score() -> void:
var config := ConfigFile.new()
config.set_value("game", "high_score", high_score)
config.save("user://save.cfg")
func _load_high_score() -> void:
var config := ConfigFile.new()
if config.load("user://save.cfg") == OK:
high_score = config.get_value("game", "high_score", 0)
7.3 使用 Autoload
# 在任何脚本中访问 GameManager
extends CharacterBody2D
func _ready() -> void:
GameManager.game_over.connect(_on_game_over)
func add_score(points: int) -> void:
GameManager.add_score(points)
func die() -> void:
GameManager.end_game()
func _on_game_over(score: int) -> void:
print(f"游戏结束!分数: {score}")
8. 场景切换
8.1 基本场景切换
extends Node
# 方法 1: 直接切换
func go_to_main_menu() -> void:
get_tree().change_scene_to_file("res://scenes/ui/main_menu.tscn")
# 方法 2: 使用 PackedScene 资源
@export var next_level: PackedScene
func go_to_next_level() -> void:
if next_level:
get_tree().change_scene_to_packed(next_level)
# 方法 3: 带过渡动画的场景切换
func change_scene_with_transition(scene_path: String) -> void:
# 播放淡出
var canvas_layer: CanvasLayer = $TransitionLayer
var color_rect: ColorRect = canvas_layer.get_node("ColorRect")
var tween := create_tween()
tween.tween_property(color_rect, "color:a", 1.0, 0.5)
await tween.finished
# 切换场景
get_tree().change_scene_to_file(scene_path)
# 等待一帧让新场景加载
await get_tree().process_frame
# 播放淡入
tween = create_tween()
tween.tween_property(color_rect, "color:a", 0.0, 0.5)
8.2 场景切换管理器
# autoload/scene_manager.gd
extends Node
signal scene_changed(scene_name: String)
var _current_scene: Node
var _transition_layer: CanvasLayer
func _ready() -> void:
# 创建过渡层
_transition_layer = CanvasLayer.new()
_transition_layer.layer = 100
add_child(_transition_layer)
_current_scene = get_tree().current_scene
func change_scene(
scene_path: String,
transition: String = "fade",
duration: float = 0.5
) -> void:
match transition:
"fade":
await _fade_transition(scene_path, duration)
"instant":
_instant_transition(scene_path)
"slide":
await _slide_transition(scene_path, duration)
func _instant_transition(scene_path: String) -> void:
get_tree().change_scene_to_file(scene_path)
_current_scene = get_tree().current_scene
scene_changed.emit(_current_scene.name)
func _fade_transition(scene_path: String, duration: float) -> void:
# 创建覆盖层
var overlay := ColorRect.new()
overlay.color = Color.BLACK
overlay.color.a = 0.0
overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
_transition_layer.add_child(overlay)
# 淡出
var tween := create_tween()
tween.tween_property(overlay, "color:a", 1.0, duration / 2)
await tween.finished
# 切换
get_tree().change_scene_to_file(scene_path)
await get_tree().process_frame
# 淡入
tween = create_tween()
tween.tween_property(overlay, "color:a", 0.0, duration / 2)
await tween.finished
overlay.queue_free()
_current_scene = get_tree().current_scene
scene_changed.emit(_current_scene.name)
func _slide_transition(scene_path: String, duration: float) -> void:
# 滑动过渡(简化实现)
await _fade_transition(scene_path, duration)
9. 游戏开发场景
场景:关卡管理系统
# autoload/level_manager.gd
extends Node
signal level_loaded(level_number: int)
signal level_completed(level_number: int, stars: int)
@export var levels: Array[PackedScene] = []
var current_level_index: int = -1
var current_level_node: Node = null
var levels_data: Dictionary = {} # 存档数据
func _ready() -> void:
_load_progress()
func load_level(index: int) -> void:
if index < 0 or index >= levels.size():
push_error(f"无效关卡索引: {index}")
return
# 清理当前关卡
if current_level_node:
current_level_node.queue_free()
await current_level_node.tree_exited
# 加载新关卡
current_level_index = index
current_level_node = levels[index].instantiate()
get_tree().current_scene.add_child(current_level_node)
# 配置关卡
if current_level_node.has_method("setup"):
current_level_node.setup(index + 1)
level_loaded.emit(index + 1)
func complete_level(stars: int) -> void:
var level_num := current_level_index + 1
var prev_stars: int = levels_data.get(level_num, 0)
if stars > prev_stars:
levels_data[level_num] = stars
_save_progress()
level_completed.emit(level_num, stars)
func next_level() -> void:
load_level(current_level_index + 1)
func restart_level() -> void:
load_level(current_level_index)
func _save_progress() -> void:
var config := ConfigFile.new()
for level_num in levels_data:
config.set_value("levels", str(level_num), levels_data[level_num])
config.save("user://levels.cfg")
func _load_progress() -> void:
var config := ConfigFile.new()
if config.load("user://levels.cfg") == OK:
for key in config.get_section_keys("levels"):
levels_data[int(key)] = config.get_value("levels", key)
10. 扩展阅读
上一章: 07 - 信号系统 下一章: 09 - 2D 渲染与 Sprite