Voltar para o Blog
Quest Log

Como Fazer um Jogo Estilo Minecraft na Godot 4 (Voxel com GridMap)

Terreno de blocos coloridos estilo voxel visto em primeira pessoa, com colinas de grama, terra e pedra geradas proceduralmente

Tutorial de jogo estilo Minecraft na Godot 4: mundo de blocos com GridMap e MeshLibrary, terreno gerado com FastNoiseLite e quebrar blocos com RayCast3D.

Todo mundo que aprende desenvolvimento de jogos esbarra nessa vontade em algum momento: fazer um jogo estilo Minecraft, com mundo de blocos, terreno gerado e a liberdade de construir e destruir qualquer coisa. A boa notícia é que a base disso cabe num tutorial. A notícia honesta, que precisa vir antes de qualquer linha de código: um clone completo de Minecraft é projeto de anos. Chunks infinitos carregados sob demanda, iluminação por voxel, save de mundo, biomas, redstone... cada um desses sistemas renderia meses de trabalho. Nenhum tutorial entrega isso, e quem promete está mentindo.

O que este tutorial entrega é a base jogável de verdade: um mundo de blocos com terreno gerado proceduralmente, onde você anda em primeira pessoa, quebra blocos com o botão esquerdo e coloca blocos com o direito. Isso já cobre os três conceitos centrais do gênero: representar o mundo como uma grade de células, gerar terreno com ruído e traduzir um clique na tela em uma célula da grade. Tudo com API real da Godot 4 e GDScript com tipagem estática.

A base de um jogo estilo Minecraft: GridMap e MeshLibrary

A Godot tem um node feito sob medida para mundos em grade 3D: o GridMap. Ele funciona como um TileMap tridimensional: você define uma biblioteca de meshes (a MeshLibrary), e cada célula da grade pode conter um desses meshes, identificado por um índice inteiro. Para o nosso escopo, isso resolve o mundo inteiro sem escrever uma linha de código de renderização.

Primeiro, os blocos. Crie uma cena nova com um Node3D como raiz e adicione três filhos MeshInstance3D, com os nomes Grama, Terra e Pedra. Em cada um:

  1. No Inspector, crie um BoxMesh na propriedade Mesh e defina o Size como 1, 1, 1.
  2. Ainda no BoxMesh, crie um StandardMaterial3D em Material e escolha uma cor chapada (verde, marrom, cinza). Textura fica pra depois; cor sólida já deixa o terreno legível.
  3. Adicione um StaticBody3D como filho do MeshInstance3D, e dentro dele um CollisionShape3D com um BoxShape3D de tamanho 1, 1, 1. Sem esse corpo estático, os blocos exportados não têm colisão: o personagem atravessa o chão e o raycast não acerta nada.

Com a cena montada, use o menu Scene > Export As > MeshLibrary e salve como blocos.meshlib. A ordem dos nodes vira a ordem dos índices: Grama é o item 0, Terra é o 1, Pedra é o 2. Guarde esses números, eles aparecem no código o tempo todo.

Agora a cena principal: um Node3D raiz com um GridMap filho. No GridMap, arraste o blocos.meshlib para a propriedade Mesh Library e, importante, mude o Cell Size para 1, 1, 1. O padrão é 2, 2, 2, e se você esquecer esse passo os blocos de 1 metro vão flutuar espalhados numa grade de 2 metros.

A API do GridMap que vamos usar se resume a quatro coisas:

  • set_cell_item(position: Vector3i, item: int) preenche a célula com o item da MeshLibrary.
  • get_cell_item(position: Vector3i) -> int devolve o índice do item na célula.
  • GridMap.INVALID_CELL_ITEM, que vale -1, representa célula vazia: é o que get_cell_item retorna quando não tem bloco, e é o que você passa pro set_cell_item quando quer apagar.
  • local_to_map(local_position: Vector3) -> Vector3i e map_to_local(map_position: Vector3i) -> Vector3 convertem entre coordenadas locais do GridMap e coordenadas de célula.

Terreno procedural com FastNoiseLite

Terreno estilo Minecraft nasce de um mapa de altura: para cada coluna (x, z) do mundo, um valor de ruído decide a altura do terreno naquele ponto. A Godot 4 traz o FastNoiseLite embutido, e o método get_noise_2d(x, y) devolve um valor entre -1 e 1 que varia suavemente entre pontos vizinhos. É exatamente o que a gente precisa: colinas, e não estática de TV.

Anexe este script ao node raiz da cena principal:

extends Node3D

const GRAMA: int = 0
const TERRA: int = 1
const PEDRA: int = 2

const TAMANHO: int = 64
const ALTURA_BASE: int = 8
const AMPLITUDE: float = 6.0

@onready var grid: GridMap = $GridMap

func _ready() -> void:
    gerar_terreno()

func gerar_terreno() -> void:
    var noise: FastNoiseLite = FastNoiseLite.new()
    noise.seed = randi()
    noise.frequency = 0.03
    for x in range(-TAMANHO / 2, TAMANHO / 2):
        for z in range(-TAMANHO / 2, TAMANHO / 2):
            var valor: float = noise.get_noise_2d(float(x), float(z))
            var altura: int = ALTURA_BASE + int(valor * AMPLITUDE)
            for y in range(altura):
                var item: int = PEDRA
                if y == altura - 1:
                    item = GRAMA
                elif y >= altura - 3:
                    item = TERRA
                grid.set_cell_item(Vector3i(x, y, z), item)

O raciocínio, linha a linha: o ruído devolve algo entre -1 e 1, multiplicamos pela AMPLITUDE pra escalar isso pra uma variação de até 6 blocos, e somamos na ALTURA_BASE pra coluna nunca ter altura zero. Aí preenchemos a coluna de baixo pra cima: pedra na base, três camadas de terra perto da superfície e grama no topo. O resultado é um platô de 64 por 64 com colinas suaves.

Dois parâmetros valem experimentação. O seed muda o mundo inteiro: fixe um valor em vez de randi() e você tem o mesmo mapa toda vez, útil pra depurar. A frequency controla o zoom do ruído: valores menores (tipo 0.01) dão colinas largas e suaves, valores maiores (tipo 0.1) dão um terreno serrilhado e caótico.

Rode a cena com uma Camera3D provisória apontada pro terreno só pra conferir. Se aparecer um tabuleiro de colinas verdes, a fundação está pronta.

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

O personagem em primeira pessoa

Pra andar sobre o terreno, precisamos de um CharacterBody3D com câmera na altura dos olhos. Se é sua primeira vez montando personagem 3D na Godot, o tutorial de primeiro jogo 3D na Godot explica cada peça com calma; aqui vai a versão direta.

Estrutura da cena do jogador, dentro da cena principal:

Jogador (CharacterBody3D)
├── CollisionShape3D (CapsuleShape3D)
├── Camera3D (position y = 1.6)
│   └── RayCast3D (target_position = 0, 0, -5)

O RayCast3D filho da câmera é a peça que vai colocar e quebrar blocos daqui a pouco: como a câmera olha para o -Z local, um target_position de Vector3(0, 0, -5) dispara um raio de 5 metros pra onde quer que o jogador esteja mirando. Script do Jogador:

extends CharacterBody3D

const VELOCIDADE: float = 5.0
const FORCA_PULO: float = 7.0
const SENSIBILIDADE: float = 0.003
const GRAVIDADE: float = 20.0

@onready var camera: Camera3D = $Camera3D

func _ready() -> void:
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _input(event: InputEvent) -> void:
    if event is InputEventMouseMotion:
        var movimento: InputEventMouseMotion = event
        rotate_y(-movimento.relative.x * SENSIBILIDADE)
        camera.rotate_x(-movimento.relative.y * SENSIBILIDADE)
        camera.rotation.x = clampf(camera.rotation.x, -1.4, 1.4)

func _physics_process(delta: float) -> void:
    if not is_on_floor():
        velocity.y -= GRAVIDADE * delta
    elif Input.is_action_just_pressed("ui_accept"):
        velocity.y = FORCA_PULO
    var entrada: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    var direcao: Vector3 = (transform.basis * Vector3(entrada.x, 0.0, entrada.y)).normalized()
    velocity.x = direcao.x * VELOCIDADE
    velocity.z = direcao.z * VELOCIDADE
    move_and_slide()

Posicione o Jogador acima do terreno (uns 20 de Y resolve) e rode: você deve cair sobre as colinas, andar com as setas e pular com espaço. O mouse capturado gira a visão. Se o personagem atravessar o chão, volte na MeshLibrary e confira se cada bloco tem o StaticBody3D com CollisionShape3D.

Colocar e quebrar blocos com RayCast3D

Aqui está o coração de um jogo estilo Minecraft: transformar um clique numa célula da grade. O RayCast3D nos dá duas informações no momento do acerto: get_collision_point(), o ponto exato onde o raio tocou a face do bloco, e get_collision_normal(), o vetor que aponta pra fora dessa face. Se o funcionamento do raycast ainda é nebuloso pra você, o guia de raycast 3D no Godot 4 destrincha esses métodos um por um.

O detalhe que derruba todo iniciante: o ponto de colisão fica exatamente na fronteira entre duas células. A face de cima de um bloco é, ao mesmo tempo, o teto da célula dele e o chão da célula vazia acima. Converter esse ponto direto com local_to_map é jogar cara ou coroa com o arredondamento. O truque da normal resolve:

  • Pra quebrar, você quer a célula do bloco atingido. Empurre o ponto pra dentro dele: ponto - normal * 0.5. Meio metro contra a normal atravessa a face e cai no centro geométrico do lado do bloco.
  • Pra colocar, você quer a célula vazia colada na face. Empurre o ponto pra fora: ponto + normal * 0.5.

Como get_collision_point() devolve coordenada global e local_to_map espera coordenada local do GridMap, convertemos com to_local antes. Adicione ao script do Jogador:

const PEDRA: int = 2

var bloco_na_mao: int = PEDRA

@onready var raio: RayCast3D = $Camera3D/RayCast3D
@onready var grid: GridMap = $"../GridMap"

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.is_pressed():
        var clique: InputEventMouseButton = event
        if clique.button_index == MOUSE_BUTTON_LEFT:
            quebrar_bloco()
        elif clique.button_index == MOUSE_BUTTON_RIGHT:
            colocar_bloco()

func quebrar_bloco() -> void:
    if not raio.is_colliding():
        return
    var ponto: Vector3 = raio.get_collision_point()
    var normal: Vector3 = raio.get_collision_normal()
    var celula: Vector3i = grid.local_to_map(grid.to_local(ponto - normal * 0.5))
    if grid.get_cell_item(celula) != GridMap.INVALID_CELL_ITEM:
        grid.set_cell_item(celula, GridMap.INVALID_CELL_ITEM)

func colocar_bloco() -> void:
    if not raio.is_colliding():
        return
    var ponto: Vector3 = raio.get_collision_point()
    var normal: Vector3 = raio.get_collision_normal()
    var celula: Vector3i = grid.local_to_map(grid.to_local(ponto + normal * 0.5))
    if grid.get_cell_item(celula) == GridMap.INVALID_CELL_ITEM:
        grid.set_cell_item(celula, bloco_na_mao)

As checagens contra GridMap.INVALID_CELL_ITEM evitam dois bugs bobos: quebrar o ar e colocar um bloco dentro de outro. A variável bloco_na_mao já deixa o caminho aberto pra uma hotbar: troque o valor dela (0, 1 ou 2) conforme a tecla numérica pressionada e você escolhe qual bloco colocar.

Rode o jogo. Mire numa colina, clique com o esquerdo e o bloco some. Clique com o direito numa face e um bloco de pedra aparece grudado nela. Você tem um mundo de blocos gerado, destrutível e construível. É pouco perto do Minecraft, e é exatamente a mesma fundação sobre a qual o Minecraft foi construído.

Os limites do GridMap (e o próximo degrau)

Vale fechar com a mesma honestidade da abertura. O GridMap é a ferramenta certa pra este escopo, mas ele tem um teto claro:

  • Sem greedy meshing. Cada célula é desenhada como um mesh próprio, incluindo faces internas que nenhum jogador jamais verá. Num terreno 64x64 como o nosso isso é irrelevante; num mundo de milhões de blocos, é desperdício brutal.
  • Sem chunks automáticos. O Minecraft só mantém na memória os pedaços do mundo perto do jogador. Com GridMap, se você quiser um mundo maior, precisa dividir em vários GridMaps e carregar e descarregar na mão.
  • Performance ok pra mundos pequenos. Dezenas de milhares de células rodam bem. Se seu plano é uma arena de construção, um puzzle de blocos ou um protótipo, o GridMap segura tranquilo.

O passo seguinte, o que a comunidade de voxel na Godot faz quando o mundo cresce, é abandonar o GridMap e gerar a geometria por código: um ArrayMesh por chunk, construindo apenas as faces de blocos expostas ao ar. É um salto grande de complexidade (você passa a montar vértices e índices na mão) e fica fora deste tutorial de propósito. Chegue lá depois de dominar a versão com GridMap.

E se o que te move é menos construir a engine de blocos e mais criar conteúdo dentro de um mundo desses, existe um atalho divertido: criar mod de Minecraft te coloca pra programar em cima da engine de voxel mais famosa do mundo, sem reinventá-la.

De todo modo, o que você construiu aqui não é descartável. Grade de células, ruído pra terreno, raycast com truque da normal: esses três conceitos aparecem em qualquer jogo de construção, de simuladores de fábrica a city builders. O jogo estilo Minecraft é só a porta de entrada mais divertida pra eles.

Perguntas frequentes

Dá pra fazer um clone completo de Minecraft na Godot?

Dá, mas é projeto de anos, não de um tutorial. Um clone completo envolve chunks infinitos gerados sob demanda, geração de mesh por chunk com greedy meshing, iluminação por voxel, save e load de mundo e muito mais. O que um iniciante consegue construir em algumas horas é a base jogável: terreno gerado, colocar e quebrar blocos em primeira pessoa. Esses são os conceitos centrais, e o resto se constrói em cima deles.

GridMap aguenta um mundo grande estilo Minecraft?

Não. O GridMap desenha cada célula como um mesh separado (com batching interno limitado), sem greedy meshing, então mundos com centenas de milhares de blocos derrubam o desempenho. Para mundos pequenos, na casa de dezenas de milhares de células, ele funciona bem. Acima disso, o caminho da comunidade é gerar ArrayMesh por chunk, desenhando só as faces visíveis.

O que o set_cell_item faz no GridMap?

Ele preenche uma célula da grade com um item da MeshLibrary. A assinatura é set_cell_item(position: Vector3i, item: int, orientation: int = 0): a posição é a coordenada da célula na grade, o item é o índice do mesh na MeshLibrary, e passar GridMap.INVALID_CELL_ITEM (que vale -1) apaga a célula. Para ler o que tem numa célula, use get_cell_item, que devolve o índice do item ou -1 se estiver vazia.

Por que usar a normal da colisão pra colocar e quebrar blocos?

Porque o ponto de colisão do raycast fica exatamente na face do bloco, na fronteira entre duas células. Se você converter esse ponto direto com local_to_map, o arredondamento pode cair em qualquer um dos dois lados. Somando metade da normal, você empurra o ponto pra dentro da célula vazia vizinha (onde o bloco novo entra); subtraindo metade da normal, empurra pra dentro do bloco atingido (que é o que você quer quebrar).

O FastNoiseLite serve pra gerar cavernas também?

Serve. Neste tutorial usamos get_noise_2d pra gerar um mapa de altura, mas a classe também tem get_noise_3d(x, y, z). O truque clássico de caverna é: depois de gerar a coluna de blocos, remover as células onde o valor do ruído 3D passa de um limiar. É uma extensão natural do código deste tutorial.