本节摘要:游戏运行的本质是一个不断旋转的主循环——每帧按固定次序完成输入采集、物理步进、逻辑处理、渲染提交。Godot 把渲染、物理、音频等底层能力拆成若干"服务器",脚本通过 RID 间接引用它们,实现逻辑与硬件的解耦。本节拆解帧次序、两类处理回调的分工与服务器架构的设计逻辑。
一帧到底有多长?如果游戏跑在每秒六十帧,答案是大约十六点七毫秒。这十几毫秒里引擎要完成多少事?为什么有的代码写在处理回调里顺滑、挪到物理回调里就发抖?想回答这些问题,必须把主循环拆开看。
引擎启动时先做引导:装载配置、初始化各子系统、把主场景实例化挂上树根。随后控制权交给主循环,游戏进入"转圈"状态,直到退出。每一圈的工序次序基本固定:
次序里藏着三条实用的推论。推论一:输入先于一切逻辑,所以你在处理回调里读到的输入状态是本帧的,不会读到"未来"。推论二:物理先于普通逻辑,依赖碰撞结果的代码写在物理处理回调里,时序才对得上。推论三:渲染最后统一提交,你在逻辑里改十次材质属性,屏幕只呈现最后一次——中间态不会被玩家看见,这是天然的批量优化。
节点上有两个最常用的回调,对应两种时间观。
物理处理回调以固定频率触发,默认每秒六十次,与真实帧率脱钩。为什么必须固定?因为物理模拟对时间步长敏感——同样是"每帧前进三米",六十帧下是每秒一百八十米,掉到二十帧就变成每秒六十米,物体忽快忽慢。固定步长让物理结果可复现,联机同步也依赖这种确定性。
处理回调每帧触发一次,与帧率同步,适合一切"视觉跟随"类逻辑:镜头跟随、界面刷新、粒子发射。它的增量要乘上帧间隔系数,才能在不同帧率下速度一致。
extends CharacterBody2D # 两种节拍的实际分工 const SPEED := 300.0 # 像素每秒 var _velocity := Vector2.ZERO func _physics_process(delta: float) -> void: # 移动与碰撞属于物理世界,放固定节拍 _velocity.x = Input.get_axis(&"move_left", &"move_right") * SPEED move_and_slide() func _process(delta: float) -> void: # 相机平滑跟随属于视觉体验,放可变节拍 var target := position + Vector2(0, -40) %Camera.global_position = %Camera.global_position.lerp(target, 5.0 * delta)
⚠️ 常见坑:在处理回调里调用移动与碰撞方法。帧率一波动,角色速度跟着漂移,且碰撞检测的精细度随帧率变化,穿墙概率上升。判断口诀——碰物理世界用物理回调,碰眼睛用处理回调。

场景树是引擎露在明面上的半身,另一半藏在暗处——一组名字以 Server 结尾的单例:渲染服务器、物理服务器、音频服务器等等。它们负责真正对接图形接口与硬件。
设计上最关键的一招是 RID(资源标识号)间接引用。脚本世界里的精灵节点,在渲染服务器那边只对应一个编号;节点持有编号,服务器持有实体。你要给某个精灵换贴图,调用渲染服务器的接口,传入编号即可,双方从不直接持有对方的内存。
这种间接层换来三样东西。其一,线程解耦:渲染可以在独立线程上吃指令队列,主线程逻辑照跑不误,多核得以利用。其二,寿命可控:节点从树上摘下,它在服务器侧的资源可按策略延后回收,避免渲染到一半资源被抽走。其三,底层可替换:4.x 的渲染后端能同时支持数种图形接口,靠的正是脚本层只认编号不认后端——换后端,编号照旧,业务代码零改动。
理解这套架构对日常开发的直接影响是:凡是能用节点表达的,优先用节点;节点是对服务器能力的友好封装,绕过节点直接操作服务器是性能敏感场景的进阶手段(比如用渲染服务器批量画几万个粒子),不是常规操作。第 9 章性能优化会回到这一点。
补一段开头略过的启动过程。引擎进程拉起后先读项目配置,注册全局脚本单例,初始化各服务器,装载主场景,然后把控制权交给主循环。这段冷启动次序解释了两个常见现象:全局单例的初始化先于任何场景节点,所以它们适合放全局状态与跨场景服务;主场景完全装载后才进入第一帧,所以首帧前的繁重加载会造成"白屏片刻",需要用加载场景遮盖(第 8 章资源一节会给出方案)。
知道次序后,"卡顿"就有了定义:某一圈的工序总耗时超过了帧的时间上限,屏幕就晚一拍。优化于是不是玄学调参,而是把每一圈的成本摊到各工序里去看。粗略的预算观是:物理与脚本逻辑吃处理器时间,渲染提交吃图形接口与显存带宽,两者性质不同、治理手段也不同——前者靠减少每帧计算量(缓存、降频、池化),后者靠减少绘制批次与材质切换(第 9 章展开)。养成"这行代码吃的是哪段预算"的直觉,是从会写逻辑到会做优化的分水岭。
顺带解释一个高频疑问:为什么把一段重计算从处理回调挪到按钮点击的回调里,帧率立刻稳了?因为处理回调每帧都跑,点击回调只在事件发生时跑。预算没变,跑的频率变了。这类"频率错位"造成的性能问题,在引擎里比比皆是,识别它们的第一步永远是问:这段代码每帧跑几次?
纸上谈完,两个一分钟级别的实验能把这些机理变成体感。实验一:在任意脚本的处理回调里打印帧间隔,观察数字随场景复杂度的波动;再在物理处理回调里打印同样的值,观察它纹丝不动。两个数字的对比,比任何文字都直观地展示"可变帧"与"固定步"的分工。
实验二:在处理回调里写一个百万次的空循环,观察帧率骤降但画面仍在更新;再在渲染设置里把垂直同步关掉,观察帧率上限的变化。前者证明逻辑耗时吃的是帧预算,后者证明帧率上限由呈现节奏决定。做完这两个实验,"卡"这个字就从模糊的感受变成了一组可测量的变量——这是后续一切性能话题的地基(第 9 章性能诊断会正式展开工具链)。
时间轴看完了,下一节转向空间维度:对象与资源的生老病死怎么记账。