第二部分:Godot 的运行结构:SceneTree、节点生命周期、脚本、信号

2026-05-19
429715 分钟
...

这一部分很重要。你后面做 RPG、地图、角色移动、天气、四季、怪物、交互、UI,都会不断碰到这几个概念:

Node 节点

Scene 场景

SceneTree 场景树

Script 脚本

Signal 信号

可以先把 Godot 理解成:

一个游戏 = 一棵正在运行的树
树上的每个对象 = Node
一组节点保存成文件 = Scene
场景运行后挂到总树上 = SceneTree
节点之间通过 Signal 通信

Godot 官方文档也把 nodes、scenes、scene tree、signals 归为最核心的概念:节点是游戏的最小构建块,多个节点组合成场景,场景再嵌入场景树,信号用于让不同节点响应彼此发生的事件。(Godot Engine documentation)

1. SceneTree 是什么

SceneTree 可以理解为 Godot 游戏运行时的“总 DOM 树”。

你是前端开发,可以这样类比:

前端页面
document
└── app
    ├── header
    ├── main
    └── button

Godot 游戏
SceneTree
└── Main
    ├── Player
    ├── TileMapLayer
    ├── Camera2D
    └── CanvasLayer

在前端里,元素必须挂到 DOM 上,才会被浏览器渲染和参与交互。

在 Godot 里,节点必须进入 SceneTree,才会真正“活过来”:能显示、能处理输入、能播放声音、能执行 _process()、能参与物理、能发出和接收通知。官方文档说明,节点连接到 root viewport 后就成为 scene tree 的一部分,并会获得 _enter_tree()_ready()_exit_tree() 等回调。(Godot Engine documentation)

2. Scene 场景是什么

Scene 不只是“地图场景”那个场景,它更像是一个可复用的节点组合模板

比如:

Player.tscn
└── Player CharacterBody2D
    ├── Sprite2D
    ├── CollisionShape2D
    ├── AnimationPlayer
    └── Camera2D

这个 Player.tscn 是一个场景。

它既可以是一个完整关卡,也可以只是一个角色、一个怪物、一个箱子、一个按钮、一张卡牌、一个 UI 面板。

Godot 的场景本质上是由节点组成的树结构。官方文档里也说,当你把节点组织成一棵树并保存后,这个结构就叫 scene;保存后的 scene 又可以像新的节点类型一样添加到其他节点下面。(Godot Engine documentation)

也就是说,Godot 的 Scene 很像前端组件:

前端组件
<Player />

Godot 场景
Player.tscn

你可以把一个怪物做成 Slime.tscn,以后地图里要几个史莱姆,就实例化几个。

3. Root、Current Scene 和 Main Scene

Godot 运行游戏时,会有一个最顶层的 root。

大致结构可以理解为:

/root
└── Main
    ├── World
    ├── Player
    ├── UI
    └── AudioManager

/root 是最顶层。

Main 通常是你的主场景。

PlayerWorldUI 都是 Main 的子节点。

在项目设置里,你会设置一个 Main Scene,也就是游戏启动时第一个加载的场景。

比如你的 RPG 项目可能是:

Main.tscn
└── Main Node2D
    ├── World Node2D
    │   └── TileMapLayer
    ├── Player CharacterBody2D
    ├── Camera2D
    ├── WeatherManager Node
    ├── TimeManager Node
    └── UI CanvasLayer

这就像前端里的入口文件:

createApp(App).mount('#app')

Godot 的 Main Scene 就有点像这里的 App.vueApp.tsx

4. get_tree() 是什么

在任意已经进入 SceneTree 的节点里,都可以用:

get_tree()

拿到当前游戏的 SceneTree。

常见用途:

get_tree().quit()

退出游戏。

get_tree().paused = true

暂停游戏。

get_tree().change_scene_to_file("res://scenes/main_menu.tscn")

切换场景。

官方文档提到,SceneTree 管理节点层级和场景本身,可以添加、获取、移除节点,也可以暂停整棵场景树、加载、切换和重载场景。(Godot Engine documentation)

你可以把 get_tree() 理解成前端里访问全局应用实例或路由系统:

router.push('/menu')
app.unmount()

Godot 里则是:

get_tree().change_scene_to_file("res://menu.tscn")
get_tree().quit()

5. 节点生命周期

这是 Godot 必须熟的内容。

一个节点从创建到销毁,大概经历这些阶段:

创建节点

进入 SceneTree

_enter_tree()

子节点也进入 SceneTree

_ready()

_process(delta)

_physics_process(delta)

退出 SceneTree

_exit_tree()

销毁

最常见的几个生命周期方法是:

func _enter_tree():
    pass

func _ready():
    pass

func _process(delta):
    pass

func _physics_process(delta):
    pass

func _exit_tree():
    pass

6. _enter_tree()

_enter_tree() 表示节点刚刚进入 SceneTree。

这时候节点已经进入树了,但它的子节点不一定都准备好了。

所以你一般不会在这里频繁获取子节点。

例如:

func _enter_tree():
    print("我进入场景树了")

生命周期顺序上,父节点的 _enter_tree() 会先执行,然后才是子节点的 _enter_tree()。官方文档说明,节点加入 SceneTree 时,会先触发父节点 _enter_tree(),再触发子节点 _enter_tree()。(Godot Engine documentation)

简单理解:

_enter_tree 更早
_ready 更安全

初学阶段,很多初始化逻辑直接放 _ready() 就行。

7. _ready()

_ready() 是最常用的初始化函数。

它表示:这个节点和它的子节点基本都准备好了。

例如:

@onready var sprite: Sprite2D = $Sprite2D

func _ready():
    sprite.visible = true

为什么这里可以用 $Sprite2D

因为 _ready() 的时候,子节点已经在树里了,比较适合获取子节点引用。官方文档说明,使用 get_node() 获取子节点时,放在父节点的 _ready() 中可以保证子节点已经存在于场景树里。(Godot Engine documentation)

前端类比:

Godot _ready()
Vue mounted()
React useEffect(() => {}, [])

比如:

func _ready():
    print("角色初始化完成")

适合写:

获取子节点
初始化变量
绑定信号
设置初始状态
读取配置
播放开场动画

8. _process(delta)

_process(delta) 每帧执行一次。

比如游戏 60 FPS,它大概一秒执行 60 次。

func _process(delta):
    rotation += 1.0 * delta

delta 表示上一帧到这一帧经过了多少秒。

为什么要乘以 delta

因为不同电脑帧率不同,如果不乘 delta,高帧率电脑会动得更快,低帧率电脑会动得更慢。

比如:

position.x += 100 * delta

意思是:

每秒移动 100 像素

而不是:

每帧移动 100 像素

官方文档说明,普通 _process() 会尽可能快地按帧率执行,并且会把处理时间差 delta 作为参数传入。(Godot Engine documentation)

适合放在 _process() 里的内容:

普通动画
UI 数值渐变
相机平滑跟随
天气视觉效果
计时器显示
不依赖物理碰撞的视觉变化

不太适合放:

角色物理移动
碰撞检测
刚体/物理相关逻辑

9. _physics_process(delta)

_physics_process(delta) 是固定频率执行的物理更新。

Godot 默认物理频率通常是每秒 60 次。官方文档说明,_physics_process() 以固定次数执行,默认 60 次/秒,适合和物理引擎相关的代码。(Godot Engine documentation)

角色移动通常写在这里。

例如 2D RPG 角色移动:

extends CharacterBody2D

@export var speed: float = 120.0

func _physics_process(delta):
    var direction = Vector2.ZERO

    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_down"):
        direction.y += 1
    if Input.is_action_pressed("move_up"):
        direction.y -= 1

    direction = direction.normalized()
    velocity = direction * speed
    move_and_slide()

这里有几个关键点:

direction 是方向。

velocity 是速度。

move_and_slide() 是让 CharacterBody2D 按速度移动,并处理碰撞滑动。

direction.normalized() 是为了防止斜向移动更快。

比如同时按右和下:

Vector2(1, 1)

如果不归一化,斜向长度会比单方向长,角色斜着走会更快。

10. _process 和 _physics_process 的区别

可以这样记:

_process
每一帧执行适合视觉UI非物理逻辑

_physics_process
固定频率执行适合角色移动碰撞物理逻辑

前端类比:

_process
requestAnimationFrame

_physics_process
固定 tick 的游戏循环

更直接一点:

角色移动优先 _physics_process
天气粒子可以 _process
UI 数字变化可以 _process
碰撞相关优先 _physics_process

11. _exit_tree()

_exit_tree() 表示节点离开 SceneTree。

比如:

func _exit_tree():
    print("节点离开场景树")

适合做一些清理工作:

取消连接
清理引用
停止计时
保存状态

但是大部分初学阶段不会频繁写 _exit_tree()

12. queue_free()

销毁节点常用:

queue_free()

例如敌人死亡:

func die():
    queue_free()

queue_free() 不是立即销毁,而是把节点加入删除队列,等当前帧处理结束后再安全删除。

官方文档也推荐通常使用 queue_free(),因为 free() 是立即销毁,可能让已有引用瞬间变成无效状态;而 queue_free() 会在当前帧结束时删除,更安全。(Godot Engine documentation)

比如子弹碰到墙:

func _on_body_entered(body):
    queue_free()

敌人血量归零:

func take_damage(amount: int):
    hp -= amount

    if hp <= 0:
        queue_free()

注意:删除一个父节点时,它的子节点也会一起被删除。官方文档说明,释放一个节点也会释放它的所有子节点,所以删除一整个分支时,只需要删除最上层父节点。(Godot Engine documentation)

13. 获取节点:get_node 和 $

Godot 中经常需要获取子节点。

比如结构是:

Player
├── Sprite2D
├── CollisionShape2D
└── AnimationPlayer

可以这样写:

func _ready():
    var sprite = get_node("Sprite2D")
    var anim = get_node("AnimationPlayer")

也可以简写:

func _ready():
    var sprite = $Sprite2D
    var anim = $AnimationPlayer

$Sprite2D 本质上就是 get_node("Sprite2D") 的简写。官方文档也说明,GDScript 中 $get_node() 的简写。(Godot Engine documentation)

更推荐这样写:

@onready var sprite: Sprite2D = $Sprite2D
@onready var anim: AnimationPlayer = $AnimationPlayer

func _ready():
    anim.play("idle")

@onready 的意思是:等节点 ready 之前再初始化这个变量。

如果你直接这样写:

var sprite = $Sprite2D

有时候会太早,因为节点还没 ready。

所以常见写法是:

@onready var sprite = $Sprite2D

这个非常常用,要记住。

14. NodePath 节点路径

节点可以通过路径查找。

假设结构:

UI
└── HealthBar
    └── Label

可以这样拿:

@onready var label = $HealthBar/Label

也可以:

@onready var label = get_node("HealthBar/Label")

类似文件路径:

./HealthBar/Label

也可以用 .. 找父级:

get_node("../Player")

不过初学阶段不建议大量用 ..

因为这会让节点之间耦合很重。

比如 UI 去找 Player:

get_node("../Player")

这意味着只要层级一变,代码就挂。

更好的方式通常是:

信号
分组
全局管理器
导出变量

这些后面慢慢讲。

15. 创建节点 add_child()

你可以在代码里动态创建节点。

例如创建一个 Sprite2D:

func _ready():
    var sprite = Sprite2D.new()
    add_child(sprite)

new() 是创建对象。

add_child() 是把它挂到当前节点下面。

如果不 add_child(),这个节点只是内存里的对象,不会出现在场景树里,也不会显示。

官方文档说明,通过代码创建节点时,可以调用节点类型的 new() 方法,然后用 add_child() 添加为当前节点的子节点。(Godot Engine documentation)

前端类比:

const div = document.createElement('div')
document.body.appendChild(div)

Godot 里:

var node = Node2D.new()
add_child(node)

16. 实例化场景 instantiate()

比起动态创建单个节点,更常见的是动态实例化一个场景。

比如你有一个怪物场景:

Slime.tscn
└── Slime CharacterBody2D
    ├── Sprite2D
    ├── CollisionShape2D
    └── AnimationPlayer

你可以在地图里生成它:

var slime_scene = preload("res://scenes/Slime.tscn")

func spawn_slime():
    var slime = slime_scene.instantiate()
    add_child(slime)
    slime.global_position = Vector2(200, 120)

这里分两步:

var slime_scene = preload("res://scenes/Slime.tscn")

先加载场景资源。

var slime = slime_scene.instantiate()

再创建一个实例。

官方文档说明,实例化场景通常是两步:先加载本地场景文件,再通过 PackedScene.instantiate() 创建实际节点树,然后把它作为子节点加入当前节点。(Godot Engine documentation)

这个在游戏里极其常用:

生成敌人
生成子弹
生成掉落物
生成卡牌
生成特效
生成对话框
生成伤害数字

比如伤害数字:

var damage_text_scene = preload("res://ui/DamageText.tscn")

func show_damage(value: int, pos: Vector2):
    var text = damage_text_scene.instantiate()
    add_child(text)
    text.global_position = pos
    text.set_value(value)

17. 信号 Signal 是什么

Signal 是 Godot 里非常重要的通信机制。

它可以理解成:

事件
EventEmitter
自定义事件
观察者模式

官方文档说明,信号是节点在某件事发生时发出的消息,例如按钮被按下;其他节点可以连接这个信号,并在事件发生时调用函数。信号能降低对象之间的耦合,让代码更灵活。(Godot Engine documentation)

前端类比:

button.addEventListener('click', () => {
  console.log('clicked')
})

Godot 里:

button.pressed.connect(_on_button_pressed)

func _on_button_pressed():
    print("按钮被点击")

18. 内置信号

很多节点自带信号。

比如 Button 有:

pressed
button_down
button_up

Area2D 有:

body_entered
body_exited
area_entered
area_exited

Timer 有:

timeout

比如 Timer:

@onready var timer: Timer = $Timer

func _ready():
    timer.timeout.connect(_on_timer_timeout)

func _on_timer_timeout():
    print("时间到了")

比如 Button:

@onready var start_button: Button = $StartButton

func _ready():
    start_button.pressed.connect(_on_start_button_pressed)

func _on_start_button_pressed():
    print("开始游戏")

19. 自定义信号

你也可以自己定义信号。

比如玩家血量变化:

extends CharacterBody2D

signal health_changed(current_hp: int, max_hp: int)
signal died

@export var max_hp: int = 100
var current_hp: int = 100

func take_damage(amount: int):
    current_hp -= amount
    current_hp = max(current_hp, 0)

    health_changed.emit(current_hp, max_hp)

    if current_hp <= 0:
        died.emit()
        queue_free()

UI 可以监听玩家血量变化:

@export var player: Node

func _ready():
    player.health_changed.connect(_on_player_health_changed)

func _on_player_health_changed(current_hp: int, max_hp: int):
    print("当前血量:", current_hp, "/", max_hp)

这样 UI 不需要每一帧去问:

player.current_hp 是多少

而是玩家血量变化时主动通知 UI。

这就是信号的核心思想:

不要一直查
发生变化时通知

20. 为什么 Signal 很适合游戏

比如你的 RPG 游戏里会有这些情况:

玩家血量变化UI 血条更新
玩家进入房子切换室内地图
玩家靠近 NPC显示交互提示
怪物死亡掉落物品
任务完成更新任务面板
天气变化地图视觉效果变化
时间到晚上点亮灯光
按钮点击打开背包
卡牌使用触发技能效果

如果全部互相 get_node(),代码会变得很乱。

比如:

player.get_node("../UI/HealthBar").update_hp()

这种写法短期能跑,但项目大了会越来越难维护。

更好的做法是:

Player 发出 health_changed
UI 自己监听 health_changed

这样 Player 不需要知道 UI 在哪里。

UI 也不需要每帧轮询 Player。

这就是低耦合。

21. 一个完整小例子:玩家受伤,UI 自动更新

场景结构:

Main
├── Player
└── UI
    └── HPLabel

Player.gd:

extends CharacterBody2D

signal health_changed(current_hp: int, max_hp: int)

@export var max_hp: int = 100
var current_hp: int = 100

func take_damage(amount: int):
    current_hp -= amount
    current_hp = clamp(current_hp, 0, max_hp)
    health_changed.emit(current_hp, max_hp)

UI.gd:

extends CanvasLayer

@onready var hp_label: Label = $HPLabel
@export var player: Node

func _ready():
    player.health_changed.connect(_on_player_health_changed)
    _on_player_health_changed(player.current_hp, player.max_hp)

func _on_player_health_changed(current_hp: int, max_hp: int):
    hp_label.text = "HP: %d / %d" % [current_hp, max_hp]

Main 里把 Player 拖给 UI 的 player 导出变量。

这样结构就很清楚:

Player 管血量
UI 管显示
Signal 负责通知

22. @export 是什么

@export 可以把脚本变量暴露到 Godot 编辑器的 Inspector 面板。

例如:

@export var speed: float = 120.0
@export var max_hp: int = 100

这样你不用改代码,就能在 Inspector 里调参数。

这对做游戏很重要。

比如:

@export var move_speed: float = 120.0
@export var jump_force: float = 300.0
@export var attack_damage: int = 10

以后你调角色速度、怪物血量、攻击力,不需要每次进代码改。

前端类比的话,@export 有点像组件 props + 可视化配置面板:

<Player speed={120} hp={100} />

Godot 里是:

@export var speed = 120
@export var hp = 100

然后在 Inspector 里直接调。

23. @onready 是什么

@onready 表示变量等到节点 ready 阶段再初始化。

常见写法:

@onready var sprite: Sprite2D = $Sprite2D
@onready var anim: AnimationPlayer = $AnimationPlayer
@onready var collision: CollisionShape2D = $CollisionShape2D

为什么不用普通 var

因为普通变量初始化可能发生得太早,子节点还没准备好。

@onready 就是告诉 Godot:

等当前节点即将执行 _ready() 再去找这些子节点

这在 Godot 里非常常见,基本每个脚本都会用。

24. 推荐你现在形成的习惯

写角色脚本时,优先这样组织:

extends CharacterBody2D

@export var speed: float = 120.0

@onready var sprite: Sprite2D = $Sprite2D
@onready var anim: AnimationPlayer = $AnimationPlayer

func _ready():
    pass

func _physics_process(delta):
    handle_movement()

func handle_movement():
    pass

写 UI 脚本时,优先这样组织:

extends CanvasLayer

@onready var hp_label: Label = $HPLabel

func _ready():
    pass

func update_hp(current_hp: int, max_hp: int):
    hp_label.text = "HP: %d / %d" % [current_hp, max_hp]

写管理器时,优先这样组织:

extends Node

signal time_changed(hour: int, minute: int)

var hour: int = 8
var minute: int = 0

func advance_time():
    minute += 10

    if minute >= 60:
        minute = 0
        hour += 1

    time_changed.emit(hour, minute)

25. 这一部分最重要的记忆点

SceneTree 是正在运行的总节点树。

Scene 是可保存、可复用的节点组合。

Node 是 Godot 里最基础的对象。

_ready() 适合初始化和获取子节点。

_process(delta) 适合每帧视觉更新。

_physics_process(delta) 适合角色移动和物理逻辑。

queue_free() 是安全删除节点的常用方式。

$NodeNameget_node("NodeName") 的简写。

@onready 用来等节点准备好后再获取子节点。

@export 用来把变量暴露到 Inspector。

signal 用来让节点之间低耦合通信。

26. 你现在可以做的小练习

建一个简单场景:

Main
├── Player CharacterBody2D
│   ├── Sprite2D
│   └── CollisionShape2D
└── UI CanvasLayer
    └── Label

目标:

1. Player hp
2. 按空格让 Player 扣血
3. Player 发出 health_changed 信号
4. UI 监听信号并更新 Label

这个练习做完,你就真正摸到 Godot 的核心了。

因为后面的 RPG 项目,本质上就是不断重复这些模式:

节点组织
生命周期初始化
输入处理
状态变化
信号通知
UI 更新
场景实例化
节点销毁

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
godot2026-07-27
Godot 4 常用 UI 节点详解
Godot 4 常用 UI 节点详解 在 Godot 4 中,UI 系统基于 Control(控件) 节点构建。所有 UI 节点都继承自 Control,形成一棵完整的 UI 树。与游戏引擎中常见的"Canvas + DOM"模式不同,Godot 的 UI 系统是声明式的——你在场景中搭好节点树,引擎自动完成布局计算。 一、布局系统:Container 家族 Container 是 Godot UI 的 骨架 。它决定了子节点的大小和位...
学习
godot2026-07-09
Godot 4 自动地形系统(AutoTileSet / Terrains)完全指南
前言 在 2D 游戏开发中,地形瓦片(tile)的拼接是一个绕不开的问题。想象一下:你有一片草地、一条河流、一段平台——如果每一块边缘、角落、过渡区域都要手动选择对应的瓦片图,工作量将是巨大的。 自动地形系统 就是为了解决这个问题而生的。 一、什么是自动地形(Autotiling)? 自动地形的核心思想很简单: 你只管画,引擎帮你选对瓦片 。 当你在 TileMap 上绘制地形时,引擎会自动检测每个瓦片的上下左右邻居,然后根据预设的规则...
学习
godot2026-06-25
Godot 中 zindex 和 ysort 的区别总结
在 Godot 2D 游戏开发中,角色、树木、怪物、地面、技能特效、UI 都需要正确的显示顺序。比如角色走到树前面时,角色应该挡住树;角色走到树后面时,树又应该挡住角色。 这种显示顺序主要和两个概念有关: 和 。 其中 用来手动控制图层顺序, 用来根据物体的 Y 坐标自动排序。 一句话理解 是手动分层。 是根据 Y 坐标自动排序。 简单来说: | 属性 | 作用 | 适合场景 | | | | | | | 数值越大,显示越靠前 | 地面、...
学习
godot2026-06-11
Tileset 资源图的标准和规范
一、什么是 Tileset 资源图 Tileset,中文通常叫“图块资源图”或“瓦片图”,是 2D 游戏中非常常见的一种地图资源组织方式。 简单来说,Tileset 就是把很多小图块按照固定尺寸排列在一张图片里。游戏引擎会按照固定的格子大小去切割这张图片,然后把每个小格子当成一个独立的地图块使用。 比如一个 32×32 像素的 Tileset 中,每一个 tile 都是 32×32 像素。地图编辑器或游戏引擎会按照 32×32 的网格,...
学习
godot2026-05-29
用户角色精灵图制作角色的完整流程
在 2D RPG 游戏里,角色通常不是用一张单独图片完成的,而是用一张“角色精灵图”来做。 所谓角色精灵图,通常是一张包含多个动作帧的大图。比如角色向下走有 4 帧,向左走有 4 帧,向右走有 4 帧,向上走有 4 帧。Godot 会根据这些帧不断切换图片,看起来角色就动起来了。 这篇文章主要介绍:拿到一张角色精灵图之后,如何在 Godot 中把它做成一个可以正常移动、播放动画、和地图产生遮挡关系的角色。 一、先理解角色精灵图是什么 角...
学习
godot2026-05-26
Godot 节点系统详细介绍
Godot 里最核心的东西不是“类”,也不是“组件”,而是 节点 Node 。 你可以把 Godot 的节点理解成: 在前端里,一个页面是由很多 DOM 元素组成的; 在 Godot 里,一个游戏场景是由很多 Node 节点组成的。 比如一个玩家角色,可能不是一个单独对象,而是这样的结构: 这里的 是根节点,下面挂着显示图片、播放动画、碰撞检测、摄像机、音效等子节点。 Godot 官方文档也把节点和场景放在一起讲:多个节点组成树状结构后...
学习

评论

请登录后发表评论

去登录
加载评论中...