许多新手把逻辑全塞进
_process,结果卡顿、逻辑乱序。本文拆解 Godot 节点生命周期:加载、就绪、物理帧、普通帧、输入、退出,并给出"放哪写什么"的范式与性能建议。
核心概念速览
- _ready():节点进入场景树后调用一次,适合初始化、找子节点、连接信号。
- _process(delta):每帧(受 fps 影响)执行,适合与显示相关、非物理逻辑。
- _physics_process(delta):固定频率(默认 60Hz)执行,适合运动、碰撞计算。
- _input(event):原始输入事件回调,UI 可能拦截。
- _unhandled_input(event):UI 未消费的输入才会来到此处,适合游戏全局输入。
- _exit_tree() / _notification:节点从树中移除/销毁时触发,用于解绑、保存。
心法:运动/碰撞放
_physics_process,视觉/插值放_process,初始化放_ready,输入优先_unhandled_input。
生命周期顺序(常见调用链)
- 构造脚本(
_init可选)。 - 进入场景树 →
_enter_tree→_ready。 - 每帧:
_process→_physics_process按引擎节奏交替。 - 输入事件
_input→ UI →_unhandled_input。 - 离开场景树:
_exit_tree→queue_free清理资源。
各阶段写什么
_ready:初始化、连接、查找
gdscript
@onready var anim := $AnimationPlayer
func _ready():
anim.play("idle")
$Area2D.body_entered.connect(_on_body_entered)
- 不要在
_init里找节点,场景尚未装配。 - 导出变量在此安全使用。
_process:视觉/插值/计时
gdscript
func _process(delta):
sprite.modulate = sprite.modulate.lerp(Color.WHITE, 0.1)
ui_time += delta
label.text = format_time(ui_time)
- 不要在这里做碰撞运算,避免 fps 变化导致穿透。
_physics_process:运动与碰撞
gdscript
func _physics_process(delta):
velocity.y += gravity * delta
move_and_slide()
- 使用
move_and_slide/move_and_collide等物理 API,保证稳定性。 - 粒度:只放必须的物理计算,其他逻辑分层。
输入处理:_input vs _unhandled_input
- UI 项目:优先
_unhandled_input,避免与按钮焦点冲突。
gdscript
func _unhandled_input(event):
if event.is_action_pressed("pause"):
toggle_pause()
get_viewport().set_input_as_handled()
- 需要原始鼠标移动(如相机)的放
_input。
退出与清理:_exit_tree
gdscript
func _exit_tree():
SignalsBus.enemy_spawned.disconnect(_on_enemy_spawned)
- 解绑全局信号/计时器,释放引用避免内存泄漏。
常见坑与规避
- 把物理放在 _process :fps 波动导致速度不稳甚至穿透;改用
_physics_process。 - 在 _ready 前访问节点 :
null instance报错;改用@onready或放到_ready。 - 重复 connect :在
_process里连接信号会导致多次绑定;连接操作放_ready,必要时先disconnect。 - 忘记 queue_free :动态实例不释放导致泄漏;场景切换时用
queue_free或call_deferred("free")。 - 输入被 UI 吃掉 :把核心输入监听放
_unhandled_input,或在_input调用accept_event()。
性能小贴士
- 减少每帧分配:在
_process内避免频繁创建对象(如 new Vector2),可复用向量或在类成员预存。 - 频繁逻辑分级:高频逻辑
_physics_process,中频逻辑用 Timer,低频逻辑用yield/await或状态机驱动。 - 关闭未用的处理:
set_process(false)、set_physics_process(false)关闭回调,UI 场景静态时尤为有效。
模板:角色控制器的分层
gdscript
func _ready():
_init_inputs()
_init_signals()
func _physics_process(delta):
_update_gravity(delta)
_update_move(delta)
_apply_motion()
func _process(delta):
_update_animations(delta)
_update_ui(delta)
- 物理与表现分开,方便调试;动画跟随状态,不直接写在物理段。
调试生命周期顺序
- 打印顺序:在各回调
print,运行一次即可确认调用次序。 - 调试器 → Monitors → Frame Time 观察
_process和_physics_process是否被启用。 - Remote SceneTree 查看节点是否已经进入场景树。
总结复盘
- 分三类:初始化
_ready,物理_physics_process,表现_process,输入_unhandled_input,清理_exit_tree。 - 避免把所有逻辑塞进
_process;关闭不需要的处理;信号连接放_ready,解绑放_exit_tree。 - 掌握这些生命周期范式,能让脚本结构更清晰、性能更稳,不再因为帧循环乱写导致卡顿。
