Ciclo de Dia e Noite no Godot 4

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.
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.
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.
