Voltar para o Blog
Quest Log

Como Fazer um Jogo Tower Defense no Godot 4: Caminho, Ondas e Torres

Cena de jogo tower defense com torres defendendo um caminho cheio de inimigos

Tower defense no Godot 4 do zero: caminho com Path2D, ondas por Timer, torres que miram e atiram, e economia que fecha o loop. Tutorial GDScript tipado.

Como Fazer um Jogo Tower Defense no Godot 4: Caminho, Ondas e Torres

Um tower defense parece complicado de longe, mas no fundo ele é a soma de quatro peças que conversam entre si. Neste tutorial você vai montar a mecânica base de um tower defense no Godot 4 com GDScript tipado: o caminho que os inimigos seguem, o spawn de ondas, as torres que miram e atiram, e a economia que amarra tudo. A ideia não é entregar um jogo pronto e bonito, e sim deixar o esqueleto funcional na sua mão, para você depois trocar arte, balancear números e expandir.

Vou assumir que você já abre o Godot 4, cria cenas e sabe arrastar nós. Se ainda está engatinhando no engine, vale fazer o melhor curso de Godot antes ou em paralelo, porque aqui o foco é a mecânica do gênero, não o básico do editor.

Uma palavra sobre o estilo do código: tudo aqui é GDScript com tipagem estática. Cada variável, cada parâmetro e cada retorno de função tem o tipo declarado. Isso não é firula. No Godot 4, tipar o código deixa o editor te avisar de erros antes de rodar, melhora o autocomplete e ainda dá um ganho de desempenho, porque o engine não precisa adivinhar tipos em tempo de execução. Num tower defense, onde você instancia dezenas de inimigos por onda, esse cuidado paga.

A peça que fecha o loop do tower defense

Antes do código, fixe o loop mental. Você coloca uma torre, a onda avança, as torres atiram, inimigos morrem e te dão dinheiro, você compra mais torre. Se uma dessas peças falha, o jogo não engata. Por isso vamos construir na ordem em que elas se sustentam: primeiro o caminho (sem ele não há inimigo se movendo), depois o spawn (sem ele não há o que atacar), depois a torre (o coração do gênero) e por fim a economia (o que dá sentido a tudo).

1. O caminho: Path2D e PathFollow2D

No Godot 4, a forma mais direta de fazer um inimigo seguir uma rota fixa é desenhar essa rota com um Path2D e prender o inimigo num PathFollow2D. Você cria um nó Path2D na cena do nível, usa a ferramenta de curva no editor para desenhar o trajeto da entrada até a base, e dentro dele coloca um PathFollow2D. O inimigo vira filho desse PathFollow2D, ou melhor, o inimigo carrega o PathFollow2D consigo.

A mágica está em duas propriedades. O progress é a distância percorrida em pixels ao longo da curva, e o progress_ratio é um valor de 0.0 a 1.0 que representa a fração do caminho já andada. Mover o inimigo é só aumentar o progress a cada frame, escalado pelo delta para ficar independente de framerate.

Estrutura sugerida da cena do inimigo: um PathFollow2D como raiz, com um Sprite2D e um Area2D (para a torre detectar) como filhos. Mas para reaproveitar o inimigo em qualquer caminho, é mais limpo manter o inimigo como uma cena própria e instanciá-lo dentro de um PathFollow2D criado pelo spawner. Aqui vou usar a versão em que o próprio inimigo é um PathFollow2D, que é a mais enxuta para começar.

# enemy.gd
# O inimigo e um PathFollow2D: ele anda sozinho pela curva do Path2D pai.
extends PathFollow2D

# Velocidade em pixels por segundo ao longo da curva.
@export var speed: float = 120.0
# Vida do inimigo. Quando chega a zero, ele morre e da recompensa.
@export var max_health: int = 30
# Quanto de dinheiro este inimigo da ao morrer.
@export var reward: int = 10
# Quanto de vida da base ele tira se chegar ao fim.
@export var damage_to_base: int = 1

var health: int = 0

# Sinais para o resto do jogo reagir sem acoplar tudo.
signal died(reward: int)
signal reached_end(damage: int)

func _ready() -> void:
    health = max_health
    # Garante que o inimigo nao gira junto com a curva, so segue a posicao.
    rotates = false
    # Sem loop: ao chegar no fim, queremos tratar o evento, nao reiniciar.
    loop = false

func _process(delta: float) -> void:
    # Avanca ao longo da curva. delta deixa o movimento estavel em qualquer FPS.
    progress += speed * delta

    # progress_ratio vai de 0.0 (inicio) a 1.0 (fim do caminho).
    if progress_ratio >= 1.0:
        _on_reached_end()

# Chamado pela torre quando um projetil acerta.
func take_damage(amount: int) -> void:
    health -= amount
    if health <= 0:
        _die()

func _die() -> void:
    # Avisa quem estiver ouvindo (a economia) que houve recompensa.
    died.emit(reward)
    queue_free()

func _on_reached_end() -> void:
    # Tira vida da base e some da cena.
    reached_end.emit(damage_to_base)
    queue_free()

Sobre a alternativa de waypoints: se você não quer desenhar curvas e prefere controle total, pode guardar um Array[Vector2] de pontos e mover o inimigo de ponto em ponto com move_toward. Funciona, mas você perde a suavidade da curva e tem que gerenciar o índice do waypoint atual na mão. Para rota fixa, Path2D ganha em simplicidade. Quando o caminho precisar ser dinâmico, com inimigos contornando obstáculos, aí sim vale olhar pathfinding com Navigation2D no Godot.

2. Spawn de ondas com Timer

O spawner é um nó que vive no nível, conhece a curva do Path2D e instancia inimigos em sequência. Um Timer controla o intervalo entre cada inimigo da onda. Cada vez que o Timer dispara, o spawner cria um PathFollow2D novo, prende o inimigo nele e adiciona ao Path2D.

A lógica de onda é um contador: quantos inimigos faltam spawnar nesta onda. Quando zera, o spawner para o Timer e espera a próxima onda começar.

# spawner.gd
extends Node2D

# A cena do inimigo (enemy.tscn), arrastada no Inspector.
@export var enemy_scene: PackedScene
# Referencia ao Path2D do nivel, onde os inimigos vao andar.
@export var path: Path2D
# Quantos inimigos spawnar na primeira onda.
@export var base_wave_size: int = 5
# Quanto a onda cresce a cada rodada.
@export var wave_growth: int = 2

@onready var spawn_timer: Timer = $SpawnTimer

var current_wave: int = 0
var enemies_to_spawn: int = 0

func _ready() -> void:
    # Cada disparo do Timer spawna um inimigo.
    spawn_timer.timeout.connect(_on_spawn_timer_timeout)
    start_next_wave()

func start_next_wave() -> void:
    current_wave += 1
    # Onda cresce de forma previsivel: 5, 7, 9, 11...
    enemies_to_spawn = base_wave_size + (current_wave - 1) * wave_growth
    spawn_timer.start()

func _on_spawn_timer_timeout() -> void:
    if enemies_to_spawn <= 0:
        # Onda terminou de spawnar. Para ate a proxima comecar.
        spawn_timer.stop()
        return

    _spawn_one_enemy()
    enemies_to_spawn -= 1

func _spawn_one_enemy() -> void:
    # enemy.gd estende PathFollow2D, entao a propria instancia ja anda na curva.
    var enemy: PathFollow2D = enemy_scene.instantiate() as PathFollow2D
    # Conecta a economia: matar da dinheiro, chegar ao fim tira vida.
    enemy.died.connect(Economy.on_enemy_died)
    enemy.reached_end.connect(Economy.on_base_damaged)
    # O inimigo precisa ser filho do Path2D para seguir a curva dele.
    path.add_child(enemy)

Note que conectamos os sinais do inimigo direto ao singleton Economy na hora do spawn. Assim o inimigo não precisa saber nada sobre dinheiro nem sobre a base, ele só emite sinais. Se quiser empilhar lógica de ondas mais elaborada (pausas, mini bosses, anúncio na tela), vale ver um modelo dedicado de spawner de inimigos em waves.

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

3. As torres: detectar, mirar e atirar

A torre é o coração do gênero. Ela precisa de três comportamentos: saber quem está no alcance, escolher um alvo e atirar com cooldown.

Para o alcance, use um Area2D com um CollisionShape2D circular do tamanho do raio de tiro. Os inimigos têm o próprio Area2D numa layer que a torre monitora, então quando um inimigo entra ou sai do círculo, a torre recebe area_entered e area_exited. Você mantém uma lista de inimigos no alcance e a mantém limpa.

Para escolher o alvo, o critério clássico de tower defense é mirar no inimigo mais à frente no caminho, ou seja, o de maior progress, porque é o que está mais perto de furar a defesa. Como o inimigo é um PathFollow2D, comparar progress é direto.

Para atirar, um Timer em modo loop serve de cooldown: a cada timeout, se há alvo, dispara.

# tower.gd
extends Node2D

# Dano por tiro.
@export var damage: int = 10
# Raio de alcance em pixels (combine com o CollisionShape2D do Range).
@export var attack_range: float = 200.0
# Tiros por segundo viram o wait_time do Timer no _ready.
@export var fire_rate: float = 1.5

@onready var fire_timer: Timer = $FireTimer
@onready var range_area: Area2D = $Range

# Inimigos atualmente dentro do alcance.
var enemies_in_range: Array[PathFollow2D] = []
var current_target: PathFollow2D = null

func _ready() -> void:
    # Converte tiros por segundo em intervalo entre tiros.
    fire_timer.wait_time = 1.0 / fire_rate
    fire_timer.timeout.connect(_on_fire_timer_timeout)
    fire_timer.start()

    range_area.area_entered.connect(_on_range_area_entered)
    range_area.area_exited.connect(_on_range_area_exited)

func _on_range_area_entered(area: Area2D) -> void:
    # O Area2D do inimigo deve ter o PathFollow2D como pai.
    var enemy: PathFollow2D = area.get_parent() as PathFollow2D
    if enemy != null and not enemies_in_range.has(enemy):
        enemies_in_range.append(enemy)

func _on_range_area_exited(area: Area2D) -> void:
    var enemy: PathFollow2D = area.get_parent() as PathFollow2D
    if enemy != null:
        enemies_in_range.erase(enemy)

func _on_fire_timer_timeout() -> void:
    _clean_dead_enemies()
    current_target = _pick_target()
    if current_target != null:
        _shoot(current_target)

# Remove da lista inimigos que ja sairam da cena (foram mortos ou chegaram ao fim).
func _clean_dead_enemies() -> void:
    var alive: Array[PathFollow2D] = []
    for enemy in enemies_in_range:
        if is_instance_valid(enemy):
            alive.append(enemy)
    enemies_in_range = alive

# Alvo = inimigo mais a frente no caminho (maior progress).
func _pick_target() -> PathFollow2D:
    var best: PathFollow2D = null
    var best_progress: float = -1.0
    for enemy in enemies_in_range:
        if enemy.progress > best_progress:
            best_progress = enemy.progress
            best = enemy
    return best

func _shoot(target: PathFollow2D) -> void:
    # Versao direta (hitscan): aplica dano na hora.
    # Para projeteis voadores, instancie uma cena de bala mirando o target.
    if target.has_method("take_damage"):
        target.take_damage(damage)

Repare que _shoot aqui é hitscan: o dano é instantâneo. É o jeito mais simples de validar o loop. Quando quiser projéteis visíveis, troque o corpo de _shoot por instanciar uma cena de bala que voa até a posição do alvo e chama take_damage ao colidir. A escolha de alvo e o cooldown continuam iguais.

Vale comentar a função _clean_dead_enemies. Inimigos somem da cena por dois motivos: morreram ou chegaram ao fim do caminho. Quando isso acontece, a referência guardada na lista da torre vira inválida, e tentar ler progress dela quebraria o jogo. Por isso, antes de mirar, a torre filtra a lista com is_instance_valid. Esse tipo de defesa é o que separa um protótipo que trava do primeiro minuto de um que aguenta dez ondas seguidas sem reclamar.

Uma observação sobre tipagem: estou tratando o inimigo como PathFollow2D porque é o que ele estende. Se você criar uma classe nomeada para o inimigo com class_name Enemy extends PathFollow2D, pode trocar todos os PathFollow2D por Enemy nas tipagens e ganhar autocomplete dos seus métodos. É a evolução natural quando o projeto cresce.

4. Economia: dinheiro, custo, recompensa e vida da base

A economia precisa ser acessível de qualquer lugar: o spawner conecta inimigos a ela, a torre custa dinheiro, o inimigo morto recompensa, o inimigo que vaza machuca a base. O jeito limpo no Godot 4 é um autoload (singleton). Crie o script abaixo e registre em Project Settings, Autoload, com o nome Economy.

# economy.gd  (registrar como autoload com o nome "Economy")
extends Node

# Dinheiro inicial do jogador.
@export var starting_money: int = 100
# Vida inicial da base.
@export var starting_base_health: int = 20

var money: int = 0
var base_health: int = 0

# Sinais para a UI atualizar numeros sem acoplar a logica.
signal money_changed(new_value: int)
signal base_health_changed(new_value: int)
signal game_over()

func _ready() -> void:
    money = starting_money
    base_health = starting_base_health

# Pode pagar por algo deste custo?
func can_afford(cost: int) -> bool:
    return money >= cost

# Tenta debitar. Retorna true se conseguiu (usado antes de colocar torre).
func try_spend(cost: int) -> bool:
    if not can_afford(cost):
        return false
    money -= cost
    money_changed.emit(money)
    return true

# Chamado quando um inimigo morre (conectado no spawner).
func on_enemy_died(reward: int) -> void:
    money += reward
    money_changed.emit(money)

# Chamado quando um inimigo chega ao fim do caminho.
func on_base_damaged(damage: int) -> void:
    base_health -= damage
    base_health_changed.emit(base_health)
    if base_health <= 0:
        base_health = 0
        game_over.emit()

Agora o gancho da compra de torre. No script que controla cliques no nível (ou no botão de comprar torre), você checa o custo antes de instanciar. Sem dinheiro, nada acontece. Com dinheiro, debita e coloca a torre na posição.

# build_manager.gd  (no no do nivel que escuta cliques)
extends Node2D

# Cena da torre (tower.tscn).
@export var tower_scene: PackedScene
# Custo de uma torre.
@export var tower_cost: int = 50

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
        _try_build_tower(get_global_mouse_position())

func _try_build_tower(position: Vector2) -> void:
    # So constroi se houver dinheiro. try_spend ja debita em caso positivo.
    if not Economy.try_spend(tower_cost):
        return

    var tower: Node2D = tower_scene.instantiate() as Node2D
    tower.global_position = position
    add_child(tower)

Num jogo de verdade você ainda valida se o ponto é válido (não pode construir em cima do caminho, nem sobrepor torres). A checagem de posição é onde entram grids: se você for para um mapa em grade, vale estudar A* em grid no Godot para alinhar construção e movimentação na mesma malha.

Amarrando o loop completo

Com as quatro peças no lugar, o ciclo se fecha sozinho. O spawner dispara a primeira onda no _ready, os inimigos andam pela curva do Path2D. Você clica para gastar dinheiro e colocar torres. As torres detectam quem entra no Area2D, miram o mais avançado e atiram no cooldown do Timer. Inimigo que morre emite died, o Economy soma a recompensa, e você junta o suficiente para a próxima torre. Inimigo que vaza emite reached_end, a base perde vida, e quando ela zera o sinal game_over dispara.

Para virar um jogo completo, os próximos passos costumam ser: chamar start_next_wave no spawner quando a onda atual acaba e todos os inimigos saem da tela, ligar a UI aos sinais money_changed e base_health_changed, e adicionar tipos de torre e de inimigo variando os @export. A base já está tipada e desacoplada por sinais, então cada adição entra sem desmontar o que existe.

O segredo de um tower defense que funciona não está em nenhuma peça isolada e sim na conversa entre elas. Construa na ordem, teste cada peça antes de seguir, e mantenha a tipagem para o compilador do Godot pegar seus erros antes do jogador pegar. A partir daqui é balanceamento e arte, que é onde o jogo ganha personalidade.

Perguntas frequentes

Preciso de plugins ou assets pagos para fazer um tower defense no Godot 4?

Nao. Tudo neste tutorial usa nos nativos do Godot 4 (Path2D, PathFollow2D, Area2D, Timer) e GDScript. Voce so vai precisar de sprites simples para inimigos e torres, que podem ser placeholders coloridos no inicio.

Devo usar Path2D ou Navigation2D para mover os inimigos?

Para um tower defense de rota fixa, Path2D com PathFollow2D e mais simples e previsivel, porque o caminho ja esta desenhado. Use Navigation2D ou A* quando o inimigo precisar contornar torres ou recalcular a rota em tempo real.

Como faco a torre escolher qual inimigo atacar?

Mantenha uma lista dos inimigos dentro do alcance (via Area2D) e escolha um criterio. O mais comum no genero e atacar o inimigo mais a frente no caminho, comparando o progress de cada PathFollow2D.

O Timer e o suficiente para o cooldown de tiro e o spawn de ondas?

Sim. Um Timer configurado com wait_time controla o intervalo entre tiros da torre, e outro Timer no spawner controla o intervalo entre inimigos de uma onda. Para projetos grandes voce pode trocar por acumuladores de delta, mas o Timer resolve a maioria dos casos.

Como ligo matar inimigo a ganhar dinheiro e comprar mais torres?

Centralize dinheiro e vida da base em um script autoload (singleton). Quando um inimigo morre, ele chama uma funcao que soma a recompensa. Antes de instanciar uma torre, voce checa e debita o custo nesse mesmo singleton.

O que acontece quando o inimigo chega ao fim do caminho?

O PathFollow2D atinge progress_ratio igual a 1.0. Nesse momento voce tira vida da base, remove o inimigo da cena e checa se a base chegou a zero para encerrar a partida.