第十四章:节点树架构
"一切皆节点——理解节点树是掌握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的节点树架构:
- 节点基础:节点类型、属性、层次结构
- 父子关系:添加、移除、更改父节点
- 节点访问:路径访问、@onready、遍历方法
- 节点分组:分组管理、组操作、最佳实践
- 节点处理:回调函数、处理顺序、优先级
- 生命周期:回调顺序、安全初始化
- 设计模式:组件模式、状态模式、工厂模式
- 实际案例:场景树管理、节点缓存
节点树架构是构建复杂游戏的基础,下一章将学习精灵与纹理。
上一章:2D场景系统
下一章:精灵与纹理