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

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

节点与场景树

节点类型概览

Godot 中一切皆节点(Node)。不同类型的节点负责不同的功能。

2D 节点

节点类型 功能
Node2D 2D 基础节点(位置、旋转、缩放)
Sprite 显示 2D 纹理
AnimatedSprite 帧动画精灵
Camera2D 2D 相机
TileMap 瓦片地图
YSort Y 轴排序容器
ParallaxBackground 视差背景
Particles2D 2D 粒子
Light2D 2D 光源

物理节点

节点类型 功能
RigidBody2D 2D 刚体(受物理模拟控制)
StaticBody2D 2D 静态体(不移动的碰撞体)
KinematicBody2D 2D 运动学体(代码控制移动)
Area2D 2D 区域(检测进入/离开)
CollisionShape2D 碰撞形状
CollisionPolygon2D 碰撞多边形

UI 节点(Control)

节点类型 功能
Control UI 基础节点
Label 文本标签
Button 按钮
LineEdit 单行文本输入
TextEdit 多行文本编辑
TextureRect 纹理显示
ProgressBar 进度条
HSlider / VSlider 滑块
CheckBox / CheckButton 复选框
OptionButton 下拉选择
Container 布局容器

通用节点

节点类型 功能
Node 基础节点(用于逻辑组织)
Timer 计时器
Tween 补间动画
AnimationPlayer 动画播放器
AnimationTree 动画状态机
AudioStreamPlayer 2D/3D 音频播放
HTTPRequest HTTP 请求

3D 节点

节点类型 功能
Spatial 3D 基础节点
MeshInstance 3D 网格显示
Camera 3D 相机
DirectionalLight 方向光
OmniLight 点光源
SpotLight 聚光灯
RigidBody 3D 刚体
KinematicBody 3D 运动学体
CSGBox / CSGSphere CSG 几何体

场景树结构

场景树是 Godot 的核心概念,每个场景都是节点的树状层次结构。

Main (Node2D)
├── Player (KinematicBody2D)
│   ├── Sprite (Sprite)
│   ├── CollisionShape2D
│   ├── Camera2D
│   └── Area2D
│       └── CollisionShape2D
├── Enemies (Node2D)
│   ├── Slime (KinematicBody2D)
│   │   ├── Sprite
│   │   └── CollisionShape2D
│   └── Goblin (KinematicBody2D)
│       ├── Sprite
│       └── CollisionShape2D
├── TileMap
├── CanvasLayer
│   └── UI (Control)
│       ├── HealthBar (ProgressBar)
│       └── ScoreLabel (Label)
└── AudioStreamPlayer

场景树规则

  1. 根节点:每个场景文件(.tscn)有一个根节点
  2. 父子关系:子节点的位置相对于父节点
  3. 场景实例化:一个场景可以作为节点嵌入另一个场景
  4. 唯一根:整个运行时有一个全局场景树根(SceneTree

节点生命周期

节点创建 (new / instance)
    │
    ├── _init()                  # 构造函数
    │
    ├── add_child() 调用
    │   ├── _enter_tree()        # 进入场景树
    │   │
    │   ├── 子节点初始化...
    │   │
    │   └── _ready()             # 子节点全部就绪(自下而上)
    │
    ├── 运行时
    │   ├── _process(delta)          # 每帧
    │   ├── _physics_process(delta)  # 每物理帧
    │   └── _input(event)            # 输入事件
    │
    ├── remove_child() / queue_free()
    │   ├── _exit_tree()         # 离开场景树
    │   └── 对象被销毁
    └── _notification()          # 各种通知

生命周期代码示例

extends Node2D

func _init() -> void:
    print("[_init] 构造函数 - 节点对象已创建")

func _enter_tree() -> void:
    print("[_enter_tree] 进入场景树 - path: ", get_path())

func _ready() -> void:
    print("[_ready] 就绪 - 所有子节点已初始化")
    print("  子节点数量: ", get_child_count())
    print("  树中的路径: ", get_path())

func _process(delta: float) -> void:
    pass  # 每帧调用(不建议在生命周期学习时开启)

func _exit_tree() -> void:
    print("[_exit_tree] 离开场景树")

func _notification(what: int) -> void:
    match what:
        NOTIFICATION_ENTER_TREE:
            pass  # 与 _enter_tree 相同时机
        NOTIFICATION_READY:
            pass  # 与 _ready 相同时机
        NOTIFICATION_EXIT_TREE:
            pass  # 与 _exit_tree 相同时机
        NOTIFICATION_PAUSED:
            print("游戏暂停")
        NOTIFICATION_UNPAUSED:
            print("游戏恢复")

_ready 的执行顺序

# 假设场景树:
# A
# ├── B
# │   └── D
# └── C

# _ready 执行顺序: D → B → C → A
# 从最深的子节点开始,由下往上执行
# 这保证了 _ready 中访问子节点时,子节点已就绪

💡 提示:在 _ready() 中可以安全地访问子节点,因为子节点的 _ready() 已经先执行完毕。

add_child / remove_child

动态添加子节点

extends Node2D

var enemy_scene = preload("res://scenes/Enemy.tscn")

func spawn_enemy(pos: Vector2) -> void:
    var enemy = enemy_scene.instance()
    enemy.position = pos
    add_child(enemy)  # 将 enemy 添加为当前节点的子节点

func spawn_bullet(pos: Vector2, direction: Vector2) -> void:
    var bullet = preload("res://scenes/Bullet.tscn").instance()
    bullet.position = pos
    bullet.direction = direction
    $Bullets.add_child(bullet)  # 添加到 Bullets 子节点下

动态移除子节点

# 方式一:直接释放
func remove_all_enemies() -> void:
    for enemy in $Enemies.get_children():
        $Enemies.remove_child(enemy)  # 从树中移除
        enemy.queue_free()            # 释放内存

# 方式二:queue_free(推荐)
func remove_enemy(enemy: Node) -> void:
    enemy.queue_free()  # 在当前帧结束后自动移除并释放

# 方式三:立即释放(不推荐,可能导致问题)
func immediate_remove() -> void:
    $Enemy.free()  # 立即释放,可能在回调中出问题

queue_free vs free

特性 queue_free() free()
释放时机 当前帧结束后 立即
安全性 🟢 安全 🔴 可能崩溃
推荐度 ✅ 推荐 ❌ 谨慎使用

⚠️ 注意:不要在 _process 或信号回调中直接 free(),使用 queue_free() 更安全。

场景实例化

PackedScene 与 instance

# 预加载场景资源
var enemy_scene = preload("res://scenes/Enemy.tscn")

# 实例化场景
func spawn(pos: Vector2) -> void:
    var instance = enemy_scene.instance()  # 创建实例
    instance.position = pos
    add_child(instance)

# 动态加载场景
func spawn_from_path(path: String, pos: Vector2) -> void:
    var scene = load(path)
    if scene:
        var instance = scene.instance()
        instance.position = pos
        add_child(instance)

传递初始化参数

# Enemy.gd
extends KinematicBody2D

var enemy_type: String = ""
var level: int = 1

func init(type: String, lvl: int) -> void:
    enemy_type = type
    level = lvl
    _apply_type_settings()

func _apply_type_settings() -> void:
    match enemy_type:
        "slime":
            health = 50
            speed = 100
        "goblin":
            health = 80
            speed = 150
        "dragon":
            health = 500
            speed = 80

# 生成时
var enemy = enemy_scene.instance()
enemy.init("dragon", 5)
add_child(enemy)

⚠️ 注意init() 方法需要在 add_child() 之前调用,否则 _ready() 中访问的变量可能未设置。

组(Groups)

组是将节点按功能分类的机制,类似于"标签"。

使用组

# 添加节点到组
$Player.add_to_group("players")
$Enemy.add_to_group("enemies")
$Enemy.add_to_group("damageable")

# 移除节点从组
$Enemy.remove_from_group("enemies")

# 检查节点是否在组中
if $Enemy.is_in_group("enemies"):
    print("是敌人")

通过组获取节点列表

# 获取组内所有节点
var enemies = get_tree().get_nodes_in_group("enemies")
print("敌人数量: ", enemies.size())

# 遍历敌人
for enemy in get_tree().get_nodes_in_group("enemies"):
    enemy.take_damage(10)

组调用(call_group)

# 对组内所有节点调用方法
get_tree().call_group("enemies", "take_damage", 50)
get_tree().call_group("players", "heal", 20)

# 延迟调用(当前帧结束后)
get_tree().call_group_flags(
    SceneTree.GROUP_CALL_DEFERRED,
    "enemies",
    "set_active",
    false
)

组的实际应用

# 敌人生成器
extends Node

func _ready() -> void:
    # 将所有敌人加入组
    for child in get_children():
        child.add_to_group("enemies")

# 伤害区域
extends Area2D

func _on_body_entered(body: Node) -> void:
    if body.is_in_group("damageable"):
        body.take_damage(25)
    if body.is_in_group("players"):
        body.collect_item()

# 存档系统
func save_all_enemies() -> Array:
    var data = []
    for enemy in get_tree().get_nodes_in_group("enemies"):
        data.append({
            "type": enemy.enemy_type,
            "position": enemy.global_position,
            "health": enemy.health,
        })
    return data

节点通信策略

策略一:信号(推荐)

# 适合:松耦合、跨场景通信
# Player.gd
signal health_changed(value)

func take_damage(amount: int) -> void:
    health -= amount
    emit_signal("health_changed", health)

# UI.gd(连接 Player 的信号)
$Player.connect("health_changed", self, "_update_health_bar")

策略二:直接调用

# 适合:紧耦合、父子关系
# Player.gd
func attack() -> void:
    $Weapon.fire()  # 直接调用子节点方法

# 跨节点调用
func _on_hit(damage: int) -> void:
    var game = get_node("/root/Main")
    game.on_player_hit(damage)

策略三:组调用

# 适合:广播消息
# 通知所有敌人玩家已死亡
get_tree().call_group("enemies", "on_player_died")

# 暂停所有敌人
get_tree().call_group("enemies", "set_active", false)

通信策略对比

策略 耦合度 适用场景 方向
信号 🟢 低 跨节点/场景 多对多
直接调用 🔴 高 父子/已知引用 一对一
组调用 🟢 低 广播/分类操作 一对多

场景管理器(Autoload)

Autoload 是全局单例,在项目启动时自动加载,用于管理全局状态和跨场景数据。

创建 Autoload

  1. 创建脚本 res://scripts/SceneManager.gd
  2. Project → Project Settings → Autoload
  3. 添加路径和名称

场景管理器实现

# res://scripts/SceneManager.gd
extends Node

var current_scene: Node = null
var scene_history: Array = []

func _ready() -> void:
    var root = get_tree().get_root()
    current_scene = root.get_child(root.get_child_count() - 1)

func goto_scene(path: String) -> void:
    # 记录历史
    scene_history.append(current_scene.filename)
    
    # 延迟切换(确保安全)
    call_deferred("_deferred_goto_scene", path)

func _deferred_goto_scene(path: String) -> void:
    # 释放当前场景
    current_scene.free()
    
    # 加载新场景
    var new_scene = load(path).instance()
    get_tree().get_root().add_child(new_scene)
    get_tree().set_current_scene(new_scene)
    current_scene = new_scene

func go_back() -> void:
    if scene_history.size() > 0:
        var previous = scene_history.pop_back()
        goto_scene(previous)

func reload_scene() -> void:
    goto_scene(current_scene.filename)

GameManager 示例

# res://scripts/GameManager.gd
extends Node

enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }

var state: int = GameState.MENU
var score: int = 0
var high_score: int = 0
var lives: int = 3
var current_level: int = 1

signal state_changed(new_state: int)
signal score_changed(new_score: int)

func change_state(new_state: int) -> void:
    state = new_state
    emit_signal("state_changed", new_state)
    
    match new_state:
        GameState.PLAYING:
            get_tree().paused = false
        GameState.PAUSED:
            get_tree().paused = true
        GameState.GAME_OVER:
            _check_high_score()

func add_score(points: int) -> void:
    score += points
    emit_signal("score_changed", score)

func lose_life() -> void:
    lives -= 1
    if lives <= 0:
        change_state(GameState.GAME_OVER)

func reset_game() -> void:
    score = 0
    lives = 3
    current_level = 1
    change_state(GameState.PLAYING)

func _check_high_score() -> void:
    if score > high_score:
        high_score = score

游戏开发场景

场景:动态生成无尽地图

extends Node2D

var chunk_scene = preload("res://scenes/MapChunk.tscn")
var chunk_size: float = 512.0
var active_chunks: Dictionary = {}
var render_distance: int = 3

func _process(delta: float) -> void:
    var player_chunk = _get_chunk_coord($Player.position)
    _update_chunks(player_chunk)

func _get_chunk_coord(pos: Vector2) -> Vector2:
    return Vector2(
        floor(pos.x / chunk_size),
        floor(pos.y / chunk_size)
    )

func _update_chunks(center: Vector2) -> void:
    # 需要加载的区块
    var needed_chunks = {}
    for x in range(-render_distance, render_distance + 1):
        for y in range(-render_distance, render_distance + 1):
            var coord = center + Vector2(x, y)
            needed_chunks[coord] = true
    
    # 加载新区块
    for coord in needed_chunks:
        if not active_chunks.has(coord):
            _load_chunk(coord)
    
    # 卸载远处区块
    for coord in active_chunks.keys():
        if not needed_chunks.has(coord):
            _unload_chunk(coord)

func _load_chunk(coord: Vector2) -> void:
    var chunk = chunk_scene.instance()
    chunk.position = coord * chunk_size
    chunk.init(coord)
    add_child(chunk)
    active_chunks[coord] = chunk

func _unload_chunk(coord: Vector2) -> void:
    active_chunks[coord].queue_free()
    active_chunks.erase(coord)

扩展阅读