pingpong
2026/4/14大约 5 分钟
最后同步日期:2026-04-15 | Godot 官方原文 — pingpong
pingpong
定义
pingpong() 用来让一个不断增长的值在 0 和指定长度之间来回"弹跳"——就像乒乓球在球桌上来回弹一样,值到达上限后就会往回走,到达 0 又掉头,周而复始。
想象一个在 0 和 100 之间来回移动的自动门:门打开到 100% 后开始关闭,关到 0% 后又开始打开,永远不停。pingpong() 就是在做这件事:你给它一个不断增大的输入值,它帮你把输出变成在 [0, length] 之间来回弹跳的值。
为什么叫 pingpong? 因为输出值的运动轨迹就像乒乓球:去(ping)——到头了——回(pong)——又到头了——再去(ping)——如此循环。
在游戏开发中,pingpong() 常用于:来回巡逻的敌人、呼吸灯效果、物体上下浮动、来回摇摆的障碍物、UI 进度条来回填充等。任何需要"在两个值之间来回运动"的效果,pingpong() 都能轻松搞定。
函数签名
C#
public static float PingPong(float value, float length)GDScript
func pingpong(value: float, length: float) -> float参数说明
| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
value | float | 是 | 输入值,通常是不断增长的(如时间或累加计数)。值在 [0, length] 和 [length, 0] 之间交替输出 |
length | float | 是 | 弹跳范围的上限。输出值在 0 到 length 之间来回运动。必须大于 0 |
返回值
float —— 在 0 到 length 之间来回弹跳的值。
具体行为:
- 当
value从 0 增长到length时,输出也从 0 增长到length(正向走) - 当
value从length增长到2*length时,输出从length减回 0(反向走) - 当
value从2*length增长到3*length时,输出又从 0 增长到length(正向走) - 如此循环往复
例子:pingpong(0, 10) = 0,pingpong(5, 10) = 5,pingpong(10, 10) = 10,pingpong(15, 10) = 5,pingpong(20, 10) = 0
代码示例
基础用法:观察 pingpong 的弹跳模式
C#
using Godot;
public partial class PingPongExample : Node
{
public override void _Ready()
{
float len = 10f;
// 正向阶段:value 从 0 到 10
GD.Print($"pingpong(0, 10) = {Mathf.PingPong(0f, len)}");
// 运行结果: pingpong(0, 10) = 0
GD.Print($"pingpong(5, 10) = {Mathf.PingPong(5f, len)}");
// 运行结果: pingpong(5, 10) = 5
GD.Print($"pingpong(10, 10) = {Mathf.PingPong(10f, len)}");
// 运行结果: pingpong(10, 10) = 10
// 反向阶段:value 从 10 到 20
GD.Print($"pingpong(15, 10) = {Mathf.PingPong(15f, len)}");
// 运行结果: pingpong(15, 10) = 5
GD.Print($"pingpong(20, 10) = {Mathf.PingPong(20f, len)}");
// 运行结果: pingpong(20, 10) = 0
// 又正向:value 从 20 到 30
GD.Print($"pingpong(25, 10) = {Mathf.PingPong(25f, len)}");
// 运行结果: pingpong(25, 10) = 5
}
}GDScript
func _ready():
var len = 10.0
# 正向阶段:value 从 0 到 10
print("pingpong(0, 10) = %f" % pingpong(0.0, len))
# 运行结果: pingpong(0, 10) = 0.000000
print("pingpong(5, 10) = %f" % pingpong(5.0, len))
# 运行结果: pingpong(5, 10) = 5.000000
print("pingpong(10, 10) = %f" % pingpong(10.0, len))
# 运行结果: pingpong(10, 10) = 10.000000
# 反向阶段:value 从 10 到 20
print("pingpong(15, 10) = %f" % pingpong(15.0, len))
# 运行结果: pingpong(15, 10) = 5.000000
print("pingpong(20, 10) = %f" % pingpong(20.0, len))
# 运行结果: pingpong(20, 10) = 0.000000
# 又正向:value 从 20 到 30
print("pingpong(25, 10) = %f" % pingpong(25.0, len))
# 运行结果: pingpong(25, 10) = 5.000000实际场景:悬浮灯上下浮动
C#
using Godot;
public partial class FloatingLamp : Node3D
{
// 导出属性:浮动幅度
[Export] public float ExAmplitude = 2.0f;
// 导出属性:浮动速度
[Export] public float ExSpeed = 1.5f;
// 内部变量:初始 Y 位置
private float _baseY;
public override void _Ready()
{
_baseY = Position.Y;
}
public override void _Process(double delta)
{
// 用时间作为输入,pingpong 让位置在 0~Amplitude 之间来回
float offset = Mathf.PingPong((float)Time.GetTicksMsec() / 1000f * ExSpeed, ExAmplitude);
// 应用到 Y 轴位置
Position = new Vector3(Position.X, _baseY + offset, Position.Z);
GD.Print($"浮动偏移: {offset:F2}");
// 运行结果: 浮动偏移: 1.23
}
}GDScript
extends Node3D
# 导出属性:浮动幅度
@export var ex_amplitude: float = 2.0
# 导出属性:浮动速度
@export var ex_speed: float = 1.5
# 内部变量:初始 Y 位置
var _base_y: float
func _ready():
_base_y = position.y
func _process(delta):
# 用时间作为输入,pingpong 让位置在 0~Amplitude 之间来回
var offset = pingpong(Time.get_ticks_msec() / 1000.0 * ex_speed, ex_amplitude)
# 应用到 Y 轴位置
position.y = _base_y + offset
print("浮动偏移: %.2f" % offset)
# 运行结果: 浮动偏移: 1.23进阶用法:巡逻敌人 + 配合 ease 做平滑摆动
C#
using Godot;
public partial class PatrolEnemy : CharacterBody2D
{
// 导出属性:巡逻范围
[Export] public float ExPatrolRange = 300f;
// 导出属性:巡逻速度
[Export] public float ExPatrolSpeed = 2.0f;
// 内部变量:起始 X 位置
private float _startX;
public override void _Ready()
{
_startX = Position.X;
}
public override void _Process(double delta)
{
// 时间不断增长 → pingpong 产生来回值 → ease 做平滑加速减速
float time = (float)Time.GetTicksMsec() / 1000f * ExPatrolSpeed;
float rawT = Mathf.PingPong(time, 1.0f);
// 归一化到 0~1 后用 ease 做平滑过渡(先快后慢,让停顿更自然)
float smoothedT = Mathf.Ease(rawT, 2.0f);
// 映射到巡逻范围
float offsetX = Mathf.Lerp(0f, ExPatrolRange, smoothedT);
Position = new Vector2(_startX + offsetX, Position.Y);
GD.Print($"巡逻进度: {rawT:F2}, 平滑后: {smoothedT:F2}, 位置X: {Position.X:F0}");
// 运行结果: 巡逻进度: 0.50, 平滑后: 0.75, 位置X: 400
}
}GDScript
extends CharacterBody2D
# 导出属性:巡逻范围
@export var ex_patrol_range: float = 300.0
# 导出属性:巡逻速度
@export var ex_patrol_speed: float = 2.0
# 内部变量:起始 X 位置
var _start_x: float
func _ready():
_start_x = position.x
func _process(delta):
# 时间不断增长 → pingpong 产生来回值 → ease 做平滑加速减速
var time = Time.get_ticks_msec() / 1000.0 * ex_patrol_speed
var raw_t = pingpong(time, 1.0)
# 归一化到 0~1 后用 ease 做平滑过渡(先快后慢,让停顿更自然)
var smoothed_t = ease(raw_t, 2.0)
# 映射到巡逻范围
var offset_x = lerp(0.0, ex_patrol_range, smoothed_t)
position.x = _start_x + offset_x
print("巡逻进度: %.2f, 平滑后: %.2f, 位置X: %.0f" % [raw_t, smoothed_t, position.x])
# 运行结果: 巡逻进度: 0.50, 平滑后: 0.75, 位置X: 400注意事项
value通常用时间驱动:最常见的用法是把Time.GetTicksMsec() / 1000.0 * speed作为value传入,这样 pingpong 就会随时间自动来回运动。length必须大于 0:如果length为 0 或负数,行为是未定义的。- 内部实现基于
wrap():pingpong(value, length)等价于:先算w = wrap(value, 0, 2*length),然后如果w > length就返回2*length - w,否则返回w。 - 输出范围是
[0, length]:两端都是闭区间。当value恰好是length的整数倍时,输出会触碰到边界。 - 正向和反向速度相同:
pingpong()的"去"和"回"速度是对称的,无法单独控制。如果需要不同速度的正反向运动,请用Tween或手动控制。
