Godot 4恒定旋转补间实现问题与优化求助
解决Godot内置Tween无法实现恒定速度旋转的问题
背景
Godot内置的Tween无法满足以下需求:
- 必须指定过渡持续时间,无法实现恒定速度的属性过渡(线性过渡不符合恒定速度的要求)
- 旋转角度过渡时不总是选择最短路径,方向逻辑不符合预期
自定义补间器实现
为此实现了一个针对Node2D的恒定速度旋转补间器,但存在两个问题:
namespace Recoyx.CarKnockOnline.Util; using Godot; public class ConstantRotationTween { public float IncrementDegrees; private Node2D _tweenNode = null; private bool _running = false; private float _incrementScale = 1; private float _targetDegrees = 0; public ConstantRotationTween(float incrementDegrees) { this.IncrementDegrees = incrementDegrees; } public bool IsRunning() => _running; public void Tween(Node2D tweenNode, float targetDegrees) { this._tweenNode = tweenNode; float a = 0; // keep target rotation between 0-360 a = targetDegrees; a = a < 0 ? 360 - (-a % 360) : a; this._targetDegrees = Mathf.Round(a) % 360; // keep node rotation between 0-360 this._lockTweenNodeRotation(); a = this._targetDegrees; float b = this._tweenNode.RotationDegrees; float ab = a - b, ba = b - a; ab = ab < 0 ? ab + 360 : ab; ba = ba < 0 ? ba + 360 : ba; bool goClockwise = ab < ba; this._incrementScale = goClockwise ? 1 : -1; this._running = true; } public void Stop() { this._running = false; this._tweenNode = null; } public void Process(double delta) { if (!this._running) { return; } this._lockTweenNodeRotation(); if (this._tweenNode.RotationDegrees == this._targetDegrees) { this._running = false; return; } this._tweenNode.RotationDegrees += this.IncrementDegrees * this._incrementScale * ((float) delta); } /// <summary>Keep node rotation between 0-360.</summary> private void _lockTweenNodeRotation() { float a = this._tweenNode.RotationDegrees; a = a < 0 ? 360 - (-a % 360) : a; this._tweenNode.RotationDegrees = Mathf.Round(a) % 360; } }
使用方式
var tween = new ConstantRotationTween(150); // 旋转速度 = 150度/秒 tween.Tween(someNode, finalRotationDegrees); // 启动补间 tween.Stop(); // 取消补间 tween.IsRunning(); // 必须在_Process中调用 tween.Process(delta);
当前存在的问题
- 可能出现无限旋转或过度旋转,原因是增量值不等于1,直接比较整数角度导致无法精准停止
- 旋转存在突兀跳变,帧率不准确,推测是
_lockTweenNodeRotation中对角度取整破坏了浮点连续性
解决方案
改进后的自定义补间器
针对上述问题,调整实现逻辑,保留浮点精度、精准控制停止时机、使用弧度计算减少误差:
namespace Recoyx.CarKnockOnline.Util; using Godot; public class ConstantRotationTween { public float RotationSpeedDegrees; // 每秒旋转速度(度) private Node2D _targetNode = null; private bool _isRunning = false; private float _direction = 1f; private float _targetRadians; public ConstantRotationTween(float speedDegrees) { RotationSpeedDegrees = speedDegrees; } public bool IsRunning() => _isRunning; public void Start(Node2D targetNode, float targetDegrees) { _targetNode = targetNode; // 将目标角度转为弧度并归一化到0~2π _targetRadians = Mathf.Wrap(targetDegrees * Mathf.DegToRad, 0f, Mathf.Tau); // 归一化当前节点旋转弧度 var currentRadians = Mathf.Wrap(_targetNode.Rotation, 0f, Mathf.Tau); // 计算最短路径方向 var deltaRadians = _targetRadians - currentRadians; if (deltaRadians > Mathf.Pi) deltaRadians -= Mathf.Tau; else if (deltaRadians < -Mathf.Pi) deltaRadians += Mathf.Tau; _direction = deltaRadians >= 0 ? 1f : -1f; _isRunning = true; } public void Stop() { _isRunning = false; _targetNode = null; } public void Process(double delta) { if (!_isRunning || _targetNode == null) return; var currentRadians = Mathf.Wrap(_targetNode.Rotation, 0f, Mathf.Tau); var speedRadians = RotationSpeedDegrees * Mathf.DegToRad * (float)delta; // 计算剩余角度(取最短路径差值) var remaining = _targetRadians - currentRadians; if (remaining > Mathf.Pi) remaining -= Mathf.Tau; else if (remaining < -Mathf.Pi) remaining += Mathf.Tau; // 考虑浮点精度,当剩余角度小于单次移动量时直接设置到目标 if (Mathf.Abs(remaining) <= speedRadians * 1.01f) { _targetNode.Rotation = _targetRadians; _isRunning = false; return; } // 应用旋转增量 _targetNode.Rotation += _direction * speedRadians; } }
简化的原生实现方式
如果不需要单独封装类,直接在Node2D脚本中实现更简洁:
using Godot; public class RotatingNode : Node2D { [Export] public float RotationSpeed = 150f; // 每秒旋转速度(度) private float _targetRotationDegrees; private bool _isRotating = false; // 调用此方法启动旋转到目标角度 public void RotateTo(float targetDegrees) { _targetRotationDegrees = targetDegrees; _isRotating = true; } public override void _Process(double delta) { if (!_isRotating) return; var targetRadians = Mathf.DegToRad(_targetRotationDegrees); var currentRadians = Rotation; // 计算最短路径的角度差(范围-π~π) var deltaRadians = Mathf.Wrap(targetRadians - currentRadians, -Mathf.Pi, Mathf.Pi); // 浮点精度容错,到达目标后停止 if (Mathf.Abs(deltaRadians) < 0.001f) { Rotation = targetRadians; _isRotating = false; return; } var speedRadians = RotationSpeed * Mathf.DegToRad * (float)delta; // 限制移动量不超过剩余角度,避免过度旋转 var moveAmount = Mathf.Clamp(Mathf.Abs(deltaRadians), 0f, speedRadians) * Mathf.Sign(deltaRadians); Rotation += moveAmount; } }
关键优化点
避免过度/无限旋转:
- 计算剩余角度时取最短路径差值,当剩余角度小于单次移动量时,直接设置到目标角度
- 使用浮点精度容错判断,而非直接比较整数角度
解决跳变问题:
- 移除每帧对角度的取整操作,保留旋转的浮点连续性
- 改用Godot内部的弧度存储进行计算,减少角度转换的精度损失
- 使用
Mathf.Wrap归一化角度,避免角度值溢出但不破坏连续性
内容的提问来源于stack exchange,提问作者Hydroper
相关产品推荐
相关产品推荐

