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

游戏昼夜循环中Quaternion欧拉角无故出现负值问题求助

昼夜循环太阳旋转异常问题

问题详情

我正在为游戏制作昼夜循环系统,通过每秒为DateTime对象增加秒数推进时间,同时计算minutes变量(公式:DateTime.Hours * 60 + DateTime.Minutes)。我希望基于这个变量旋转太阳,使用公式Quaternion.Euler(minutes / 4, 0, 0)。

一切运行正常,直到DateTime到达正午时分,太阳开始出现异常:旋转值变为负值,之后又恢复正常状态直至午夜。此外,Inspector面板显示的太阳旋转值与Debug.Log输出的结果不一致。

问题截图
问题演示视频:画质欠佳,展示了正午时分太阳旋转异常的情况

怀疑是万向节锁(Gimbal lock)导致的,但对四元数了解有限,以下是我的代码:

public enum DayTime
{
    Morning,
    Noon,
    AfterNoon,
    Evening,
    Night
}

public class MoonTime : MonoBehaviour
{
    public DateTime date = new DateTime(2023, 1, 1, 0, 0, 0);

    [HideInInspector]
    public float minutes = 0;
    public float dayLength = 4;

    public float smoothness = 4.5f;

    [HideInInspector]
    public DayTime dayTime;

    public delegate void TimeChange(DateTime date);
    public event TimeChange onTimeChange;

    public delegate void DayTimeChange(DayTime dayTime);
    public event DayTimeChange onDayTimeChange;


    public Material daySkyBox;
    public Material nightSkyBox;

    public Light Sun;
    public Light Moon;

    private void Awake()
    {
        StartCoroutine(TimeClock());

        onTimeChange += (d) =>
        {
            var sunCurrentRot = Sun.transform.rotation.eulerAngles;

            Debug.Log("What rot is should be " + Quaternion.Euler(minutes / 4, sunCurrentRot.y, sunCurrentRot.z).eulerAngles);
            Sun.transform.rotation = Quaternion.Euler(minutes / 4, sunCurrentRot.y, sunCurrentRot.z);
            Debug.Log("What is it " + Sun.transform.rotation.eulerAngles);

            //var moonCurrentRot = Moon.transform.rotation.eulerAngles;

            //Moon.transform.rotation = Quaternion.Euler((minutes * 0.25f) + 180, moonCurrentRot.y, moonCurrentRot.z);
        };
    }

    private void Update()
    {
    }

    public IEnumerator TimeClock()
    {
        while (true)
        {
            yield return new WaitForSecondsRealtime(1 / smoothness);

            float finalValue = 1440 / (dayLength * 60) / smoothness;

            date = date.AddSeconds(finalValue);

            minutes = (date.Hour * 60) + date.Minute;


            if (date.Hour >= 0 && date.Hour < 12)
            {
                if (dayTime != DayTime.Morning)
                {
                    dayTime = DayTime.Morning;
                    Debug.Log("Morning");
                    onDayTimeChange?.Invoke(DayTime.Morning);
                }
            }
            else if (date.Hour == 12)
            {
                if (dayTime != DayTime.Noon)
                {
                    dayTime = DayTime.Noon;
                    Debug.Log("Noon");
                    onDayTimeChange?.Invoke(DayTime.Noon);
                }
            }
            else if(date.Hour > 12 && date.Hour <= 15)
            {
                if (dayTime != DayTime.AfterNoon)
                {
                    dayTime = DayTime.AfterNoon;
                    Debug.Log("After Noon");
                    onDayTimeChange?.Invoke(DayTime.AfterNoon);
                }
            }
            else if (date.Hour > 15 && date.Hour <= 17)
            {
                if (dayTime != DayTime.Evening)
                {
                    dayTime = DayTime.Evening;
                    Debug.Log("Evening");
                    onDayTimeChange?.Invoke(DayTime.Evening);
                }
            }
            else
            {
                if (dayTime != DayTime.Night)
                {
                    dayTime = DayTime.Night;
                    Debug.Log("Night");
                    onDayTimeChange?.Invoke(DayTime.Night);
                }
            }

            onTimeChange?.Invoke(date);

            Debug.Log(date);
        }
    }
}

解决方案

核心问题

这不是万向节锁导致的,问题出在Unity对欧拉角的归一化处理以及你的旋转更新逻辑:

  1. Unity的transform.rotation.eulerAngles返回的角度永远在[0, 360)范围内,不会出现负数;但当你设置的欧拉角X值超过180度时,Unity会自动转换为等价的负角度(比如190度转为-170度),这就是Inspector显示负值的原因。
  2. 你每次更新旋转时都读取当前太阳的欧拉角再赋值,会因为归一化的偏差导致计算混乱。

修复代码

1. 修改太阳旋转逻辑

直接基于时间计算目标旋转,不依赖当前旋转值,同时调整角度公式避免跨180度的异常转换:

onTimeChange += (d) =>
{
    // 调整角度逻辑:0点对应-90度(地平线以下),12点对应90度(头顶),24点回到-90度
    float sunXRot = (minutes / 4) - 90f;
    
    // 直接设置旋转,保持Y/Z轴为初始值(若不需要Y轴旋转)
    Sun.transform.rotation = Quaternion.Euler(sunXRot, 0, 0);

    // 查看原始计算值,而非Unity归一化后的结果
    Debug.Log("太阳计算旋转值:" + sunXRot);
    Debug.Log("Unity显示旋转值:" + Sun.transform.rotation.eulerAngles);
};

2. 优化时间推进逻辑(可选)

避免DateTime累加秒数的精度问题,直接维护minutes变量:

public IEnumerator TimeClock()
{
    while (true)
    {
        yield return new WaitForSecondsRealtime(1 / smoothness);

        // 计算每帧增加的分钟数:一天1440分钟,按dayLength小时的周期推进
        float minutesPerFrame = 1440f / (dayLength * 60f * smoothness);
        minutes += minutesPerFrame;
        
        // 超过一天后重置
        if (minutes >= 1440f)
        {
            minutes -= 1440f;
            date = date.AddDays(1);
        }
        else
        {
            date = date.AddMinutes(minutesPerFrame);
        }

        // 以下昼夜判断逻辑保持不变
        if (date.Hour >= 0 && date.Hour < 12)
        {
            if (dayTime != DayTime.Morning)
            {
                dayTime = DayTime.Morning;
                Debug.Log("Morning");
                onDayTimeChange?.Invoke(DayTime.Morning);
            }
        }
        else if (date.Hour == 12)
        {
            if (dayTime != DayTime.Noon)
            {
                dayTime = DayTime.Noon;
                Debug.Log("Noon");
                onDayTimeChange?.Invoke(DayTime.Noon);
            }
        }
        else if(date.Hour > 12 && date.Hour <= 15)
        {
            if (dayTime != DayTime.AfterNoon)
            {
                dayTime = DayTime.AfterNoon;
                Debug.Log("After Noon");
                onDayTimeChange?.Invoke(DayTime.AfterNoon);
            }
        }
        else if (date.Hour > 15 && date.Hour <= 17)
        {
            if (dayTime != DayTime.Evening)
            {
                dayTime = DayTime.Evening;
                Debug.Log("Evening");
                onDayTimeChange?.Invoke(DayTime.Evening);
            }
        }
        else
        {
            if (dayTime != DayTime.Night)
            {
                dayTime = DayTime.Night;
                Debug.Log("Night");
                onDayTimeChange?.Invoke(DayTime.Night);
            }
        }

        onTimeChange?.Invoke(date);
        Debug.Log(date);
    }
}

额外说明

  • Inspector显示的欧拉角是Unity归一化后的等价角度,和你设置的原始值可能不同,但旋转效果完全一致。如果要验证计算逻辑,直接输出你计算的sunXRot即可。
  • 万向节锁是三个欧拉轴重合导致自由度丢失的情况,你仅修改X轴旋转,不会触发万向节锁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 13:15:32