Voltar para o Blog
Quest Log

Objetos destrutiveis: quebrar caixas no Godot 4

Uma caixa de madeira rachando e se quebrando em pedacos com particulas no Godot 4

Aprenda a criar um objeto destrutivel godot com vida, dano por estagios, particulas, som e loot. Tutorial pratico de GDScript para quebrar caixas no Godot 4.

Ferramenta:Godot

Quebrar uma caixa de madeira e jogar os cacos para os lados continua sendo uma das coisas mais satisfatorias de qualquer jogo 2D. Montar um objeto destrutivel godot que funciona de verdade nao depende de magica: voce precisa de uma variavel de vida, uma funcao de dano, um pouco de troca de sprite e um momento bem cuidado para liberar o no da memoria sem cortar a animacao ou o som no meio. Neste tutorial a gente parte de uma caixa simples e vai adicionando camadas: estagios de rachadura, particulas de poeira, som de impacto, drop de loot e cacos fisicos com RigidBody2D. Tudo em Godot 4 e com codigo que voce pode colar direto no editor.

A base de qualquer objeto destrutivel godot e a mesma: um corpo com colisao, um Sprite2D para a aparencia e um script que controla a vida. Comece com uma cena bem enxuta. Use um StaticBody2D como raiz se a caixa fica parada no cenario, ou um RigidBody2D se ela ja precisa cair e ser empurrada. Para o exemplo vou usar StaticBody2D, que e o caso mais comum para caixas de obstaculo.

Estrutura da cena:

  • Caixa (StaticBody2D) com o script
  • Sprite2D (a textura visual)
  • CollisionShape2D (a colisao)
  • GpuParticles2D (a poeira da quebra)
  • AudioStreamPlayer2D (o som de impacto)

Vida e dano com estagios de rachadura

O coracao do sistema e a funcao receber_dano. Ela desconta a vida, atualiza a aparencia e, quando a vida chega a zero, dispara a sequencia de destruicao. Para os estagios de rachadura, a ideia mais simples e ter uma lista de texturas e escolher a certa de acordo com a porcentagem de dano que a caixa ja sofreu.

extends StaticBody2D

@export var vida_max: int = 3
@export var sprites_dano: Array[Texture2D] = []
@export var loot_cena: PackedScene
@export var caco_cena: PackedScene
@export var quantidade_cacos: int = 5

var vida: int = 0

@onready var sprite: Sprite2D = $Sprite2D
@onready var colisao: CollisionShape2D = $CollisionShape2D
@onready var particulas: GpuParticles2D = $GpuParticles2D
@onready var som: AudioStreamPlayer2D = $AudioStreamPlayer2D

func _ready() -> void:
    vida = vida_max
    _atualizar_visual()

func receber_dano(qtd: int) -> void:
    if vida <= 0:
        return
    vida = max(vida - qtd, 0)
    _atualizar_visual()
    if vida == 0:
        _destruir()

func _atualizar_visual() -> void:
    if sprites_dano.is_empty():
        return
    # Quanto mais dano, maior o indice da rachadura.
    var dano_sofrido := vida_max - vida
    var indice := int(float(dano_sofrido) / vida_max * sprites_dano.size())
    indice = clamp(indice, 0, sprites_dano.size() - 1)
    sprite.texture = sprites_dano[indice]

Repare em dois cuidados. O primeiro if vida <= 0 evita que um segundo golpe no mesmo frame chame a destruicao duas vezes. O segundo e o calculo do indice: ele converte o dano sofrido em uma fatia da lista de sprites, entao a mesma logica serve para 3 ou 8 estagios de rachadura sem mexer no codigo.

Para chamar o dano, qualquer coisa que colida com a caixa pode usar receber_dano. Um exemplo simples vindo de uma area de ataque do jogador:

func _on_area_ataque_body_entered(corpo: Node2D) -> void:
    if corpo.has_method("receber_dano"):
        corpo.receber_dano(1)

Usar has_method em vez de checar o tipo da classe deixa o sistema generico. Qualquer barril, vaso ou parede fragil que tenha receber_dano reage ao mesmo ataque, e voce nao precisa de heranca complicada.

A sequencia de destruicao sem cortar nada

Aqui mora o erro mais comum. As pessoas chamam queue_free() na mesma linha em que emitem as particulas e tocam o som. O resultado: o no some no mesmo frame, as particulas nunca aparecem e o audio e cortado. A solucao e esconder a parte visivel da caixa, deixar particulas e som rodarem, e so depois liberar o no.

func _destruir() -> void:
    # Esconde a caixa, mas mantem o no vivo para particulas e som.
    sprite.visible = false
    colisao.set_deferred("disabled", true)

    # Dispara a poeira.
    particulas.one_shot = true
    particulas.emitting = true

    # Som de impacto.
    if som.stream:
        som.play()

    # Solta o loot e os cacos enquanto a poeira sobe.
    _soltar_loot()
    _soltar_cacos()

    # Espera as particulas terminarem antes de liberar.
    await get_tree().create_timer(particulas.lifetime).timeout
    queue_free()

Tres detalhes importam. Use set_deferred("disabled", true) na colisao porque desabilitar uma forma de colisao durante o processamento de fisica causa erro no Godot 4; o deferred adia a mudanca para um momento seguro. O one_shot = true faz as particulas emitirem um unico jato em vez de um loop continuo. E o await em cima de particulas.lifetime segura a destruicao pelo tempo exato que uma particula vive, garantindo que nenhuma poeira corte no ar.

Se voce prefere nao depender de await, da para fazer a mesma coisa com um Timer. Conecte o sinal timeout a uma funcao que chama queue_free:

func _destruir_com_timer() -> void:
    sprite.visible = false
    colisao.set_deferred("disabled", true)
    particulas.one_shot = true
    particulas.emitting = true
    if som.stream:
        som.play()

    var timer := Timer.new()
    timer.one_shot = true
    timer.wait_time = particulas.lifetime
    add_child(timer)
    timer.timeout.connect(queue_free)
    timer.start()

Os dois caminhos chegam no mesmo lugar. O await deixa o codigo mais curto e linear; o Timer e util quando voce ja tem um Timer na cena ou quer ajustar o tempo pelo inspetor. Se preferir CpuParticles2D, a API e quase identica: troque o tipo do no e o lifetime continua valendo.

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

Soltando loot e cacos fisicos

Com a destruicao no lugar, falta a parte que da peso ao momento: o que sobra quando a caixa quebra. O loot e o caso mais simples. Voce instancia uma cena (uma moeda, uma pocao) na posicao da caixa e a adiciona ao mundo, nao a caixa, porque a caixa esta prestes a sumir.

func _soltar_loot() -> void:
    if loot_cena == null:
        return
    var item := loot_cena.instantiate()
    get_parent().add_child(item)
    item.global_position = global_position

Adicionar o item em get_parent() em vez de add_child(item) direto na caixa e fundamental. Se voce filiar o loot a caixa, o queue_free() da caixa leva o item junto e ele desaparece antes de o jogador pegar.

Os cacos sao o toque que transforma uma quebra simples em algo memoravel. Cada caco e um RigidBody2D pequeno com um pedaco da textura. Voce instancia varios, espalha em angulos diferentes e aplica um impulso para eles voarem.

func _soltar_cacos() -> void:
    if caco_cena == null:
        return
    for i in quantidade_cacos:
        var caco: RigidBody2D = caco_cena.instantiate()
        get_parent().add_child(caco)
        caco.global_position = global_position

        # Direcao aleatoria com forca variavel.
        var angulo := randf_range(0.0, TAU)
        var forca := randf_range(120.0, 260.0)
        var direcao := Vector2(cos(angulo), sin(angulo))
        caco.apply_central_impulse(direcao * forca)

        # Um giro para parecer natural.
        caco.angular_velocity = randf_range(-8.0, 8.0)

A cena do caco e bem leve: um RigidBody2D com um Sprite2D pequeno e um CollisionShape2D. Vale dar aos cacos um tempo de vida proprio para nao acumular centenas deles na cena. Um script curto no caco resolve:

extends RigidBody2D

@export var vida_segundos: float = 1.5

func _ready() -> void:
    var timer := get_tree().create_timer(vida_segundos)
    timer.timeout.connect(queue_free)

Com isso cada caco vive um segundo e meio, cai com a fisica e some sozinho. Se quiser um efeito mais limpo, ative uma transicao de transparencia no caco antes do queue_free, mas mesmo sem isso o resultado ja fica bom.

Ajustando o sentimento da quebra

Sistema funcionando e so metade do trabalho. O que diferencia uma caixa qualquer de uma caixa gostosa de quebrar e o ajuste fino. Comece pelas particulas: um lifetime curto, entre 0.4 e 0.7 segundos, com bastante variacao na velocidade inicial, da aquela explosao rapida de poeira. Evite lifetime longo, porque ele segura a destruicao por tempo demais e o jogo parece travado.

No som, varie levemente o pitch_scale a cada quebra para que dez caixas seguidas nao soem identicas:

func _tocar_som_variado() -> void:
    if som.stream == null:
        return
    som.pitch_scale = randf_range(0.9, 1.1)
    som.play()

Para o impacto visual, considere um pequeno tremor de tela quando a caixa quebra perto do jogador, e nunca subestime o valor de um leve atraso entre o golpe e a quebra. Esse atraso de poucos frames da peso ao acerto. Se a caixa some no mesmo instante do toque, o cerebro nao registra o impacto.

Vale lembrar que a mesma estrutura serve para muito alem de caixas. Vasos, barris explosivos, paredes frageis e cristais usam exatamente esse esqueleto: vida, dano, estagios visuais e uma sequencia de destruicao. O que muda sao as texturas, o numero de cacos e o tipo de loot. Se a sua caixa tambem precisa ser movida pelo jogador antes de quebrar, da uma olhada em como empurrar caixas e blocos no Godot, porque a logica de fisica combina muito bem com a destruicao.

E se voce quer mergulhar fundo no lado visual, as particulas merecem um estudo dedicado. O sistema de poeira que usamos aqui e bem basico, mas da para ir muito alem com fluxos de emissao, gradientes de cor e curvas de escala. Veja o guia de particulas com GPUParticles para transformar aquela poeira simples em algo com fumaca, faiscas e fragmentos coloridos.

Tremor de tela na hora da quebra

Citei o tremor de tela como tempero, mas ele merece código, porque é o detalhe que mais vende o impacto por tão pouco esforço. A ideia é empurrar a câmera pra posições aleatórias por alguns décimos de segundo e voltar ela ao lugar. Um script curto num Camera2D resolve, e a caixa só precisa avisar que quebrou.

extends Camera2D

var tremor_restante: float = 0.0
var tremor_forca: float = 0.0
var posicao_base: Vector2 = Vector2.ZERO

func _ready() -> void:
    posicao_base = position

func tremer(forca: float, duracao: float) -> void:
    tremor_forca = forca
    tremor_restante = duracao

func _process(delta: float) -> void:
    if tremor_restante <= 0.0:
        offset = Vector2.ZERO
        return
    tremor_restante -= delta
    offset = Vector2(
        randf_range(-tremor_forca, tremor_forca),
        randf_range(-tremor_forca, tremor_forca),
    )

O segredo de não virar bagunça está em dois pontos. Primeiro, mexo no offset da câmera, não na position: assim o tremor é independente de a câmera estar seguindo o player, e some sozinho quando tremor_restante zera, sem eu ter que lembrar a posição original a cada frame. Segundo, o tremor decai com o delta, então ele sempre termina, nunca fica preso ligado.

Na caixa, dentro do _destruir, basta chamar a câmera. Use um grupo pra não acoplar a caixa a um caminho de nó fixo:

func _tremer_camera() -> void:
    var cameras: Array = get_tree().get_nodes_in_group("camera_tremor")
    if cameras.is_empty():
        return
    var camera: Camera2D = cameras[0]
    camera.tremer(4.0, 0.2)

Adicione a câmera ao grupo camera_tremor no editor e chame _tremer_camera() junto das partículas. Uma força de 4 pixels por 0.2 segundo é suficiente pra uma caixa; guarde tremores fortes pra explosões de verdade, senão o jogador enjoa. Um erro comum é tremer com força alta em todo acerto: o efeito perde o sentido e vira ruído visual. Reserve o peso pro momento que importa, que aqui é a quebra, não cada golpe.

Erros comuns que estragam a quebra

Quase todo bug de objeto destrutivel cai em um destes cinco. Confira antes de culpar a engine.

Chamar queue_free() na mesma linha das particulas e do som. O no some no mesmo frame e o efeito nunca aparece. Esconda o visual, deixe particulas e som rodarem, libere por ultimo.

Desabilitar a colisao direto durante a fisica. Fazer colisao.disabled = true dentro do processamento de fisica gera erro no Godot 4. Use sempre colisao.set_deferred("disabled", true), que adia a mudanca para um momento seguro.

Filiar o loot a caixa. Se voce der add_child(item) na propria caixa, o queue_free() dela leva o loot junto e o item some antes de o jogador pegar. Adicione em get_parent().

Deixar os cacos acumularem pra sempre. Sem um tempo de vida proprio, cada caixa quebrada deixa dezenas de RigidBody2D na cena e o jogo engasga. Dê a cada caco um timer curto que chama queue_free.

Nao proteger contra golpe duplo. Dois acertos no mesmo frame podem chamar _destruir() duas vezes. O if vida <= 0: return no topo de receber_dano resolve.

Checklist rapido antes de considerar a caixa pronta: vida e dano funcionando, estagios de rachadura trocando com o dano, particulas em one_shot, som tocando sem corte, loot caindo no pai certo, cacos com tempo de vida e nenhum erro de colisao no console. Para ir alem da caixa e conhecer o resto do ferramental 2D da engine, as ferramentas e guias de Godot do CursoGame.Dev reunem o que voce precisa.

Juntando tudo

O fluxo completo fica assim. A caixa nasce com vida cheia e o sprite limpo. Cada receber_dano desconta a vida e troca a textura para um estagio mais rachado. Quando a vida zera, a caixa esconde o sprite, desativa a colisao com seguranca, dispara particulas em one_shot, toca o som, solta loot no pai correto, espalha cacos com impulso fisico e so entao, depois de esperar o tempo das particulas, chama queue_free. Cada peca e opcional e independente: voce pode comecar so com vida e dano, e ir somando particulas, som e cacos conforme o jogo pede.

A maior licao deste sistema nao e o codigo em si, e o timing. Quase todo bug de objeto destrutivel vem de liberar o no cedo demais. Mantenha a regra simples: esconda primeiro, espere o efeito, libere por ultimo. Com isso suas caixas vao quebrar do jeito certo, e o jogador vai querer quebrar todas que encontrar pela frente.