_physics_process
最后同步日期:2026-04-16 | Godot 官方原文 — _physics_process
Node._physics_process
定义
想象你在一个精密的工厂流水线上工作。不管外面刮风下雨、交通堵塞,流水线的传送带每隔一段固定的时间就会匀速前进一次——这就是 _physics_process。
在 Godot 中,_physics_process 是一个固定时间间隔被调用的虚方法(Virtual Method)。默认情况下,它每秒被调用 60 次,也就是说每次调用之间的间隔(delta)大约是 1/60 秒 ≈ 0.0167 秒。不管你的游戏是跑在高端电脑上(200 FPS)还是老旧手机上(30 FPS),_physics_process 的调用频率始终不变。
这个"固定节奏"的特性让它非常适合处理所有与物理有关的逻辑:
- 角色移动(走路、跳跃、冲刺)
- 碰撞检测和响应
- 速度、加速度的更新
- 刚体(RigidBody)的相关计算
- 射线检测(RayCast)
一句话总结
_physics_process 就像节拍器——不管外界帧率如何波动,它始终以固定节拍滴答运转,是处理所有物理和运动逻辑的最佳场所。
与 _process 的区别
_process 每一帧调用一次,帧率不固定(60 FPS 时 delta 约 0.0167 秒,30 FPS 时 delta 约 0.0333 秒)。_physics_process 以固定频率调用,delta 始终一致。物理和运动相关的代码请放在 _physics_process 里,UI 动画、粒子特效等视觉更新的代码放在 _process 里。
函数签名
public override void _PhysicsProcess(double delta)func _physics_process(delta: float) -> void参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
delta | C#: double / GDScript: float | 是 | 距离上一次 _physics_process 被调用所经过的时间,单位为秒。默认物理帧率为 60 Hz 时,delta 约等于 0.016667(即 1/60 秒)。你可以在项目设置中通过 Physics Ticks Per Second 修改这个频率。 |
返回值
无返回值(void)。
代码示例
基础用法
最简单的用法——在 _physics_process 中打印每帧的时间间隔 delta,验证它确实以固定频率被调用:
using Godot;
public partial class MyNode : Node
{
// 内部变量:记录物理帧计数
private int _physicsFrameCount = 0;
public override void _PhysicsProcess(double delta)
{
_physicsFrameCount++;
// delta 始终约等于 0.016667(即 1/60 秒)
GD.Print($"物理帧 #{_physicsFrameCount}, delta = {delta:F6} 秒");
// 运行结果: 物理帧 #1, delta = 0.016667 秒
// 运行结果: 物理帧 #2, delta = 0.016667 秒
// 运行结果: 物理帧 #3, delta = 0.016667 秒
// (每秒打印 60 行,delta 值始终稳定)
}
}extends Node
# 内部变量:记录物理帧计数
var _physics_frame_count: int = 0
func _physics_process(delta: float) -> void:
_physics_frame_count += 1
# delta 始终约等于 0.016667(即 1/60 秒)
print("物理帧 #%d, delta = %.6f 秒" % [_physics_frame_count, delta])
# 运行结果: 物理帧 #1, delta = 0.016667 秒
# 运行结果: 物理帧 #2, delta = 0.016667 秒
# 运行结果: 物理帧 #3, delta = 0.016667 秒
# (每秒打印 60 行,delta 值始终稳定)实际场景
在实际游戏开发中,_physics_process 最常见的用途就是处理角色的移动。下面是一个 2D 平台游戏角色的移动控制——读取玩家输入,计算速度,然后使用 MoveAndSlide() 让角色移动并自动处理碰撞:
using Godot;
public partial class Player : CharacterBody2D
{
// 导出属性:在编辑器 Inspector 面板中可调整
[Export] public float ExMoveSpeed = 300f; // 移动速度(像素/秒)
[Export] public float ExJumpVelocity = -400f; // 跳跃初速度(负数=向上)
[Export] public float ExGravity = 980f; // 重力加速度(像素/秒²)
// 内部变量
private float _gravity;
public override void _Ready()
{
_gravity = ExGravity;
}
public override void _PhysicsProcess(double delta)
{
var velocity = Velocity;
// 1. 重力:让角色不断往下掉
if (!IsOnFloor())
{
velocity.Y += _gravity * (float)delta;
}
// 2. 跳跃:只在站在地面上时才能跳
if (Input.IsActionJustPressed("jump") && IsOnFloor())
{
velocity.Y = ExJumpVelocity;
}
// 3. 水平移动:读取左右按键输入
var direction = Input.GetVector("move_left", "move_right", "move_up", "move_down");
velocity.X = direction.X * ExMoveSpeed;
// 4. 应用速度并移动(MoveAndSlide 会自动处理碰撞)
Velocity = velocity;
MoveAndSlide();
}
}extends CharacterBody2D
# 导出属性:在编辑器 Inspector 面板中可调整
@export var ex_move_speed: float = 300.0 ## 移动速度(像素/秒)
@export var ex_jump_velocity: float = -400.0 ## 跳跃初速度(负数=向上)
@export var ex_gravity: float = 980.0 ## 重力加速度(像素/秒²)
# 内部变量
var _gravity: float
func _ready() -> void:
_gravity = ex_gravity
func _physics_process(delta: float) -> void:
var velocity := velocity
# 1. 重力:让角色不断往下掉
if not is_on_floor():
velocity.y += _gravity * delta
# 2. 跳跃:只在站在地面上时才能跳
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = ex_jump_velocity
# 3. 水平移动:读取左右按键输入
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity.x = direction.x * ex_move_speed
# 4. 应用速度并移动(move_and_slide 会自动处理碰撞)
self.velocity = velocity
move_and_slide()上面的代码中,velocity.Y += _gravity * (float)delta 这行就是 delta 的核心用法:速度 += 加速度 x 时间间隔。因为 delta 是固定的,所以每帧增加的速度量也是固定的,保证了不同设备上角色下落的速度完全一致。
进阶用法
下面展示更复杂的场景:一个可以动态开关物理处理的巡逻敌人,在多个巡逻点之间来回移动,并在被击败时停止所有物理逻辑:
using Godot;
public partial class PatrolEnemy : CharacterBody2D
{
// 导出属性
[Export] public float ExMoveSpeed = 150f;
[Export] public float ExPatrolRange = 200f; // 巡逻范围(像素)
// 内部变量
private Vector2 _startPosition;
private int _moveDirection = 1; // 1 = 向右, -1 = 向左
private bool _isAlive = true;
private float _elapsedTime = 0f;
public enum EnumState { Patrolling, Chasing, Dead }
private EnumState _currentState = EnumState.Patrolling;
public override void _Ready()
{
_startPosition = GlobalPosition;
GD.Print($"巡逻敌人就绪,起始位置: {_startPosition}");
// 运行结果: 巡逻敌人就绪,起始位置: (100, 300)
}
public override void _PhysicsProcess(double delta)
{
if (!_isAlive) return;
// 使用 delta 累计时间(用于动画或计时)
_elapsedTime += (float)delta;
switch (_currentState)
{
case EnumState.Patrolling:
Patrol(delta);
break;
case EnumState.Chasing:
ChasePlayer(delta);
break;
case EnumState.Dead:
break;
}
// 应用速度并移动
MoveAndSlide();
// 根据移动方向翻转朝向
if (Velocity.X != 0)
{
// 1 表示面朝右,-1 表示面朝左
var sprite = GetNodeOrNull<Sprite2D>("Sprite2D");
if (sprite != null)
{
sprite.FlipH = Velocity.X < 0;
}
}
}
private void Patrol(double delta)
{
var velocity = Velocity;
// 水平匀速移动
velocity.X = _moveDirection * ExMoveSpeed;
velocity.Y += 980f * (float)delta; // 重力
Velocity = velocity;
// 超出巡逻范围时掉头
float distanceFromStart = GlobalPosition.X - _startPosition.X;
if (Mathf.Abs(distanceFromStart) >= ExPatrolRange)
{
_moveDirection *= -1; // 反转方向
GD.Print("到达巡逻边界,掉头!");
// 运行结果: 到达巡逻边界,掉头!
}
}
private void ChasePlayer(double delta)
{
// 简单追踪:朝玩家方向移动
var player = GetTree().GetFirstNodeInGroup("player");
if (player == null) return;
var direction = (player as Node2D).GlobalPosition - GlobalPosition;
var velocity = Velocity;
velocity.X = direction.Normalized().X * ExMoveSpeed * 1.5f;
velocity.Y += 980f * (float)delta;
Velocity = velocity;
}
/// <summary>
/// 敌人被击败时调用——停止物理处理
/// </summary>
public void Die()
{
_isAlive = false;
_currentState = EnumState.Dead;
// 方式一:直接跳过逻辑(上面的 if (!_isAlive) return 已处理)
// 方式二:彻底关闭物理帧回调(推荐在不需要时使用)
SetPhysicsProcess(false);
GD.Print("敌人已被击败,物理处理已停止");
// 运行结果: 敌人已被击败,物理处理已停止
}
/// <summary>
/// 敌人复活——重新开启物理处理
/// </summary>
public void Revive()
{
_isAlive = true;
_currentState = EnumState.Patrolling;
SetPhysicsProcess(true);
GlobalPosition = _startPosition;
GD.Print("敌人复活,物理处理已恢复");
// 运行结果: 敌人复活,物理处理已恢复
}
}extends CharacterBody2D
# 导出属性
@export var ex_move_speed: float = 150.0
@export var ex_patrol_range: float = 200.0 ## 巡逻范围(像素)
# 内部变量
var _start_position: Vector2
var _move_direction: int = 1 # 1 = 向右, -1 = 向左
var _is_alive: bool = true
var _elapsed_time: float = 0.0
enum EnumState { PATROLLING, CHASING, DEAD }
var _current_state: EnumState = EnumState.PATROLLING
func _ready() -> void:
_start_position = global_position
print("巡逻敌人就绪,起始位置: %s" % _start_position)
# 运行结果: 巡逻敌人就绪,起始位置: (100, 300)
func _physics_process(delta: float) -> void:
if not _is_alive:
return
# 使用 delta 累计时间(用于动画或计时)
_elapsed_time += delta
match _current_state:
EnumState.PATROLLING:
_patrol(delta)
EnumState.CHASING:
_chase_player(delta)
EnumState.DEAD:
pass
# 应用速度并移动
move_and_slide()
# 根据移动方向翻转朝向
if velocity.x != 0:
var sprite = get_node_or_null("Sprite2D")
if sprite:
sprite.flip_h = velocity.x < 0
func _patrol(delta: float) -> void:
var vel := velocity
# 水平匀速移动
vel.x = _move_direction * ex_move_speed
vel.y += 980.0 * delta # 重力
self.velocity = vel
# 超出巡逻范围时掉头
var distance_from_start = global_position.x - _start_position.x
if absf(distance_from_start) >= ex_patrol_range:
_move_direction *= -1 # 反转方向
print("到达巡逻边界,掉头!")
# 运行结果: 到达巡逻边界,掉头!
func _chase_player(delta: float) -> void:
# 简单追踪:朝玩家方向移动
var player = get_tree().get_first_node_in_group("player")
if not player:
return
var direction = player.global_position - global_position
var vel := velocity
vel.x = direction.normalized().x * ex_move_speed * 1.5
vel.y += 980.0 * delta
self.velocity = vel
## 敌人被击败时调用——停止物理处理
func die() -> void:
_is_alive = false
_current_state = EnumState.DEAD
# 方式一:直接跳过逻辑(上面的 if not _is_alive: return 已处理)
# 方式二:彻底关闭物理帧回调(推荐在不需要时使用)
set_physics_process(false)
print("敌人已被击败,物理处理已停止")
# 运行结果: 敌人已被击败,物理处理已停止
## 敌人复活——重新开启物理处理
func revive() -> void:
_is_alive = true
_current_state = EnumState.PATROLLING
set_physics_process(true)
global_position = _start_position
print("敌人复活,物理处理已恢复")
# 运行结果: 敌人复活,物理处理已恢复注意事项
- 固定帧率,不受渲染帧率影响:
_physics_process的调用频率由项目设置中的 Physics Ticks Per Second 决定(默认 60 Hz),与游戏的渲染帧率(FPS)完全无关。即使渲染帧率暴跌到 10 FPS,_physics_process仍然每秒稳定调用 60 次。这是它与_process最本质的区别。 - delta 乘法模式:在
_physics_process中更新速度、位置等物理量时,务必使用值 += 速率 * delta的写法。虽然delta在这里理论上是常量,但养成这个习惯能确保当你修改物理帧率设置后,游戏行为不会出问题。 - 这是 MoveAndSlide / MoveAndCollide 的正确位置:对于
CharacterBody2D和CharacterBody3D,MoveAndSlide()和MoveAndCollide()必须在_physics_process中调用,而不是_process中。因为这些函数依赖物理引擎的状态,只有在物理帧中调用才能保证碰撞检测的准确性。 - 运行在物理模拟步骤之前:
_physics_process在每次物理模拟(碰撞检测、刚体运动等)之前被调用。这意味着你在_physics_process中修改的速度、位置等数据会在同一物理帧内立即被物理引擎使用,确保逻辑和物理状态同步。 - 用 SetPhysicsProcess 动态控制:你可以通过
SetPhysicsProcess(true)/SetPhysicsProcess(false)在运行时随时开关某个节点的_physics_process回调。当一个对象不需要物理更新时(比如敌人已死亡、道具已拾取),关闭它可以节省 CPU 开销。 - 不要在这里做非物理逻辑:UI 更新、粒子效果、相机跟随等与物理无关的逻辑应该放在
_process中。把所有东西都塞进_physics_process会导致代码职责混乱,也可能在物理帧率和渲染帧率不一致时产生视觉抖动。 - C# 差异:C# 中方法名用 PascalCase(
_PhysicsProcess),GDScript 中用 snake_case(_physics_process)。C# 中需要override关键字来重写,GDScript 中只需直接定义同名函数即可。C# 中delta类型是double,GDScript 中是float。
