Voltar para o Blog
Quest Log

Ciclo de Dia e Noite na Unity: Sol, Céu e Luz Ambiente

Paisagem low poly dividida entre dia e noite, com o sol de um lado e a lua do outro num arco sobre o terreno

Unity day night cycle em C#: relógio do jogo, Directional Light girando, cor e intensidade com Gradient e AnimationCurve e luz ambiente sem pesar o frame.

Ferramenta:UnityC#

Ciclo de Dia e Noite na Unity: Sol, Céu e Luz Ambiente

Um Unity day night cycle se resume a três coisas que andam juntas: um relógio que transforma tempo real em hora do jogo, uma Directional Light que gira conforme essa hora, e cores (do sol, do céu e da luz ambiente) que mudam pela hora. Com um script de cerca de 100 linhas, um Gradient para as cores e uma AnimationCurve para a intensidade, você tem o ciclo inteiro, e o skybox procedural acompanha sozinho.

Este guia monta o script DayNightCycle para o Unity 6.3 LTS no URP, explica por que cada número está ali e mostra como ajustar a luz ambiente sem recalcular iluminação a cada frame, que é onde a maioria dos ciclos de dia e noite derruba o desempenho.

Como funciona um ciclo de dia e noite na Unity

Na Unity, a iluminação de uma cena 3D ao ar livre vem de três fontes:

  • Luz direta: a Directional Light (o sol). Ela tem direção, cor, intensidade e gera sombras. A posição dela não importa, só a rotação.
  • Luz ambiente: a luz "de todo lado", que clareia as sombras. Configurada em Window > Rendering > Lighting, aba Environment, seção Environment Lighting, campo Source: Skybox, Gradient ou Color.
  • Céu: o material em Skybox Material, na mesma aba. Com o shader Skybox/Procedural, o desenho do sol no céu segue a luz definida em Sun Source.

Um ciclo de dia e noite mexe nas três ao mesmo tempo. O erro comum é mexer só no sol: o dia escurece, mas as sombras continuam azuladas de meio-dia e o céu continua claro.

Relógio do jogo: transformar tempo real em hora do dia

Tudo parte de um número: a hora do jogo, de 0 a 24. A conversão de tempo real para tempo de jogo é uma regra de três. Se um dia do jogo dura 12 minutos reais (720 segundos), cada segundo real avança 24 / 720 = 0,0333 hora de jogo. Multiplicando pelo Time.deltaTime, o relógio anda igual em qualquer frame rate, assunto que o post de delta time e movimento por frame explica em detalhe.

Trabalhar com a hora (e não com um ângulo) é uma decisão de projeto: o resto do jogo (lojas que fecham, inimigos noturnos, plantação que cresce) também precisa saber "que horas são". Um número legível vira a fonte de verdade.

Girando o sol (Directional Light) por script

A rotação do sol no eixo X define a altura dele no céu: 0 grau é o horizonte, 90 graus é meio-dia a pino, 180 graus é o horizonte do outro lado, e de 180 a 360 o sol está "embaixo do chão", iluminando de baixo para cima (o que você não quer ver). A conversão: hora 6 vira 0 grau (nascer), hora 12 vira 90, hora 18 vira 180.

ângulo = (hora / 24) x 360 - 90

O eixo Y define de que lado o sol nasce. Deixe fixo, por exemplo 170 graus, para o sol não passar exatamente por cima da câmera.

Arquivo DayNightCycle.cs, num GameObject vazio DayNight:

using System;
using UnityEngine;
using UnityEngine.Rendering;

public class DayNightCycle : MonoBehaviour
{
    [Header("Relógio")]
    [SerializeField, Range(0f, 24f)] private float timeOfDay = 8f;
    [SerializeField] private float dayLengthMinutes = 12f;
    [SerializeField] private bool paused = false;

    [Header("Sol")]
    [SerializeField] private Light sun;
    [SerializeField] private float sunYaw = 170f;
    [SerializeField] private Gradient sunColor = new Gradient();
    [SerializeField] private AnimationCurve sunIntensity = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
    [SerializeField] private float maxSunIntensity = 1.5f;

    [Header("Lua")]
    [SerializeField] private Light moon;
    [SerializeField] private float maxMoonIntensity = 0.15f;

    [Header("Luz ambiente (Source = Gradient)")]
    [SerializeField] private Gradient ambientSky = new Gradient();
    [SerializeField] private Gradient ambientEquator = new Gradient();
    [SerializeField] private Gradient ambientGround = new Gradient();

    public event Action<int> HourChanged;

    public float TimeOfDay => timeOfDay;

    private int lastHour = -1;

    private void Start()
    {
        RenderSettings.sun = sun;
        RenderSettings.ambientMode = AmbientMode.Trilight;
        ApplyLighting();
    }

    private void Update()
    {
        if (!paused)
        {
            float hoursPerSecond = 24f / (dayLengthMinutes * 60f);
            timeOfDay += hoursPerSecond * Time.deltaTime;
            if (timeOfDay >= 24f)
            {
                timeOfDay -= 24f;
            }
        }

        ApplyLighting();
        CheckHourChanged();
    }

    private void ApplyLighting()
    {
        float t = timeOfDay / 24f;

        float sunPitch = t * 360f - 90f;
        sun.transform.rotation = Quaternion.Euler(sunPitch, sunYaw, 0f);
        sun.color = sunColor.Evaluate(t);
        sun.intensity = sunIntensity.Evaluate(t) * maxSunIntensity;

        bool sunUp = sun.intensity > 0.01f;
        sun.enabled = sunUp;

        if (moon != null)
        {
            moon.transform.rotation = Quaternion.Euler(sunPitch + 180f, sunYaw, 0f);
            moon.intensity = (1f - sunIntensity.Evaluate(t)) * maxMoonIntensity;
            moon.enabled = moon.intensity > 0.01f;
        }

        RenderSettings.ambientSkyColor = ambientSky.Evaluate(t);
        RenderSettings.ambientEquatorColor = ambientEquator.Evaluate(t);
        RenderSettings.ambientGroundColor = ambientGround.Evaluate(t);
    }

    private void CheckHourChanged()
    {
        int hour = Mathf.FloorToInt(timeOfDay);
        if (hour != lastHour)
        {
            lastHour = hour;
            HourChanged?.Invoke(hour);
        }
    }

    public void SetTime(float hour)
    {
        timeOfDay = Mathf.Repeat(hour, 24f);
        ApplyLighting();
    }
}

O que cada parte faz:

  • RenderSettings.sun = sun garante que o skybox procedural siga essa luz, o mesmo que preencher Sun Source no Lighting window.
  • RenderSettings.ambientMode = AmbientMode.Trilight é o equivalente ao Source: Gradient no Inspector: três cores (céu, horizonte, chão) que você troca por script sem custo de recálculo.
  • O sol é desligado quando a intensidade chega perto de zero. Uma Directional Light com intensidade zero continua calculando sombras.
  • A lua é uma segunda Directional Light, oposta ao sol, com intensidade baixa e cor fria.
  • O evento HourChanged avisa outros scripts quando a hora inteira muda, sem cada um ficar comparando o relógio todo frame.

Cor e intensidade com Gradient e AnimationCurve

O Gradient e a AnimationCurve são os dois campos que transformam o script em ferramenta de arte: você ajusta tudo no Inspector, com o jogo rodando, sem mexer em código. O eixo de ambos vai de 0 (meia-noite) a 1 (meia-noite seguinte), e 0,5 é meio-dia.

Valores de partida para o sunColor:

  • 0,25 (6h): laranja quente, como #FF9A4D.
  • 0,35 (8h24): amarelo claro, #FFE6B3.
  • 0,5 (12h): quase branco, #FFF8EB.
  • 0,7 (16h48): amarelo, #FFE0A0.
  • 0,75 (18h): laranja avermelhado, #FF7A3D.

Para a sunIntensity, a curva fica em 0 até 0,23, sobe até 1 por volta de 0,35, fica em 1 até 0,65 e desce a 0 em 0,77. O desenho da curva decide quanto tempo dura o amanhecer.

Para a luz ambiente: ambientSky azul claro de dia (#8FB5E6) e azul escuro à noite (#1A2440); ambientEquator mais claro no meio do dia e lilás no entardecer; ambientGround marrom escuro, quase constante.

Céu e luz ambiente: Skybox procedural e Environment Lighting

Para o céu acompanhar o sol:

  1. Assets > Create > Material, chame de Sky_Procedural.
  2. No Shader, busque e escolha Skybox/Procedural.
  3. Window > Rendering > Lighting, aba Environment: arraste o material para Skybox Material e a Directional Light do sol para Sun Source.

O Skybox/Procedural não usa textura: ele calcula o céu pela direção da luz. Quando o sol desce, o horizonte avermelha e o céu escurece sozinho. Os parâmetros do material (Sun Size, Atmosphere Thickness, Sky Tint, Ground, Exposure) dão o estilo. Para trocar o céu por outro tipo, o post de skybox na Unity mostra Procedural, Panoramic e Cubemap.

E a luz ambiente? Aqui está a escolha de desempenho:

  • Source: Gradient (o que o script usa): você define as três cores por código, todo frame se quiser, e a Unity aplica direto. Barato e previsível.
  • Source: Skybox: a luz ambiente vem do céu, calculada num cubemap de ambiente. Quando o céu muda, esse cubemap não se atualiza sozinho. A API DynamicGI.UpdateEnvironment() agenda essa atualização, e a documentação avisa que ela lê o resultado de volta da GPU (pode atrasar um ou mais frames, ou travar a thread onde não há leitura assíncrona) e que pedidos em excesso são ignorados. Se você escolher esse caminho, chame a cada poucos segundos, não a cada frame.

Para a maioria dos jogos, Gradient com cores ajustadas à mão fica melhor e custa menos.

Fazer o sol girar é a parte vistosa. Ligar esse relógio ao resto (NPCs que mudam de rotina, inimigos que só aparecem à noite, loja que fecha, save que guarda a hora) é o trabalho que dá sentido ao ciclo. Construir esses sistemas conversando entre si, com o código organizado para crescer, é o tipo de projeto que um curso de Unity percorre do começo ao fim.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

Erros comuns: noite preta demais, sombras piscando e custo de performance

Noite preta demais. O ambiente noturno está em preto e não há lua. Noite em jogo é convenção, não física: use azul escuro no ambiente (nunca #000000), a lua com intensidade entre 0,1 e 0,2 e, se o jogo pedir, luzes locais (tochas, janelas) que ligam pelo evento HourChanged.

Sombras piscando ou "andando". Girar a Directional Light todo frame faz o mapa de sombras ser recalculado de um ângulo ligeiramente diferente, e as bordas tremem. Em ângulos baixos (amanhecer, entardecer), as sombras ficam longas e a resolução por metro cai. Correções: aplique a rotação em degraus pequenos (a cada 0,1 grau) em vez de contínua; desligue as sombras do sol quando a intensidade estiver baixa; ajuste Depth Bias e Normal Bias da luz se aparecer serrilhado.

O céu não muda. O Sun Source está vazio ou aponta para outra luz, ou o material não é Skybox/Procedural. Panoramic e Cubemap são imagens fixas.

Mudar o material do céu por código altera o asset. RenderSettings.skybox.SetFloat(...) mexe no material salvo no projeto, e a mudança persiste depois do Play no editor. Crie uma cópia com new Material(original) no Start e atribua a RenderSettings.skybox.

Custo de performance. Iluminação baked (Lightmaps) não combina com o sol se movendo, porque o lightmap é calculado para uma posição fixa. Num ciclo de dia e noite, o sol é luz em tempo real. Se o jogo precisa de muitas luzes ou roda em celular, meça antes de decidir; o post de otimização para 60 FPS mostra como usar o Profiler para achar o gargalo.

Próximo passo: eventos por hora do dia

Com o evento HourChanged, ligue um sistema simples: um script StreetLamp que acende às 18h e apaga às 6h, inscrito no evento no OnEnable e removido no OnDisable. Depois, um spawner que só cria inimigos entre 20h e 4h. Quando dois sistemas diferentes estiverem reagindo ao mesmo relógio, o ciclo deixa de ser efeito visual e vira mecânica. E salve o TimeOfDay junto com o save do jogo, usando o SetTime para restaurar.

Perguntas frequentes

Como fazer o skybox mudar junto com o sol na Unity?

Use um material com o shader Skybox/Procedural e defina a sua Directional Light como Sun Source no Lighting window (aba Environment). O céu procedural lê a rotação dessa luz, então girar o sol por script já escurece o céu e muda a cor do horizonte.

Por que a noite fica completamente preta no meu ciclo de dia e noite?

Porque a única luz da cena é o sol e a luz ambiente vem do céu, que à noite fica escuro. Defina uma cor ambiente noturna azulada em vez de preto e adicione uma segunda Directional Light fraca para a lua, com cor fria e intensidade baixa.

Preciso chamar DynamicGI.UpdateEnvironment todo frame?

Não. Ele agenda uma atualização do cubemap de ambiente, com leitura de volta da GPU, e pedidos em excesso são ignorados. Se a luz ambiente vem do Skybox, chame em intervalos de alguns segundos. Se vem de Gradient ou Color, você define as cores direto por script e não precisa dele.

Quanto tempo deve durar um dia no jogo?

Depende do ritmo. Jogos de fazenda e sobrevivência costumam usar dias de 10 a 24 minutos reais. O importante é deixar a duração num campo ajustável e testar jogando: dia curto demais vira pisca-pisca, longo demais e o jogador não percebe o ciclo.