Node._process
最后同步日期:2026-04-16 | Godot 官方原文 — Node._process
Node._process
定义
想象你在看一部电影——电影其实就是一帧一帧的画面快速闪过,每秒钟闪过 60 帧(60 FPS),你的眼睛就会觉得画面是"连续流动"的。Godot 引擎的工作原理和电影一模一样:它每秒钟会把整个游戏画面重新画很多次,每一次重新画就叫做"一帧"。
_process 就是"每一帧画出来之前"引擎自动调用的一段代码。也就是说,引擎每画一帧,就会执行一次你的 _process 方法。如果你的游戏跑在 60 FPS,那么 _process 大约每秒会被调用 60 次。
参数 delta 的意思是"距离上一次调用过去了多少秒"。打个比方:你和朋友约好每隔一段时间报告一下自己的位置,delta 就是你上次报告到这次报告之间隔了多长时间。60 FPS 时,delta 大约是 0.0167 秒(即 1/60 秒)。但 delta 不是固定不变的——如果某一帧卡了一下,delta 就会变大;如果电脑性能好帧率很高,delta 就会很小。
你可以在 _process 里做的事情包括:
- 更新 UI 显示(血条、分数、倒计时等)
- 让角色或物体移动(非物理相关的简单移动)
- 播放动画、粒子特效
- 检测输入并做出响应
- 执行倒计时逻辑
一句话总结
_process 就像闹钟的"滴答声"——每响一次,你就做一件事。delta 告诉你距离上次滴答过了多久,帮你算准每一步的量。
函数签名
public override void _Process(double delta)func _process(delta: float) -> void参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
delta | C#: double / GDScript: float | 是 | 距离上一帧经过的时间,单位是秒。60 FPS 时约为 0.0167 秒。它不是固定值,会随实际帧率波动。 |
返回值
无返回值(void)。
代码示例
基础用法
最简单的用法——在 _process 中使用 delta 让一个数字随时间累加,观察每帧的 delta 值:
using Godot;
public partial class DeltaDemo : Node
{
// 内部变量:累计经过的总时间
private double _elapsedTime = 0.0;
public override void _Process(double delta)
{
// delta 是距离上一帧经过的秒数
// 把每一帧的 delta 累加起来,就得到了总运行时间
_elapsedTime += delta;
// 每 60 帧大约打印一次(大约每秒一次)
if (_elapsedTime >= 1.0)
{
GD.Print($"已经过了 {_elapsedTime:F2} 秒");
// 运行结果: 已经过了 1.00 秒
// 重置计时器,这样下一秒再打印
_elapsedTime = 0.0;
}
}
}extends Node
# 内部变量:累计经过的总时间
var _elapsed_time: float = 0.0
func _process(delta: float) -> void:
# delta 是距离上一帧经过的秒数
# 把每一帧的 delta 累加起来,就得到了总运行时间
_elapsed_time += delta
# 每 60 帧大约打印一次(大约每秒一次)
if _elapsed_time >= 1.0:
print("已经过了 %.2f 秒" % _elapsed_time)
# 运行结果: 已经过了 1.00 秒
# 重置计时器,这样下一秒再打印
_elapsed_time = 0.0实际场景
在实际游戏开发中,_process 最常用的场景之一就是让角色移动。这里演示一个 2D 角色通过键盘控制移动的例子,用 delta 来保证移动速度不受帧率影响:
using Godot;
public partial class Player : CharacterBody2D
{
// 导出属性:移动速度,可在编辑器中调整
[Export] public float ExMoveSpeed = 200f;
// 内部变量:精灵图引用
private Sprite2D _sprite;
public override void _Ready()
{
// 在 _ready 中获取子节点引用
_sprite = GetNode<Sprite2D>("Sprite2D");
}
public override void _Process(double delta)
{
// 1. 读取键盘输入方向
Vector2 direction = Vector2.Zero;
if (Input.IsKeyPressed(Key.Right))
direction.X += 1;
if (Input.IsKeyPressed(Key.Left))
direction.X -= 1;
if (Input.IsKeyPressed(Key.Down))
direction.Y += 1;
if (Input.IsKeyPressed(Key.Up))
direction.Y -= 1;
// 2. 根据方向翻转精灵图(朝左/朝右)
if (_sprite != null)
{
if (direction.X > 0)
_sprite.FlipH = false; // 朝右
else if (direction.X < 0)
_sprite.FlipH = true; // 朝左(水平翻转)
}
// 3. 移动角色
// 关键公式:位移 = 速度 * 时间(delta)
// 这样不管帧率是 30 还是 120,移动速度都一样
Position += direction.Normalized() * ExMoveSpeed * (float)delta;
// 假设按住右键 1 秒(delta 累计 = 1.0):
// 位移 = Vector2(1, 0) * 200 * 1.0 = 每秒移动 200 像素
// 运行结果: 角色以每秒 200 像素的速度向右移动
}
}extends CharacterBody2D
# 导出属性:移动速度,可在编辑器中调整
@export var ex_move_speed: float = 200.0
# 内部变量:精灵图引用
var _sprite: Sprite2D
func _ready() -> void:
# 在 _ready 中获取子节点引用
_sprite = get_node("Sprite2D")
func _process(delta: float) -> void:
# 1. 读取键盘输入方向
var direction := Vector2.ZERO
if Input.is_key_pressed(KEY_RIGHT):
direction.x += 1
if Input.is_key_pressed(KEY_LEFT):
direction.x -= 1
if Input.is_key_pressed(KEY_DOWN):
direction.y += 1
if Input.is_key_pressed(KEY_UP):
direction.y -= 1
# 2. 根据方向翻转精灵图(朝左/朝右)
if _sprite:
if direction.x > 0:
_sprite.flip_h = false # 朝右
elif direction.x < 0:
_sprite.flip_h = true # 朝左(水平翻转)
# 3. 移动角色
# 关键公式:位移 = 速度 * 时间(delta)
# 这样不管帧率是 30 还是 120,移动速度都一样
position += direction.normalized() * ex_move_speed * delta
# 假设按住右键 1 秒(delta 累计 = 1.0):
# 位移 = Vector2(1, 0) * 200 * 1.0 = 每秒移动 200 像素
# 运行结果: 角色以每秒 200 像素的速度向右移动进阶用法
下面展示更高级的技巧:动态开关 _process、用 _process 制作倒计时器、以及平滑插值动画。这些技巧在实际项目中非常实用:
using Godot;
public partial class AdvancedProcessDemo : Node
{
// 导出属性
[Export] public float ExCooldownDuration = 3.0f; // 技能冷却时间(秒)
// 内部变量
private float _cooldownTimer = 0f; // 冷却计时器
private bool _isCooldownActive = false; // 是否正在冷却
private float _displayAlpha = 1.0f; // UI 透明度(用于淡入淡出)
private Label _statusLabel;
public override void _Ready()
{
_statusLabel = GetNode<Label>("StatusLabel");
// 一开始不需要 _process,先关闭它以节省性能
// SetProcess(false) 会停止引擎调用 _Process
SetProcess(false);
GD.Print("准备就绪,按 Space 使用技能");
// 运行结果: 准备就绪,按 Space 使用技能
}
public override void _Process(double delta)
{
// --- 1. 技能冷却倒计时 ---
if (_isCooldownActive)
{
_cooldownTimer -= (float)delta;
if (_cooldownTimer <= 0f)
{
_cooldownTimer = 0f;
_isCooldownActive = false;
// 冷却结束,关闭 _process 节省性能
SetProcess(false);
GD.Print("技能冷却完毕,可以再次使用!");
// 运行结果: 技能冷却完毕,可以再次使用!
if (_statusLabel != null)
_statusLabel.Text = "技能就绪";
}
else
{
// 更新 UI 显示剩余时间
if (_statusLabel != null)
_statusLabel.Text = $"冷却中: {_cooldownTimer:F1}s";
}
}
// --- 2. 平滑淡入淡出效果 ---
// Lerp(线性插值)可以让数值平滑地过渡到目标值
// 用法:当前值 + (目标值 - 当前值) * 插值系数
// 插值系数乘以 delta,保证在不同帧率下速度一致
float targetAlpha = _isCooldownActive ? 0.3f : 1.0f;
_displayAlpha += (targetAlpha - _displayAlpha) * 5.0f * (float)delta;
if (_statusLabel != null)
{
// 设置标签透明度
_statusLabel.Modulate = new Color(1, 1, 1, _displayAlpha);
}
}
public override void _UnhandledInput(InputEvent @event)
{
// 按空格键使用技能
if (@event is InputEventKey key && key.PhysicalKeycode == Key.Space && key.Pressed)
{
if (!_isCooldownActive)
{
// 使用技能
_isCooldownActive = true;
_cooldownTimer = ExCooldownDuration;
// 启动 _process 来驱动冷却倒计时
SetProcess(true);
GD.Print("技能已释放!进入冷却...");
// 运行结果: 技能已释放!进入冷却...
}
else
{
GD.Print($"技能冷却中,还需 {_cooldownTimer:F1} 秒");
// 运行结果: 技能冷却中,还需 2.3 秒
}
}
}
}extends Node
# 导出属性
@export var ex_cooldown_duration: float = 3.0 # 技能冷却时间(秒)
# 内部变量
var _cooldown_timer: float = 0.0 # 冷却计时器
var _is_cooldown_active: bool = false # 是否正在冷却
var _display_alpha: float = 1.0 # UI 透明度(用于淡入淡出)
var _status_label: Label
func _ready() -> void:
_status_label = get_node("StatusLabel")
# 一开始不需要 _process,先关闭它以节省性能
# set_process(false) 会停止引擎调用 _process
set_process(false)
print("准备就绪,按 Space 使用技能")
# 运行结果: 准备就绪,按 Space 使用技能
func _process(delta: float) -> void:
# --- 1. 技能冷却倒计时 ---
if _is_cooldown_active:
_cooldown_timer -= delta
if _cooldown_timer <= 0.0:
_cooldown_timer = 0.0
_is_cooldown_active = false
# 冷却结束,关闭 _process 节省性能
set_process(false)
print("技能冷却完毕,可以再次使用!")
# 运行结果: 技能冷却完毕,可以再次使用!
if _status_label:
_status_label.text = "技能就绪"
else:
# 更新 UI 显示剩余时间
if _status_label:
_status_label.text = "冷却中: %.1fs" % _cooldown_timer
# --- 2. 平滑淡入淡出效果 ---
# lerp(线性插值)可以让数值平滑地过渡到目标值
# 用法:lerp(当前值, 目标值, 插值系数)
# 插值系数乘以 delta,保证在不同帧率下速度一致
var target_alpha = 0.3 if _is_cooldown_active else 1.0
_display_alpha = lerpf(_display_alpha, target_alpha, 5.0 * delta)
if _status_label:
# 设置标签透明度
_status_label.modulate = Color(1, 1, 1, _display_alpha)
func _unhandled_input(event: InputEvent) -> void:
# 按空格键使用技能
if event is InputEventKey and event.physical_keycode == KEY_SPACE and event.pressed:
if not _is_cooldown_active:
# 使用技能
_is_cooldown_active = true
_cooldown_timer = ex_cooldown_duration
# 启动 _process 来驱动冷却倒计时
set_process(true)
print("技能已释放!进入冷却...")
# 运行结果: 技能已释放!进入冷却...
else:
print("技能冷却中,还需 %.1f 秒" % _cooldown_timer)
# 运行结果: 技能冷却中,还需 2.3 秒注意事项
与帧率绑定:
_process的调用频率取决于实际渲染帧率。60 FPS 时每秒调用约 60 次,30 FPS 时每秒约 30 次。如果你的电脑性能好,帧率高,调用就频繁;反之则稀疏。因此,一切与位置、速度、时间相关的计算都必须乘以delta,否则在不同帧率下游戏体验会完全不同。不适合物理运算:
_process与渲染帧率挂钩,帧率不稳定时delta也会波动。如果你需要做碰撞检测、刚体运动、重力模拟等物理相关的逻辑,应该使用_physics_process而不是_process。_physics_process的调用频率是固定的(默认每秒 60 次),不受帧率影响,能保证物理运算的稳定性。动态开关:你可以通过
SetProcess(bool)(C#)或set_process(bool)(GDScript)随时打开或关闭_process的调用。关闭后引擎不会再执行这个方法,能节省 CPU 资源。一个常见的优化技巧是:只有在真正需要每帧更新时才打开_process,不需要时关闭它。ProcessMode 属性:节点的
ProcessMode属性控制着_process的行为。设为Disabled时完全禁用;设为Always时无论场景是否暂停都执行;设为Pausable(默认)时,游戏暂停期间_process也会跟着暂停。你可以利用这个特性来实现暂停菜单——暂停所有游戏逻辑,但菜单本身的_process仍然工作。不要在 _process 中做重操作:因为
_process每帧都调用,如果里面写了耗时很长的计算(比如遍历几千个节点、加载资源等),会直接拖慢帧率,导致游戏卡顿。对于复杂计算,考虑分帧处理(每帧只处理一部分),或者放到单独的线程中。delta 的精度差异:C# 中
delta是double类型(双精度浮点数),GDScript 中是float类型(单精度浮点数)。这意味着 C# 版本的时间精度更高,但对于绝大多数游戏来说,两者的差异可以忽略不计。C# 差异:C# 中方法名用 PascalCase(
_Process),参数类型是double;GDScript 中用 snake_case(_process),参数类型是float。C# 中需要override关键字来重写,GDScript 中只需直接定义同名函数即可。C# 中做乘法时可能需要将delta强制转换为float(如(float)delta),以匹配 Godot 的Vector2/Vector3等类型的运算。
