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.

Ferramenta:Godot

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.

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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot 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 Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 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, vale ver como montar iluminacao 2D no Godot com luz e sombra, e 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.

Levando a cor para um shader

O CanvasModulate e a solucao mais limpa, mas ele tinge tudo igual. Em algum momento voce vai querer que partes da cena reajam diferente da media, por exemplo janelas que acendem a noite enquanto o resto escurece, ou uma lampada que ignora o escurecimento. Para esse controle fino, um shader de canvas_item por material resolve, e ele le a mesma hora do autoload. Essa e a ponte natural entre esse sistema e o resto do cluster de shaders. Se voce nunca escreveu um, comece pelos shaders no Godot para iniciantes, que cobrem a sintaxe que uso abaixo.

Crie um arquivo ambiente.gdshader:

shader_type canvas_item;

// Cor de ambiente vinda do autoload, 0..1 por canal.
uniform vec4 ambient_color : source_color = vec4(1.0, 1.0, 1.0, 1.0);

// Quanto este material obedece ao ciclo. 0.0 ignora, 1.0 obedece tudo.
uniform float reacao : hint_range(0.0, 1.0) = 1.0;

void fragment() {
    vec4 tex = texture(TEXTURE, UV);
    vec3 tingido = tex.rgb * ambient_color.rgb;
    // Interpola entre a cor original e a tingida conforme "reacao".
    COLOR = vec4(mix(tex.rgb, tingido, reacao), tex.a);
}

No lado do GDScript, um ShaderMaterial recebe a cor a cada virada de hora. Reaproveite o mesmo gradiente do CanvasModulate para manter tudo coerente.

extends Sprite2D

@onready var _time := TimeOfDay
var _mat: ShaderMaterial

func _ready() -> void:
    _mat = material as ShaderMaterial
    _time.hour_changed.connect(_on_hour_changed)
    _on_hour_changed(_time.current_hour)

func _on_hour_changed(hour: float) -> void:
    # Reaproveita a cor que o CanvasModulate ja calcula pelo gradiente.
    var cor: Color = TimeOfDay.cor_ambiente_para(hour)
    _mat.set_shader_parameter("ambient_color", cor)

Para isso funcionar, mova o calculo do gradiente para dentro do autoload e exponha um metodo cor_ambiente_para(hour: float) -> Color, assim tanto o CanvasModulate quanto cada shader leem a mesma fonte. Uma janela que acende a noite e so um Sprite2D com reacao baixo e um segundo material aditivo controlado pelo signal became_night. A partir daqui da para empilhar efeitos de ambientacao mais pesados do mesmo cluster, como uma nevoa de guerra por shader que escurece as bordas do mapa, ou um shader de agua 2D cujo brilho muda entre o dia e a noite usando essa mesma ambient_color. Todos esses tutoriais, junto com o resto do que da para fazer na engine, estao reunidos no hub de Godot.

Erro comum: a HUD escurecendo junto com a cena

O tropeço mais frequente de quem monta esse sistema: a noite chega e o relogio, a barra de vida e o menu de pausa escurecem junto com o cenario, ficando ilegiveis de madrugada. Acontece porque o CanvasModulate multiplica a cor de tudo que vive no mesmo canvas, e a HUD montada como filha direta da cena principal esta exatamente nesse canvas.

A solucao e colocar toda a interface dentro de um no CanvasLayer. Cada CanvasLayer cria um canvas proprio, fora do alcance do CanvasModulate da cena. A estrutura fica assim:

Main (Node2D)
├── CanvasModulate        # tinge o mundo
├── Mundo (Node2D)        # tiles, player, inimigos
└── HUD (CanvasLayer)     # relogio, vida, menus: imune ao tingimento
    └── Label

Nenhuma linha de codigo a mais: basta a interface morar no CanvasLayer que ela permanece com as cores originais em qualquer hora do dia. O mesmo vale ao contrario: se voce quer que algum elemento de tela escureca junto de proposito, como uma moldura de vinheta, deixe esse elemento fora do CanvasLayer da HUD.

Inimigos que so existem a noite

Os signals became_night e became_day ja acendem lampadas no exemplo acima, mas o uso que mais muda o jogo e controlar spawn. Um spawner conectado as duas transicoes cria os inimigos quando escurece e limpa tudo quando amanhece:

extends Node2D

@export var cena_inimigo: PackedScene
@export var pontos_de_spawn: Array[Marker2D] = []

var _inimigos_vivos: Array[Node2D] = []

func _ready() -> void:
    TimeOfDay.became_night.connect(_on_became_night)
    TimeOfDay.became_day.connect(_on_became_day)

func _on_became_night() -> void:
    for ponto in pontos_de_spawn:
        var inimigo: Node2D = cena_inimigo.instantiate()
        inimigo.global_position = ponto.global_position
        add_child(inimigo)
        _inimigos_vivos.append(inimigo)

func _on_became_day() -> void:
    for inimigo in _inimigos_vivos:
        if is_instance_valid(inimigo):
            inimigo.queue_free()
    _inimigos_vivos.clear()

Dois detalhes fazem diferenca aqui. O is_instance_valid protege contra inimigos que o jogador ja derrotou durante a noite: sem essa checagem, o queue_free num no morto gera erro. E o array _inimigos_vivos tipado garante que o spawner so limpa o que ele mesmo criou, sem encostar em inimigos diurnos ou de outros sistemas.

O mesmo padrao serve para qualquer regra ligada ao horario: loja que fecha a noite, NPC que vai dormir, musica que troca de faixa. Cada sistema se conecta aos signals e cuida da propria reacao, sem o autoload saber que eles existem. E se a regra depender de uma faixa de hora especifica em vez da virada (um mercador que so aparece entre 10h e 14h, por exemplo), basta ler TimeOfDay.current_hour no _process do proprio NPC e comparar com a janela desejada.

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. Um complemento natural e um sistema de clima com chuva na Godot, que se encaixa direto nessa mesma logica de tempo. Esse relogio e a espinha dorsal de um farming sim, o caso de uso classico do ciclo de dia; se a ideia e seguir por esse caminho, veja como fazer um jogo de fazenda.