Numeros Aleatorios e RNG no Godot 4

Guia pratico de numeros aleatorios godot 4 com randi, randf, randi_range, seed, RandomNumberGenerator, pick_random, chance percentual e distribuicao ponderada.
Quase todo jogo precisa de alguma dose de acaso: o dano critico que acontece de vez em quando, o item que cai de um inimigo, o spawn que escolhe uma posicao livre. Trabalhar com numeros aleatorios godot parece simples no comeco (chama uma funcao, recebe um valor), mas existem detalhes que separam um sistema confiavel de um que gera bugs dificeis de reproduzir. Neste post a gente cobre as funcoes globais (randi, randf, randi_range, randf_range), o papel do randomize() e do seed, quando usar a classe RandomNumberGenerator, e como resolver casos comuns como sortear item de array, chance percentual e distribuicao ponderada.
Numeros Aleatorios e RNG no Godot 4
A ideia central do RNG (Random Number Generator) e que o computador nao gera acaso de verdade. Ele gera uma sequencia determinada por um valor inicial chamado seed. Mesmo seed, mesma sequencia. Entender isso e o que permite tanto variar o jogo a cada partida quanto reproduzir uma partida exata para debug ou testes.
As funcoes globais de numeros aleatorios godot
O Godot 4 expoe funcoes de RNG direto no escopo global, entao voce pode chamar de qualquer script sem instanciar nada. As quatro mais usadas:
extends Node
func _ready() -> void:
# Inteiro de 0 ate 2^32 - 1 (valor bruto, raramente usado direto)
var bruto: int = randi()
# Float entre 0.0 e 1.0 (inclui 0, nao inclui 1)
var fracao: float = randf()
# Inteiro dentro de um intervalo, incluindo os dois extremos
var dado: int = randi_range(1, 6)
# Float dentro de um intervalo
var angulo: float = randf_range(0.0, TAU)
print(bruto, " ", fracao, " ", dado, " ", angulo)
Os detalhes que importam na pratica:
- randf() devolve algo no intervalo [0, 1). O 1.0 nunca sai. Isso e util porque multiplicar por um numero da um intervalo limpo.
- randi_range(a, b) inclui tanto a quanto b. Entao randi_range(1, 6) cobre 1, 2, 3, 4, 5 e 6, como um dado de verdade.
- randf_range(a, b) tambem trabalha com os limites do jeito esperado para floats.
Se voce so precisa de um inteiro num intervalo, prefira randi_range em vez de fazer conta com randi() e operador de resto. O resto pode introduzir um vies pequeno (alguns valores saem com mais frequencia que outros) e o codigo fica mais dificil de ler.
Por que randomize() e o seed importam
Quando o jogo abre, o gerador global comeca com um seed fixo. Sem nenhuma intervencao, isso significa que a sequencia de numeros aleatorios pode se repetir igual entre execucoes. Para a maioria dos jogos voce quer variacao a cada partida, e e ai que entra o randomize():
func _ready() -> void:
randomize() # sorteia um seed novo baseado no tempo do sistema
Chame randomize() uma unica vez, cedo na vida do jogo (no autoload ou na cena inicial). Nao precisa chamar antes de cada randi(). Chamar repetidamente nao deixa o resultado "mais aleatorio", so adiciona ruido sem proposito.
O outro lado da moeda e quando voce quer o oposto: resultados reproduziveis. Imagine um bug que so acontece com uma combinacao especifica de drops. Se voce fixa o seed, consegue rodar a mesma sequencia quantas vezes precisar:
func _ready() -> void:
seed(12345) # toda execucao com este seed gera a mesma sequencia
print(randi_range(1, 100))
print(randi_range(1, 100))
Jogos de mundo procedural costumam usar essa propriedade para gerar o mesmo mapa a partir de um codigo que o jogador digita. Se voce esta indo por esse caminho, vale ler tambem o material sobre geracao procedural com IA, porque seed estavel e a base de qualquer conteudo procedural consistente.
RandomNumberGenerator para controle isolado
As funcoes globais usam um unico gerador compartilhado por todo o projeto. Isso e pratico, mas tem um problema: se um sistema chama o gerador global, ele desloca a sequencia de todos os outros. Quando voce quer um fluxo de numeros aleatorios godot independente e reproduzivel para um sistema especifico, use a classe RandomNumberGenerator.
extends Node
var rng := RandomNumberGenerator.new()
func _ready() -> void:
rng.seed = 987654321 # seed proprio, isolado do gerador global
# alternativa: rng.randomize() para um seed aleatorio neste gerador
for i in 5:
print(rng.randi_range(1, 6))
Cada instancia de RandomNumberGenerator tem o proprio estado. Voce pode ter um gerador para o sistema de loot, outro para o gerador de mapa, outro para efeitos visuais. Cada um avanca de forma independente, entao mexer em um nao quebra a reproducibilidade dos demais. A API espelha as funcoes globais: rng.randi(), rng.randf(), rng.randi_range(a, b), rng.randf_range(a, b).
Um detalhe util: alem de seed, a classe expoe a propriedade state. Voce pode salvar o state atual e restaurar depois para continuar exatamente de onde parou, o que ajuda em sistemas de save que precisam preservar o fluxo de RNG.
Sortear um item de array com pick_random
Escolher um elemento aleatorio de uma lista e tao comum que o Godot 4 ja traz o metodo pronto direto no Array:
var inimigos := ["slime", "morcego", "esqueleto", "goblin"]
func sortear_inimigo() -> String:
return inimigos.pick_random()
O pick_random() usa o gerador global, entao ele tambem responde ao randomize() e ao seed que voce configurou. Se voce precisa de sorteio reproduzivel e isolado, faca o sorteio manualmente com a sua instancia de RandomNumberGenerator:
var rng := RandomNumberGenerator.new()
var inimigos := ["slime", "morcego", "esqueleto", "goblin"]
func sortear_inimigo() -> String:
var indice := rng.randi_range(0, inimigos.size() - 1)
return inimigos[indice]
Repare no inimigos.size() - 1. Como randi_range inclui os dois extremos, o indice maximo precisa ser o tamanho menos um, senao voce estoura o array.
Chance percentual
Muito do design de jogos se resume a "isso acontece X por cento das vezes". O padrao para isso e comparar um randf() com um limite:
func tem_critico(chance_percentual: float) -> bool:
# chance_percentual no formato 0 a 100
return randf() < (chance_percentual / 100.0)
func _ready() -> void:
if tem_critico(15.0):
print("Acertou um critico")
else:
print("Dano normal")
Como randf() devolve [0, 1), comparar com 0.15 acerta aproximadamente 15 por cento das vezes ao longo de muitas chamadas. Vale lembrar que "15 por cento" e uma media de longo prazo, nao uma garantia em janelas curtas. O jogador pode passar dez golpes sem critico mesmo com chance alta, e isso e estatisticamente normal. Se o seu design precisa suavizar isso, existem tecnicas de chance crescente (pity), que entram mais no campo de balanceamento e formulas do que no RNG puro.
Distribuicao ponderada (weighted)
Nem todo sorteio e justo de proposito. Um item comum deve cair muito mais que um lendario. Para isso voce atribui um peso a cada opcao e sorteia proporcionalmente. A logica e somar todos os pesos, sortear um numero ate esse total e percorrer a lista subtraindo ate encontrar o item:
extends Node
# Quanto maior o peso, maior a chance relativa
var loot := {
"comum": 70.0,
"raro": 25.0,
"epico": 4.0,
"lendario": 1.0,
}
var rng := RandomNumberGenerator.new()
func _ready() -> void:
rng.randomize()
func sortear_loot() -> String:
var total := 0.0
for peso in loot.values():
total += peso
var sorteio := rng.randf_range(0.0, total)
var acumulado := 0.0
for item in loot:
acumulado += loot[item]
if sorteio < acumulado:
return item
# fallback de seguranca por arredondamento de float
return loot.keys().back()
Como funciona na pratica: a soma dos pesos e 100. O sorteio cai em algum ponto entre 0 e 100. Ao percorrer o dicionario somando os pesos, a faixa de 0 a 70 corresponde ao comum, de 70 a 95 ao raro, de 95 a 99 ao epico e de 99 a 100 ao lendario. Quanto maior a fatia, maior a chance.
A vantagem desse formato e que voce nao precisa que os pesos somem 100. Pode usar 7, 2, 1 e o calculo se ajusta sozinho ao total. Isso facilita o balanceamento, porque adicionar uma nova opcao nao obriga a recalcular todas as outras porcentagens. O fallback no final cobre o caso raro em que arredondamento de ponto flutuante deixa o sorteio um fio acima do acumulado.
Se voce vai usar distribuicao ponderada em spawns de inimigos, o mesmo principio se encaixa no controle de ondas. O tutorial de spawner de inimigos com waves mostra onde plugar esse sorteio dentro do loop de geracao.
Erros comuns para evitar
Alguns tropecos aparecem com frequencia em projetos iniciantes:
- Esquecer o randomize() e estranhar que o jogo sempre comeca igual. Coloque a chamada no autoload e resolva de uma vez.
- Chamar randomize() dentro de funcoes que rodam todo frame. Isso nao melhora nada e ainda custa processamento.
- Usar o gerador global em sistemas que deveriam ser reproduziveis. Para mapa procedural, loot com seed compartilhado ou replay, prefira instancias de RandomNumberGenerator.
- Calcular indice de array com size() em vez de size() - 1 e estourar o limite.
- Confundir randf() (que vai ate quase 1) com a expectativa de receber exatamente 1.0. Para chance percentual isso quase nunca importa, mas e bom saber.
Como escolher a abordagem
Para regras gerais: se o sorteio e descartavel e voce nao precisa reproduzir (efeito visual, variacao de pitch de som, jitter de posicao), as funcoes globais com randomize() resolvem com menos codigo. Se o sorteio precisa ser reproduzivel, isolado ou salvo (geracao de mundo, sistemas com seed compartilhado, replays e testes), use RandomNumberGenerator com seed explicito.
Dominar essas ferramentas e mais sobre entender o seed do que sobre decorar nomes de funcao. Quando voce sabe que o RNG e uma sequencia determinada por um valor inicial, fica facil decidir quando quer surpresa e quando quer controle. Se voce ainda esta firmando a base da linguagem, vale revisar os fundamentos no guia de GDScript do zero antes de montar sistemas maiores em cima do acaso.


