Voltar para o Blog
Quest Log

Ciclo de Dia e Noite no Godot 4

Cena 2D no Godot mostrando a transicao gradual de cor entre dia e noite

Monte um ciclo dia e noite godot com autoload, CanvasModulate e gradiente por hora. Interpole cores, emita signals ao virar e use delta para FPS estavel.

Um ciclo dia e noite godot bem feito muda a sensacao de um jogo 2D sem custo de arte: a mesma cena ganha manha, tarde e madrugada so trocando a cor que cobre tudo. Neste tutorial voce vai montar a logica de tempo em um autoload, escurecer a tela com um CanvasModulate, calcular a cor a partir de um gradiente por hora do dia e disparar signals quando o periodo virar. Tudo independente de FPS, usando delta, para que o relogio do jogo ande na mesma velocidade em qualquer maquina.

Ciclo de Dia e Noite no Godot 4

A ideia central e separar duas responsabilidades. Uma parte controla o tempo (que horas sao no jogo, quanto um dia dura, se virou noite). Outra parte le esse tempo e aplica o efeito visual. Manter isso separado deixa o sistema reaproveitavel: a HUD pode mostrar o relogio, os inimigos podem ficar mais fortes a noite e a iluminacao reage, tudo lendo a mesma fonte de verdade.

O relogio do jogo em um autoload

O tempo precisa existir fora de qualquer cena especifica, porque ele continua correndo quando voce troca de fase. Um autoload (singleton) resolve isso. Se voce ainda nao usa esse padrao, vale ler autoload e singleton no Godot antes de seguir, porque o restante depende dele.

Crie um script time_of_day.gd e registre em Project Settings, Autoload, com o nome TimeOfDay.

extends Node

## Duracao de um dia completo do jogo, em segundos reais.
@export var day_length_seconds: float = 120.0

## Hora atual do jogo, de 0.0 ate 24.0.
var current_hour: float = 8.0

signal hour_changed(hour: float)
signal became_night
signal became_day

# Faixa que consideramos "noite".
const NIGHT_START: float = 19.0
const NIGHT_END: float = 6.0

var _is_night: bool = false

func _process(delta: float) -> void:
    # Quantas horas do jogo passam por segundo real.
    var hours_per_second: float = 24.0 / day_length_seconds
    current_hour += hours_per_second * delta

    if current_hour >= 24.0:
        current_hour -= 24.0

    hour_changed.emit(current_hour)
    _check_day_night()

func _check_day_night() -> void:
    var night_now: bool = current_hour >= NIGHT_START or current_hour < NIGHT_END
    if night_now and not _is_night:
        _is_night = true
        became_night.emit()
    elif not night_now and _is_night:
        _is_night = false
        became_day.emit()

O ponto importante e o delta. Em vez de somar um valor fixo por frame (o que faria o dia passar rapido em 144 FPS e devagar em 30 FPS), multiplicamos a velocidade por delta. Assim current_hour avanca a mesma quantidade por segundo real, nao por frame. Com day_length_seconds = 120, um dia completo do jogo leva dois minutos reais, em qualquer hardware.

Note tambem que _check_day_night so emite o signal na transicao, comparando o estado atual com _is_night. Sem essa guarda, became_night dispararia em todo frame da noite, o que entupiria qualquer codigo conectado.

Escurecendo a cena com CanvasModulate

Em 2D, a forma mais limpa de aplicar uma cor global e o no CanvasModulate. Ele multiplica a cor de tudo que esta no mesmo CanvasLayer. Um CanvasModulate branco (Color(1, 1, 1)) nao muda nada; um azul escuro deixa a cena com cara de madrugada. Adicione um CanvasModulate na sua cena principal e crie um script que reage ao tempo.

extends CanvasModulate

@onready var _time := TimeOfDay

func _ready() -> void:
    _time.hour_changed.connect(_on_hour_changed)
    # Aplica a cor inicial sem esperar o proximo frame.
    _on_hour_changed(_time.current_hour)

func _on_hour_changed(hour: float) -> void:
    color = _color_for_hour(hour)

Aqui usamos a referencia direta ao autoload TimeOfDay e conectamos ao signal hour_changed. O calculo da cor fica em _color_for_hour, que vamos definir com um gradiente.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Gradiente por hora do dia

Poderiamos escrever varios if para cada faixa de horario, mas um Gradient deixa o ajuste muito mais simples e visual. O recurso Gradient do Godot mapeia uma posicao de 0.0 a 1.0 para uma cor interpolada. Vamos tratar essa posicao como a hora normalizada (hour / 24.0) e deixar o proprio Godot interpolar entre os pontos.

Voce pode criar o Gradient pela interface, mas montar por codigo deixa o exemplo completo e reproduzivel.

extends CanvasModulate

@onready var _time := TimeOfDay

var _gradient: Gradient

func _ready() -> void:
    _build_gradient()
    _time.hour_changed.connect(_on_hour_changed)
    _on_hour_changed(_time.current_hour)

func _build_gradient() -> void:
    _gradient = Gradient.new()
    # offsets vao de 0.0 (meia-noite) a 1.0 (meia-noite seguinte).
    _gradient.offsets = PackedFloat32Array([
        0.0,   # 00h madrugada
        0.25,  # 06h amanhecer
        0.5,   # 12h meio-dia
        0.75,  # 18h entardecer
        1.0    # 24h madrugada
    ])
    _gradient.colors = PackedColorArray([
        Color(0.10, 0.12, 0.30),  # noite profunda
        Color(0.85, 0.70, 0.60),  # tom quente do nascer
        Color(1.00, 1.00, 1.00),  # dia pleno, sem alteracao
        Color(0.90, 0.65, 0.55),  # laranja do por do sol
        Color(0.10, 0.12, 0.30)   # volta para a noite
    ])

func _color_for_hour(hour: float) -> Color:
    var position: float = hour / 24.0
    return _gradient.sample(position)

func _on_hour_changed(hour: float) -> void:
    color = _color_for_hour(hour)

O metodo sample faz a interpolacao linear entre os offsets. Como hour_changed dispara a cada frame, a cor muda de forma continua e suave, sem precisar de Tween. O primeiro e o ultimo ponto usam a mesma cor para que a virada de 24h para 0h nao tenha salto. Repare que o meio-dia usa branco puro, ou seja, o CanvasModulate nao altera a arte original quando o sol esta a pino.

Se preferir, exporte o Gradient como recurso e ajuste no inspetor, arrastando os pontos ate o visual ficar do seu gosto. A logica do script nao muda; voce so troca como o gradiente e carregado.

Reagindo a virada de dia e noite

Os signals became_night e became_day existem para o resto do jogo reagir a transicao, nao a cor. Um exemplo comum: acender lampadas, mudar a musica ou liberar inimigos noturnos. Qualquer no pode se conectar.

extends Node2D

@onready var _lamp: PointLight2D = $PointLight2D

func _ready() -> void:
    TimeOfDay.became_night.connect(_on_became_night)
    TimeOfDay.became_day.connect(_on_became_day)
    # Estado inicial coerente.
    _lamp.enabled = false

func _on_became_night() -> void:
    _lamp.enabled = true

func _on_became_day() -> void:
    _lamp.enabled = false

Como o autoload dispara o signal uma unica vez na transicao, esse codigo roda exatamente quando precisa. Se voce quiser combinar com luzes 2D mais elaboradas, o artigo sobre iluminacao 3D no Godot traz conceitos de cor e intensidade que tambem ajudam a pensar a ambientacao em 2D.

Suavizando a transicao com Tween

O CanvasModulate ja muda suave porque o gradiente e amostrado a cada frame. Mas em momentos especificos, como pular direto para a noite ao entrar numa caverna, voce pode querer uma transicao controlada. Para isso, um Tween anima a propriedade color ao longo de um tempo fixo.

extends CanvasModulate

func force_to(target_color: Color, duration: float = 1.5) -> void:
    var tween := create_tween()
    tween.tween_property(self, "color", target_color, duration)

Chamando force_to(Color(0.05, 0.05, 0.15), 2.0) voce escurece a cena em dois segundos, sem depender da hora do jogo naquele instante. Para entender melhor as opcoes de easing e encadeamento, o material sobre Tween e animacao por codigo cobre os ajustes finos. Lembre de pausar o avanco do relogio enquanto a cutscene roda, ou reativar hour_changed so depois, para a cor nao brigar com o Tween.

Mostrando a hora na HUD

Com o autoload pronto, exibir um relogio e trivial. Qualquer Label se conecta ao signal e formata o valor.

extends Label

func _ready() -> void:
    TimeOfDay.hour_changed.connect(_on_hour_changed)

func _on_hour_changed(hour: float) -> void:
    var h: int = int(hour)
    var m: int = int((hour - h) * 60.0)
    text = "%02d:%02d" % [h, m]

Convertendo a parte fracionaria de current_hour em minutos, voce mostra algo como 08:30. Como o valor vem do mesmo autoload, HUD, iluminacao e logica de inimigos ficam sempre sincronizados.

Ajustes praticos

Alguns pontos que valem atencao quando o sistema cresce. Primeiro, evite chamar _gradient.sample em dezenas de nos por frame. Centralize o calculo da cor no autoload e exponha o resultado, ou deixe so o CanvasModulate amostrar e os demais lerem current_hour. Segundo, se voce pausar o jogo, lembre que _process continua rodando a menos que voce ajuste o process_mode do autoload; defina o comportamento de pausa que faz sentido para o seu jogo.

Terceiro, para salvar e carregar, persista apenas current_hour. Ao restaurar, chame _check_day_night uma vez para acertar o estado de noite sem disparar a transicao errada, e reaplique a cor com _on_hour_changed(current_hour). Assim o jogador volta exatamente para o momento do dia em que parou.

Com esses pilares (tempo no autoload, cor por gradiente, signals na virada e tudo regido por delta), voce tem um ciclo de dia e noite que escala junto com o projeto. A partir daqui da para somar fases da lua, estacoes do ano ou eventos que so acontecem em certos horarios, sempre lendo a mesma fonte de tempo.