Voltar para o Blog
Quest Log

Jogo Asteroids na Godot: Inércia, Rotação e Tela Infinita

Nave triangular aplicando empuxo e atirando em asteroides que se partem em pedaços menores, no estilo do arcade clássico Asteroids feito na Godot

Aprenda como fazer Asteroids na Godot 4: nave com inércia e rotação, wrap de tela, tiros com Area2D e asteroides que se dividem, tudo em GDScript tipado.

Ferramenta:Godot

Asteroids (1979) parece simples até você tentar cloná-lo: a nave não anda para onde você aperta, ela gira e empurra a si mesma, deslizando pelo espaço com inércia de verdade. Neste tutorial de como fazer Asteroids na Godot 4 você monta esse movimento do zero, em GDScript tipado, junto com as outras três mecânicas que definem o jogo: a tela que dá a volta (wrap-around), os asteroides que se quebram em pedaços menores e as ondas que ficam mais cheias a cada rodada.

Isso o coloca em uma categoria diferente de um shmup comum. Em um jogo de nave tradicional, que destrinchamos no tutorial de como fazer um jogo de nave estilo shmup na Godot, a nave responde na hora: apertou para a direita, andou para a direita, soltou, parou. No Asteroids, o input controla aceleração, não posição. Essa única decisão de design muda tudo: mirar exige planejamento, frear exige manobra e cada asteroide grande destruído vira dois problemas menores voando em direções novas.

É também um projeto excelente para quem está começando: escopo fechado, zero necessidade de arte elaborada e três ou quatro scripts curtos. Se ainda está decidindo o que construir primeiro, ele aparece com destaque na nossa lista de ideias de jogos simples para iniciantes. E se você ainda está no mundo dos blocos e quer um desafio que impressiona antes de migrar para uma engine, dá até para fazer um jogo em 3D no Scratch.

A Cena da Nave

Crie uma cena Ship com esta estrutura:

  • Ship (CharacterBody2D), o nó raiz que carrega o script
  • Sprite2D ou Polygon2D, o visual da nave
  • CollisionShape2D com um CircleShape2D pequeno

Um detalhe que evita horas de confusão: desenhe ou rotacione o visual da nave com o nariz apontando para a direita. Na Godot, rotação 0 aponta para +X, e vamos usar Vector2.from_angle(rotation) como direção "para frente". Se o seu sprite aponta para cima, rotacione o nó filho em 90 graus (só o filho, nunca a raiz).

Escolhemos CharacterBody2D em vez de RigidBody2D de propósito. O RigidBody2D daria a inércia de graça, mas teleportar um corpo controlado pelo motor de física (que é o que o wrap de tela faz) exige _integrate_forces e cuidados que não valem a pena aqui. Com CharacterBody2D e um velocity manual, cada linha da física é sua e o comportamento é totalmente previsível.

Como Fazer Asteroids na Godot: O Movimento com Inércia

Aqui está o coração do jogo. O script inteiro do movimento cabe em vinte linhas:

extends CharacterBody2D

@export var thrust_power: float = 220.0
@export var turn_speed: float = 3.5
@export var max_speed: float = 420.0
@export var drag: float = 0.15

func _physics_process(delta: float) -> void:
    var turn: float = Input.get_axis("ui_left", "ui_right")
    rotation += turn * turn_speed * delta

    if Input.is_action_pressed("ui_up"):
        velocity += Vector2.from_angle(rotation) * thrust_power * delta

    velocity = velocity.limit_length(max_speed)
    velocity -= velocity * drag * delta
    move_and_slide()
    _wrap_around_screen()

Linha a linha, o que importa:

  • Input.get_axis("ui_left", "ui_right") devolve um float entre -1 e 1. Multiplicado por turn_speed (em radianos por segundo) e por delta, ele gira a nave. Repare que girar não muda a trajetória: a nave pode estar indo para a esquerda com o nariz apontando para a direita.
  • Vector2.from_angle(rotation) converte o ângulo atual da nave em um vetor unitário de direção. É o mesmo que transform.x para um nó sem escala. Esse é o vetor do empuxo.
  • velocity += direção * thrust_power * delta é a linha que cria a inércia. Não estamos definindo a velocidade, estamos somando aceleração à velocidade que já existia. Solte o botão e o velocity acumulado continua lá: a nave desliza.
  • limit_length(max_speed) impõe um teto. Sem ele, segurar o empuxo por dez segundos deixa a nave incontrolável.
  • velocity -= velocity * drag * delta é um atrito artificial bem leve. O arcade original praticamente não tinha; deixe drag em 0.0 se quiser a experiência raiz, ou suba para 0.5 se os playtesters reclamarem do controle.

Rode a cena e brinque trinta segundos. A sensação de "empurrar um carrinho no gelo" já está inteira nessas linhas.

Wrap de Tela: Saiu Pela Direita, Voltou Pela Esquerda

No Asteroids não existe parede. A tela é um toro: sair por uma borda significa reaparecer na oposta, mantendo velocidade e rotação. A implementação na Godot é uma função de quatro linhas, graças a fposmod:

func _wrap_around_screen() -> void:
    var screen: Vector2 = get_viewport_rect().size
    position.x = fposmod(position.x, screen.x)
    position.y = fposmod(position.y, screen.y)

fposmod(a, b) é o módulo de ponto flutuante que sempre devolve resultado positivo. É isso que dispensa qualquer if: numa tela de 1152 pixels de largura, uma nave em x = -10.0 vira x = 1142.0, e uma em x = 1160.0 vira x = 8.0. O operador % não serve aqui porque só funciona com inteiros em GDScript, e uma subtração manual exigiria quatro comparações.

Chame _wrap_around_screen() no final do _physics_process da nave, e depois reaproveite a mesma lógica nos asteroides. Um refinamento clássico para depois: dar uma margem do tamanho do sprite (fposmod(position.x + margem, screen.x + margem * 2.0) - margem) para o objeto só teleportar quando sumir por completo, em vez de cortar no meio.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

O Tiro: Uma Area2D Que Herda a Direção da Nave

O tiro do Asteroids é um projétil reto e de vida curta. Na nave, adicione o disparo com cooldown:

@export var bullet_scene: PackedScene
@export var fire_rate: float = 0.25
var _cooldown: float = 0.0

func _process(delta: float) -> void:
    _cooldown -= delta
    if Input.is_action_pressed("ui_accept") and _cooldown <= 0.0:
        _cooldown = fire_rate
        var bullet: Area2D = bullet_scene.instantiate() as Area2D
        bullet.global_position = global_position
        bullet.rotation = rotation
        get_parent().add_child(bullet)

O detalhe importante: o tiro nasce com a rotação da nave, não com a direção do movimento dela. Você pode estar deslizando para trás e atirando para frente, e é exatamente esse desacoplamento que torna o combate interessante.

A cena Bullet é uma Area2D com Sprite2D e CollisionShape2D:

extends Area2D

@export var speed: float = 600.0
@export var lifetime: float = 0.9

func _ready() -> void:
    area_entered.connect(_on_area_entered)
    get_tree().create_timer(lifetime).timeout.connect(queue_free)

func _process(delta: float) -> void:
    position += Vector2.from_angle(rotation) * speed * delta

func _on_area_entered(area: Area2D) -> void:
    if area.has_method("break_apart"):
        area.break_apart()
    queue_free()

O lifetime de 0.9 segundos reproduz o arcade: o tiro morre sozinho antes de cruzar a tela inteira, o que impede o jogador de encher o espaço de balas. Se quiser fidelidade total, aplique o mesmo wrap de tela ao tiro, que no original também dava a volta. Para aprofundar em pooling, colisão por camadas e variações de projétil, o post sobre como atirar projéteis em 2D na Godot cobre o sistema em detalhe.

Configure as camadas de física para evitar acidentes: nave na layer 1, asteroides na layer 2, tiros na layer 3. O tiro só monitora a layer 2, o asteroide só monitora as layers 1 e 3. Assim tiro não colide com nave, nem asteroide com asteroide.

Asteroides Que Se Quebram em Pedaços

A mecânica que dá nome ao jogo: destruir um asteroide grande gera dois médios, destruir um médio gera dois pequenos, e só o pequeno morre de vez. Modelamos isso com um campo size (3, 2 ou 1) e um sinal, deixando a criação dos pedaços para o gerenciador da fase.

A cena Asteroid é uma Area2D com visual e colisão:

extends Area2D

signal exploded(size: int, at: Vector2)

var size: int = 3
var drift: Vector2 = Vector2.ZERO
var spin: float = 0.0

func _ready() -> void:
    add_to_group("asteroids")
    spin = randf_range(-1.5, 1.5)

func _process(delta: float) -> void:
    position += drift * delta
    rotation += spin * delta
    var screen: Vector2 = get_viewport_rect().size
    position.x = fposmod(position.x, screen.x)
    position.y = fposmod(position.y, screen.y)

func break_apart() -> void:
    exploded.emit(size, global_position)
    queue_free()

Repare que o asteroide não sabe se dividir. Ele só anuncia "explodi, tinha tamanho X, estava aqui" e some. Quem decide o que acontece depois é o script principal, e essa separação evita o problema clássico de uma cena precisar instanciar a si mesma.

Game Loop: Spawn, Divisão, Ondas e Pontuação

O nó raiz da fase (Main, um Node2D com a nave como filha) amarra tudo:

extends Node2D

@export var asteroid_scene: PackedScene
var score: int = 0
var wave: int = 0

func _ready() -> void:
    _start_wave()

func _start_wave() -> void:
    wave += 1
    for i: int in 2 + wave:
        _spawn(3, _edge_position())

func _spawn(size: int, at: Vector2) -> void:
    var rock: Area2D = asteroid_scene.instantiate() as Area2D
    rock.size = size
    rock.position = at
    rock.scale = Vector2.ONE * 0.4 * float(size)
    rock.drift = Vector2.from_angle(randf() * TAU) * (200.0 - 45.0 * float(size))
    rock.exploded.connect(_on_asteroid_exploded)
    add_child(rock)

func _on_asteroid_exploded(size: int, at: Vector2) -> void:
    score += (4 - size) * 100
    if size > 1:
        for i: int in 2:
            _spawn(size - 1, at)
    if get_tree().get_nodes_in_group("asteroids").size() <= 1:
        _start_wave.call_deferred()

func _edge_position() -> Vector2:
    var screen: Vector2 = get_viewport_rect().size
    if randf() < 0.5:
        return Vector2(0.0, randf() * screen.y)
    return Vector2(randf() * screen.x, 0.0)

As decisões de design escondidas nesse script:

  • Velocidade inversa ao tamanho. 200.0 - 45.0 * size faz o asteroide grande vagar a 65 pixels por segundo e o pequeno voar a 155. É isso que transforma cada divisão em um aumento real de perigo, não só de quantidade.
  • Pontuação inversa também. (4 - size) * 100 paga 100 pelo grande, 200 pelo médio, 300 pelo pequeno. O alvo mais difícil vale mais, como no arcade.
  • Spawn nas bordas. _edge_position() sorteia um ponto na borda esquerda ou superior (que, com o wrap, cobre as quatro), garantindo que nenhuma onda nasce em cima da nave.
  • Onda nova via sinal, sem polling. Quando o último asteroide anuncia a explosão, ele ainda está na árvore, por isso o teste é <= 1 e não is_empty(). O call_deferred adia o spawn para o fim do frame, quando o queue_free já resolveu.

A morte da nave segue o mesmo padrão: conecte o area_entered de uma Area2D filha da nave (ou use move_and_slide com colisão nas layers certas), tire uma vida, reposicione no centro e dê dois segundos de invencibilidade piscando o sprite.

Polish e Próximos Passos

Com o loop fechado, uma tarde de polish transforma o protótipo em jogo:

  1. Chama de empuxo. Um GPUParticles2D filho da nave, ligado só enquanto ui_up está pressionado. É feedback visual da mecânica central, o upgrade de maior retorno.
  2. Explosões e som. Partículas de estilhaço no break_apart e três sons: tiro, explosão e o bump grave do empuxo. Áudio carrega metade da sensação de arcade.
  3. Screen shake sutil quando um asteroide grande explode perto da nave.
  4. HUD e high score. Um CanvasLayer com Label para pontos e vidas; salve o recorde com FileAccess em user://.
  5. Disco voador. O inimigo que atravessa a tela atirando é o próximo desafio de código natural: reutiliza o wrap, o tiro e o sinal de explosão que você já escreveu.

O padrão que fica deste projeto vale para tudo o que vier depois: física manual quando o motor atrapalha, sinais para desacoplar quem morre de quem gerencia, e fposmod no bolso para qualquer mundo que dá a volta. Nave na tela, asteroide partido ao meio: agora é ajustar números até o controle ficar com a sua cara.

Perguntas frequentes

Por que a nave do Asteroids continua deslizando depois que eu solto o botão?

Porque o empuxo altera a velocidade, não a posição. A cada frame você soma um vetor de aceleração ao velocity, e quando solta o botão o velocity acumulado continua lá, movendo a nave. Isso é inércia simulada: para parar, o jogador precisa girar a nave e aplicar empuxo na direção contrária, exatamente como no arcade original.

Devo usar CharacterBody2D ou RigidBody2D para a nave do Asteroids?

Para fins didáticos, CharacterBody2D com velocity manual. Você controla cada linha da física, o wrap de tela funciona com uma simples troca de position e o comportamento é 100% previsível. RigidBody2D dá inércia de graça, mas teleportar um corpo físico de um lado da tela para o outro exige _integrate_forces e cuidados extras que só atrapalham o aprendizado.

Como fazer o wrap de tela na Godot, com a nave saindo de um lado e voltando do outro?

Use fposmod(position.x, largura_da_tela) e fposmod(position.y, altura_da_tela) a cada frame. A função fposmod devolve sempre um resto positivo, então uma posição de -10 numa tela de 1152 pixels vira 1142 automaticamente, sem nenhum if. Pegue o tamanho da tela com get_viewport_rect().size.

Quanto tempo leva para fazer um jogo estilo Asteroids na Godot?

Com este tutorial, um fim de semana para a versão jogável: nave, tiro, asteroides que se dividem e pontuação. Nave voando com inércia sai em menos de uma hora. O que consome mais tempo é o polish: partículas na explosão, sons, telas de início e de game over, invencibilidade ao renascer. Reserve uma segunda semana se quiser algo apresentável.

Preciso de arte própria para fazer um Asteroids?

Não. O jogo original usava vetores brancos sobre fundo preto, então um Polygon2D triangular para a nave e polígonos irregulares para os asteroides já entregam o visual clássico. Se preferir sprites, o pacote gratuito Space Shooter Redux, do Kenney, tem naves, meteoros em vários tamanhos e tiros prontos, com licença livre até para uso comercial.