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

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

节点与场景树

节点类型概览

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

2D 节点

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

物理节点

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

UI 节点(Control)

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

通用节点

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

3D 节点

节点类型功能
Spatial3D 基础节点
MeshInstance3D 网格显示
Camera3D 相机
DirectionalLight方向光
OmniLight点光源
SpotLight聚光灯
RigidBody3D 刚体
KinematicBody3D 运动学体
CSGBox / CSGSphereCSG 几何体

场景树结构

场景树是 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)

扩展阅读