强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Godot 4 GDScript 教程 / 节点与场景树

节点与场景树

节点(Node)和场景树(Scene Tree)是 Godot 引擎的核心架构。每个游戏都由一棵节点树构成,理解节点体系、生命周期和通信模式是开发 Godot 游戏的基础。

1. 节点类型体系

1.1 核心节点分类

类别常用节点用途
基础节点Node, Node2D, Node3D通用容器和空间变换
2D 节点Sprite2D, AnimatedSprite2D, TileMapLayer2D 可视化
3D 节点MeshInstance3D, Camera3D, Light3D3D 可视化
物理 2DCharacterBody2D, RigidBody2D, Area2D2D 物理
物理 3DCharacterBody3D, RigidBody3D, Area3D3D 物理
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