You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
    }
}

关键优化点

  1. 避免过度/无限旋转:

    • 计算剩余角度时取最短路径差值,当剩余角度小于单次移动量时,直接设置到目标角度
    • 使用浮点精度容错判断,而非直接比较整数角度
  2. 解决跳变问题:

    • 移除每帧对角度的取整操作,保留旋转的浮点连续性
    • 改用Godot内部的弧度存储进行计算,减少角度转换的精度损失
    • 使用Mathf.Wrap归一化角度,避免角度值溢出但不破坏连续性

内容的提问来源于stack exchange,提问作者Hydroper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 07:57:30