第十四章:节点树架构

第十四章:节点树架构

"一切皆节点——理解节点树是掌握Godot的关键。"

节点树是Godot引擎的核心架构。本章将深入探讨节点的本质、层次关系、遍历方法,以及如何设计高效的节点架构。


14.1 节点基础

14.1.1 什么是节点

节点(Node)是Godot中所有游戏元素的基类:

# 节点的核心特性
# - 可以有名字(name)
# - 可以有父节点(parent)
# - 可以有子节点(children)
# - 可以处理回调(_ready, _process等)
# - 可以被添加到分组(groups)
# - 可以有附加的脚本(script)

14.1.2 常用2D节点类型

Node (所有节点的基类)
├── Node2D (2D节点基类)
│   ├── Sprite2D (精灵)
│   ├── AnimatedSprite2D (动画精灵)
│   ├── TileMap (瓦片地图)
│   ├── Camera2D (2D相机)
│   ├── CollisionObject2D
│   │   ├── Area2D (检测区域)
│   │   ├── StaticBody2D (静态物体)
│   │   ├── RigidBody2D (刚体)
│   │   └── CharacterBody2D (角色)
│   ├── Light2D (光源)
│   │   ├── PointLight2D
│   │   └── DirectionalLight2D
│   ├── Path2D (路径)
│   └── Marker2D (标记点)
├── CanvasItem (画布元素基类)
│   ├── Control (UI基类)
│   │   ├── Button, Label, TextEdit...
│   │   └── Container (容器)
│   └── Node2D (见上)
└── CanvasLayer (画布层)

14.1.3 节点属性

extends Node2D

func show_node_info():
    # 基本信息
    print("节点名称:", name)
    print("节点路径:", get_path())
    print("场景文件:", scene_file_path)
    
    # 层次关系
    print("父节点:", get_parent())
    print("子节点数:", get_child_count())
    print("索引位置:", get_index())
    
    # 状态信息
    print("在场景树中:", is_inside_tree())
    print("正在处理:", is_processing())
    print("可见:", visible)
    
    # Node2D 特有属性
    print("位置:", position)
    print("全局位置:", global_position)
    print("旋转:", rotation)
    print("缩放:", scale)

14.2 父子关系

14.2.1 添加子节点

# 基本添加
func _ready():
    var child = Node2D.new()
    child.name = "Child"
    add_child(child)
    
    # 添加并设置所有者(用于保存场景)
    add_child(child, true)  # force_readable_name
    child.owner = self  # 设置所有者

# 添加到特定位置
func add_at_position():
    var child = Sprite2D.new()
    add_child(child)
    move_child(child, 0)  # 移动到第一个位置

# 内部节点(编辑器中隐藏)
func add_internal():
    var internal_node = Node.new()
    add_child(internal_node, false, Node.INTERNAL_MODE_FRONT)

14.2.2 移除子节点

# 从树中移除(不销毁)
func remove_from_tree():
    var child = $Child
    remove_child(child)
    # child仍然存在,可以稍后添加回来

# 移除并销毁
func destroy_child():
    var child = $Child
    child.queue_free()  # 推荐:帧结束时销毁
    # 或者
    child.free()  # 立即销毁(谨慎使用)

# 移除所有子节点
func clear_children():
    for child in get_children():
        child.queue_free()

14.2.3 更改父节点

# reparent - Godot 4 新方法
func change_parent():
    var node = $Child
    var new_parent = $NewParent
    
    # 保持全局变换
    node.reparent(new_parent)
    
    # 不保持全局变换
    node.reparent(new_parent, false)

# 手动更改(旧方法)
func manual_reparent():
    var node = $Child
    var old_transform = node.global_transform
    
    var old_parent = node.get_parent()
    old_parent.remove_child(node)
    
    var new_parent = $NewParent
    new_parent.add_child(node)
    
    node.global_transform = old_transform  # 恢复变换

14.3 节点访问

14.3.1 路径访问

# $ 语法(NodePath简写)
func access_by_path():
    # 直接子节点
    var sprite = $Sprite2D
    
    # 嵌套子节点
    var weapon = $Body/Arm/Weapon
    
    # 父节点
    var parent = $".."
    
    # 兄弟节点
    var sibling = $"../Sibling"

# get_node()
func access_by_get_node():
    var sprite = get_node("Sprite2D")
    var weapon = get_node("Body/Arm/Weapon")
    var parent = get_node("..")
    
    # 绝对路径(从根开始)
    var player = get_node("/root/Main/Player")

# get_node_or_null() - 安全访问
func safe_access():
    var node = get_node_or_null("MayNotExist")
    if node:
        node.do_something()

14.3.2 @onready 延迟获取

extends Node2D

# 在_ready()之前获取节点引用
@onready var sprite: Sprite2D = $Sprite2D
@onready var collision: CollisionShape2D = $CollisionShape2D
@onready var animation: AnimationPlayer = $AnimationPlayer

# 可以使用表达式
@onready var player = get_tree().get_first_node_in_group("player")
@onready var screen_size = get_viewport_rect().size

func _ready():
    # 此时@onready变量已经初始化
    sprite.texture = load("res://icon.png")

14.3.3 遍历子节点

# 获取所有直接子节点
func iterate_children():
    for child in get_children():
        print(child.name)

# 按类型筛选
func find_by_type():
    for child in get_children():
        if child is Sprite2D:
            child.modulate = Color.RED

# 递归遍历所有后代
func iterate_all_descendants(node: Node):
    for child in node.get_children():
        print(child.name)
        iterate_all_descendants(child)

# 使用find_children
func find_with_pattern():
    # 查找所有名称匹配的节点
    var buttons = find_children("Button*", "Button", true, false)
    
    # 参数:模式, 类型, 递归, 只查找拥有的节点
    var sprites = find_children("*", "Sprite2D", true, false)

14.4 节点分组

14.4.1 分组基础

# 添加到分组
func _ready():
    add_to_group("enemies")
    add_to_group("damageable")
    add_to_group("saveable")

# 从分组移除
func remove_from_groups():
    remove_from_group("enemies")

# 检查是否在分组中
func check_group():
    if is_in_group("enemies"):
        print("这是敌人")

14.4.2 分组操作

# 获取分组中的所有节点
func get_all_enemies():
    var enemies = get_tree().get_nodes_in_group("enemies")
    return enemies

# 调用分组中所有节点的方法
func notify_all_enemies():
    get_tree().call_group("enemies", "on_alert")

# 带参数调用
func damage_all_enemies(amount: int):
    get_tree().call_group("enemies", "take_damage", amount)

# 调用标志
func call_with_flags():
    # CALL_GROUP_DEFERRED - 延迟调用
    get_tree().call_group_flags(
        SceneTree.GROUP_CALL_DEFERRED,
        "enemies",
        "die"
    )

14.4.3 分组最佳实践

# 定义分组常量
class_name Groups

const PLAYER = "player"
const ENEMY = "enemies"
const BULLET = "bullets"
const DAMAGEABLE = "damageable"
const SAVEABLE = "saveable"
const PAUSEABLE = "pauseable"

# 使用
func _ready():
    add_to_group(Groups.ENEMY)
    add_to_group(Groups.DAMAGEABLE)

# 实用函数
class_name GroupUtils

static func get_player() -> Player:
    var tree = Engine.get_main_loop()
    var players = tree.get_nodes_in_group(Groups.PLAYER)
    return players[0] if players.size() > 0 else null

static func get_nearest_enemy(from: Vector2) -> Node2D:
    var tree = Engine.get_main_loop()
    var enemies = tree.get_nodes_in_group(Groups.ENEMY)
    var nearest: Node2D = null
    var min_dist = INF
    
    for enemy in enemies:
        var dist = from.distance_to(enemy.global_position)
        if dist < min_dist:
            min_dist = dist
            nearest = enemy
    
    return nearest

14.5 节点处理

14.5.1 处理回调

extends Node2D

# 处理模式
func _ready():
    # 设置处理模式
    process_mode = PROCESS_MODE_PAUSABLE  # 默认,暂停时停止
    # PROCESS_MODE_ALWAYS - 总是处理
    # PROCESS_MODE_DISABLED - 禁用处理
    # PROCESS_MODE_INHERIT - 继承父节点

# 启用/禁用处理
func toggle_processing():
    set_process(true)       # 启用_process
    set_physics_process(true)  # 启用_physics_process
    set_process_input(true)    # 启用_input
    set_process_unhandled_input(true)  # 启用_unhandled_input

# 处理回调
func _process(delta: float) -> void:
    # 每帧调用(帧率相关)
    position.x += speed * delta

func _physics_process(delta: float) -> void:
    # 固定间隔调用(默认60次/秒)
    velocity = move_and_slide()

func _input(event: InputEvent) -> void:
    # 接收所有输入事件
    pass

func _unhandled_input(event: InputEvent) -> void:
    # 未处理的输入事件
    pass

14.5.2 处理顺序

# 同一层级的节点按照树中的顺序处理
# 可以调整处理优先级

func _ready():
    # 负数先处理,正数后处理
    process_priority = -1  # 优先处理
    
    # 子节点在父节点之后处理(默认)
    # 可以通过process_priority调整

# 顺序控制示例
# 输入管理器先处理
input_manager.process_priority = -100
# 物理先于渲染
physics_controller.process_priority = -10
# 渲染后处理
render_controller.process_priority = 10

14.6 节点生命周期

14.6.1 回调顺序详解

extends Node2D

# 1. 构造
func _init():
    # 节点对象创建时调用
    # 注意:此时节点未加入场景树
    print("1. _init")

# 2. 进入场景树
func _enter_tree():
    # 节点加入场景树时调用
    # 父节点先于子节点调用
    print("2. _enter_tree")

# 3. 就绪
func _ready():
    # 节点及其所有子节点都已进入场景树
    # 子节点先于父节点调用
    print("3. _ready")

# 4. 处理循环
func _process(delta):
    # 每帧调用
    pass

func _physics_process(delta):
    # 物理帧调用
    pass

# 5. 退出场景树
func _exit_tree():
    # 节点离开场景树时调用
    # 子节点先于父节点调用
    print("4. _exit_tree")

# 6. 通知
func _notification(what):
    match what:
        NOTIFICATION_PARENTED:
            print("获得父节点")
        NOTIFICATION_UNPARENTED:
            print("失去父节点")
        NOTIFICATION_PAUSED:
            print("游戏暂停")
        NOTIFICATION_UNPAUSED:
            print("游戏继续")

14.6.2 安全的初始化模式

extends Node2D

var _initialized: bool = false

func _ready():
    # 延迟初始化
    call_deferred("_deferred_ready")

func _deferred_ready():
    if _initialized:
        return
    _initialized = true
    
    # 安全的初始化代码
    setup_connections()
    load_data()
    initialize_state()

# 确保单次执行
func setup_connections():
    var player = get_tree().get_first_node_in_group("player")
    if player and not player.died.is_connected(_on_player_died):
        player.died.connect(_on_player_died)

14.7 节点树设计模式

14.7.1 组件模式

# 将功能拆分为独立的组件节点

# Player场景结构:
# Player (CharacterBody2D)
#   ├── Sprite2D
#   ├── CollisionShape2D
#   ├── HealthComponent
#   ├── MovementComponent
#   ├── WeaponComponent
#   └── AnimationComponent

# health_component.gd
class_name HealthComponent
extends Node

signal health_changed(current: int, maximum: int)
signal died

@export var max_health: int = 100
var current_health: int

func _ready():
    current_health = max_health

func take_damage(amount: int) -> void:
    current_health = max(0, current_health - amount)
    health_changed.emit(current_health, max_health)
    if current_health == 0:
        died.emit()

func heal(amount: int) -> void:
    current_health = min(max_health, current_health + amount)
    health_changed.emit(current_health, max_health)

# 在Player中使用
# player.gd
extends CharacterBody2D

@onready var health = $HealthComponent
@onready var movement = $MovementComponent

func _ready():
    health.died.connect(_on_died)

14.7.2 状态模式

# 使用子节点作为状态

# StateMachine
#   ├── IdleState
#   ├── RunState
#   ├── JumpState
#   └── AttackState

# state_machine.gd
class_name StateMachine
extends Node

var current_state: State
var states: Dictionary = {}

func _ready():
    for child in get_children():
        if child is State:
            states[child.name.to_lower()] = child
            child.state_machine = self
    
    # 设置初始状态
    change_state("idle")

func change_state(new_state: String) -> void:
    if current_state:
        current_state.exit()
    
    current_state = states.get(new_state)
    if current_state:
        current_state.enter()

func _process(delta):
    if current_state:
        current_state.update(delta)

func _physics_process(delta):
    if current_state:
        current_state.physics_update(delta)

14.7.3 工厂模式

# entity_factory.gd (Autoload)
class_name EntityFactory
extends Node

var _scenes: Dictionary = {}

func _ready():
    _preload_scenes()

func _preload_scenes():
    _scenes["player"] = preload("res://scenes/player.tscn")
    _scenes["enemy_goblin"] = preload("res://scenes/enemies/goblin.tscn")
    _scenes["enemy_orc"] = preload("res://scenes/enemies/orc.tscn")
    _scenes["bullet"] = preload("res://scenes/bullet.tscn")
    _scenes["explosion"] = preload("res://scenes/effects/explosion.tscn")

func create(type: String, parent: Node = null) -> Node:
    if not _scenes.has(type):
        push_error("未知实体类型:" + type)
        return null
    
    var instance = _scenes[type].instantiate()
    
    if parent:
        parent.add_child(instance)
    
    return instance

func create_at(type: String, pos: Vector2, parent: Node) -> Node:
    var instance = create(type, parent)
    if instance and instance is Node2D:
        instance.global_position = pos
    return instance

# 使用
func spawn_enemy():
    var enemy = EntityFactory.create_at("enemy_goblin", spawn_point, self)

14.8 实际案例

14.8.1 场景树管理器

# tree_manager.gd (Autoload)
extends Node

func find_by_class(root: Node, class_name: String) -> Array[Node]:
    var result: Array[Node] = []
    _find_by_class_recursive(root, class_name, result)
    return result

func _find_by_class_recursive(node: Node, class_name: String, result: Array[Node]):
    if node.get_class() == class_name:
        result.append(node)
    for child in node.get_children():
        _find_by_class_recursive(child, class_name, result)

func get_all_nodes() -> Array[Node]:
    var result: Array[Node] = []
    _get_all_recursive(get_tree().root, result)
    return result

func _get_all_recursive(node: Node, result: Array[Node]):
    result.append(node)
    for child in node.get_children():
        _get_all_recursive(child, result)

func print_tree(node: Node = null, indent: int = 0):
    if node == null:
        node = get_tree().root
    
    var prefix = "  ".repeat(indent)
    print(prefix + node.name + " (" + node.get_class() + ")")
    
    for child in node.get_children():
        print_tree(child, indent + 1)

14.8.2 节点缓存系统

# node_cache.gd
class_name NodeCache
extends RefCounted

var _cache: Dictionary = {}
var _root: Node

func _init(root: Node):
    _root = root

func get_node(path: String) -> Node:
    if _cache.has(path):
        var node = _cache[path]
        if is_instance_valid(node):
            return node
        else:
            _cache.erase(path)
    
    var node = _root.get_node_or_null(path)
    if node:
        _cache[path] = node
    return node

func invalidate(path: String = ""):
    if path.is_empty():
        _cache.clear()
    else:
        _cache.erase(path)

# 使用示例
var cache: NodeCache

func _ready():
    cache = NodeCache.new(self)

func update():
    var player = cache.get_node("World/Player")
    var ui = cache.get_node("UI/HUD")

本章小结

本章深入学习了Godot的节点树架构:

  1. 节点基础:节点类型、属性、层次结构
  2. 父子关系:添加、移除、更改父节点
  3. 节点访问:路径访问、@onready、遍历方法
  4. 节点分组:分组管理、组操作、最佳实践
  5. 节点处理:回调函数、处理顺序、优先级
  6. 生命周期:回调顺序、安全初始化
  7. 设计模式:组件模式、状态模式、工厂模式
  8. 实际案例:场景树管理、节点缓存

节点树架构是构建复杂游戏的基础,下一章将学习精灵与纹理。


上一章:2D场景系统

下一章:精灵与纹理

← 返回目录