Voltar para o Blog
Quest Log

Terreno procedural na Godot 4 com FastNoiseLite (ruído Perlin/Simplex)

mapa de terreno gerado por computador com regiões de água, areia, grama e montanha

Aprenda a gerar terreno procedural na Godot 4 com FastNoiseLite: leia ruído com get_noise_2d, use seed, mapeie biomas e pinte no TileMapLayer.

Gerar terreno procedural na Godot 4 é mais simples do que parece quando você entende uma peça central: o recurso FastNoiseLite. Neste tutorial você vai criar um mapa 2D top-down por tiles, ler valores de ruído com get_noise_2d(x, y), fixar um seed para reproduzir o mundo e mapear a faixa do ruído em biomas (água, areia, grama, montanha) pintados num TileMapLayer. Tudo em GDScript tipado, com a API real da Godot, sem inventar método.

O que é ruído procedural e por que ele serve para terreno

Antes de pensar em terreno, é preciso separar dois conceitos que iniciantes costumam confundir: aleatório puro e ruído procedural.

Se você preencher cada tile do mapa com randi(), cada célula recebe um valor sem nenhuma relação com o vizinho. O resultado é estática de TV: pontos soltos, sem forma, sem continente, sem praia. Não serve para terreno porque terreno do mundo real tem continuidade. Perto da água há areia, perto da areia há grama, e a montanha fica no miolo, não pulando aleatoriamente ao lado do oceano.

Ruído procedural (as famílias Perlin e Simplex) resolve isso. Ele é uma função matemática que, dado um ponto (x, y), devolve um número que varia de forma suave conforme você caminha pelo espaço. Dois pontos vizinhos retornam valores próximos. Isso cria gradientes: regiões baixas que sobem devagar até virar picos. É exatamente a propriedade que faz um monte de números parecer um relevo.

Na Godot 4, esse gerador vem pronto na classe FastNoiseLite. Você não precisa implementar Perlin na mão. Cria o recurso, configura, e pede valores.

Criando o recurso FastNoiseLite em GDScript

Você pode criar um FastNoiseLite pela interface (num export de recurso) ou direto no código. Para aprender, o código deixa tudo explícito:

extends Node2D

var noise: FastNoiseLite

func _ready() -> void:
    noise = FastNoiseLite.new()
    noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
    noise.seed = 1337
    noise.frequency = 0.02

Três linhas de configuração importam já aqui. O noise_type escolhe o algoritmo; TYPE_SIMPLEX é uma boa escolha padrão para terreno (rápido e com poucos artefatos). O seed fixa qual mundo será gerado. O frequency controla o "tamanho" do relevo, e já volto nele com calma.

Lendo valores com get_noise_2d

Com o recurso pronto, ler um ponto é uma chamada só:

func amostrar(x: int, y: int) -> float:
    var valor: float = noise.get_noise_2d(float(x), float(y))
    return valor

O detalhe que pega todo mundo de surpresa: get_noise_2d retorna um float aproximadamente entre -1.0 e 1.0. Não é 0 a 1, não é 0 a 255. É uma faixa centrada no zero. Guarde isso, porque a decisão de bioma depende inteira dessa faixa.

Se você passar coordenadas inteiras de tile diretamente (0, 1, 2, 3...), o ruído varia devagar entre tiles vizinhos justamente por causa da frequency baixa. É isso que dá a continuidade do terreno.

Seed: reprodutibilidade do mundo

O seed é o número que semeia o gerador. Com o mesmo seed e a mesma configuração, get_noise_2d(10, 20) devolve sempre o mesmo valor. Isso é ouro por dois motivos.

Primeiro, teste e depuração: se um mapa deu bom, você anota o seed e recria aquele exato mundo quando quiser. Segundo, mundos compartilháveis: dá para deixar o jogador digitar um número e jogar o mesmo mapa que um amigo, no estilo dos códigos de mundo de vários jogos de sobrevivência.

Se você quer um mundo diferente a cada partida, sorteie o seed uma vez, no início:

func novo_mundo() -> void:
    noise.seed = randi()
    print("seed deste mundo: ", noise.seed)

Note que o sorteio acontece uma vez, não por tile. O que gera a variação espacial do terreno é o ruído em si, não randi() por célula. O seed só escolhe qual dos infinitos mundos possíveis você vai visitar.

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

Mapeando a faixa do ruído em biomas

Agora a parte que transforma números em mapa. Você tem, para cada tile, um float de -1 a 1. A ideia é cortar essa faixa em faixas menores, cada uma virando um bioma. Pense como altitude: o mais baixo é água, sobe para areia (praia), depois grama, e o topo é montanha.

enum Bioma { AGUA, AREIA, GRAMA, MONTANHA }

func bioma_para(valor: float) -> Bioma:
    if valor < -0.2:
        return Bioma.AGUA
    elif valor < 0.0:
        return Bioma.AREIA
    elif valor < 0.4:
        return Bioma.GRAMA
    else:
        return Bioma.MONTANHA

Esses limiares (-0.2, 0.0, 0.4) são o seu botão de design. Baixar o corte da água aumenta a área de mar. Subir o corte da montanha deixa picos raros. Ajuste no olho até o mapa ficar com a proporção que você quer. Não existe número mágico; existe o mapa que combina com o seu jogo.

Pintando no TileMapLayer

Na Godot 4.3 em diante, o nó de tiles moderno é o TileMapLayer (o antigo TileMap com múltiplas camadas foi dividido em nós de camada). Você precisa de um TileMapLayer com um TileSet configurado, onde cada bioma corresponde a um tile no atlas.

O método que pinta uma célula é set_cell. A assinatura na Godot 4 é set_cell(coords: Vector2i, source_id: int, atlas_coords: Vector2i). Vamos mapear cada bioma para uma coordenada de atlas e varrer o mapa:

@export var mapa: TileMapLayer
@export var largura: int = 128
@export var altura: int = 128

const SOURCE_ID: int = 0

func atlas_do_bioma(bioma: Bioma) -> Vector2i:
    match bioma:
        Bioma.AGUA:
            return Vector2i(0, 0)
        Bioma.AREIA:
            return Vector2i(1, 0)
        Bioma.GRAMA:
            return Vector2i(2, 0)
        Bioma.MONTANHA:
            return Vector2i(3, 0)
        _:
            return Vector2i(0, 0)

func gerar_mapa() -> void:
    mapa.clear()
    for y: int in range(altura):
        for x: int in range(largura):
            var valor: float = noise.get_noise_2d(float(x), float(y))
            var bioma: Bioma = bioma_para(valor)
            var atlas: Vector2i = atlas_do_bioma(bioma)
            mapa.set_cell(Vector2i(x, y), SOURCE_ID, atlas)

Junte as peças no _ready e você já tem um gerador funcional:

func _ready() -> void:
    noise = FastNoiseLite.new()
    noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
    noise.seed = randi()
    noise.frequency = 0.02
    gerar_mapa()

Ajuste SOURCE_ID e as coordenadas de atlas para bater com o seu TileSet real. Se o mapa sair todo de um tile só, quase sempre o source_id ou o atlas estão errados, ou a frequency está tão alta que tudo vira ruído sem forma. É o gancho para o próximo tópico.

Controlando frequency e octaves: o tamanho e a textura do relevo

Dois parâmetros definem a "cara" do terreno.

A frequency controla a escala. Frequência baixa (algo como 0.01) espalha o ruído: as manchas ficam grandes, gerando continentes e oceanos amplos. Frequência alta (perto de 0.2 ou mais) comprime tudo: cada tile vira quase independente do vizinho e o mapa perde a continuidade, voltando à aparência de estática. Comece em 0.02 e mexa devagar.

Os octaves entram pela parte fractal do FastNoiseLite. Ruído fractal soma várias camadas de ruído em escalas diferentes: uma camada grande dá a forma geral, camadas menores adicionam detalhe. Você liga isso com o tipo fractal e a contagem de oitavas:

func configurar_fractal() -> void:
    noise.fractal_type = FastNoiseLite.FRACTAL_FBM
    noise.fractal_octaves = 4
    noise.fractal_lacunarity = 2.0
    noise.fractal_gain = 0.5

Com fractal_octaves = 1 o relevo é liso e suave. Subindo para 4 ou 5, as bordas dos biomas ganham recortes, penínsulas e ilhotas, um litoral menos "certinho". O fractal_gain controla quanto cada oitava adicional pesa (0.5 é o padrão sensato), e fractal_lacunarity controla o salto de escala entre oitavas. Para a maioria dos mapas top-down, FRACTAL_FBM com 4 oitavas é um ponto de partida honesto.

A regra prática: comece com poucas oitavas e frequência baixa para acertar a forma dos continentes, depois adicione oitavas para dar textura ao litoral. Mexer nos dois ao mesmo tempo confunde; isole uma variável por vez.

Próximos passos

Você tem um gerador de terreno procedural completo: recurso FastNoiseLite configurado, leitura com get_noise_2d, seed reproduzível, mapeamento por biomas e pintura no TileMapLayer. A partir daqui, alguns caminhos naturais.

Para deixar a transição entre biomas bonita, sem aquele degrau seco entre grama e areia, o próximo passo é usar autotile de terreno no TileMap, que escolhe automaticamente o tile de borda certo. Se o seu interesse é gerar espaços fechados em vez de mundo aberto, a lógica muda de ruído para grafo, e vale ver como gerar uma dungeon procedural em 2D. E se a ideia é levar essa mesma noção de ruído para mundos por blocos, dá para escalar o conceito e fazer um jogo estilo Minecraft na Godot, onde o mesmo get_noise_2d vira mapa de altura por coluna.

O importante é ter internalizado a ideia central: ruído não é aleatório puro, é aleatório com continuidade. É essa continuidade que faz números virarem terreno. Com FastNoiseLite na Godot 4, você controla a escala com frequency, a textura com fractal_octaves e a reprodutibilidade com seed. O resto é ajustar limiares até o mapa parecer um lugar onde dá vontade de jogar.

Perguntas frequentes

Qual a diferença entre get_noise_2d e get_noise_3d?

get_noise_2d(x, y) amostra o ruído em um plano, ideal para mapas top-down por tiles. get_noise_3d(x, y, z) adiciona uma terceira dimensão, útil para volumes ou para animar o ruído no tempo usando z como quadro. Para terreno 2D top-down, get_noise_2d resolve.

Por que meu terreno parece só ruído sem forma de continente?

Quase sempre a frequency está alta demais. Valores como 0.01 a 0.05 geram manchas grandes e contínuas (continentes). Valores próximos de 0.5 deixam cada tile independente do vizinho, virando estática. Baixe a frequency até ver massas suaves.

O mesmo seed sempre gera o mesmo mapa?

Sim. Fixar noise.seed garante que get_noise_2d retorne os mesmos valores para as mesmas coordenadas, então o terreno é reproduzível. Troque o seed para gerar um mundo diferente e guarde o número se quiser recriar aquele mapa depois.

FastNoiseLite serve para mundos infinitos?

Serve. Como get_noise_2d é uma função pura das coordenadas, você pode consultar qualquer (x, y) sob demanda, inclusive coordenadas negativas e distantes, sem precisar guardar o mapa inteiro na memória. Isso é a base de geração por chunks.