Godot do Zero: Sistema de dano, vida e invencibilidade temporária

Capítulo 10

Tempo estimado de leitura: 9 minutos

+ Exercício

Objetivo do sistema

Neste capítulo, vamos criar um sistema reutilizável de vida (HP), dano e invencibilidade temporária (i-frames). A ideia é que tanto jogador quanto inimigos usem o mesmo componente de Health, e que o restante do jogo reaja via sinais (por exemplo, tocar som, piscar sprite, aplicar knockback, remover/respawn).

Também vamos impor regras importantes para evitar bugs comuns: prevenir múltiplos hits no mesmo frame, invencibilidade por tempo após levar dano e controle de remoção/respawn de inimigos.

Estrutura sugerida de nós

Você pode adaptar ao seu projeto, mas uma estrutura prática é:

  • Player (CharacterBody2D)
    • AnimatedSprite2D (ou Sprite2D)
    • Hurtbox (Area2D) + CollisionShape2D
    • Health (Node) com script Health.gd
    • AudioStreamPlayer2D (hit/death)
  • Enemy (CharacterBody2D ou Node2D)
    • Sprite/AnimatedSprite2D
    • Hitbox (Area2D) + CollisionShape2D (para causar dano)
    • Hurtbox (Area2D) + CollisionShape2D (para receber dano, se aplicável)
    • Health (Node) com Health.gd

Separar Hitbox (causa dano) de Hurtbox (recebe dano) facilita controlar quem bate e quem apanha, além de permitir ataques com espada/projéteis no futuro.

Componente reutilizável: Health.gd

Conceito

O componente Health será um Node simples que guarda HP atual/máximo, aplica dano, controla invencibilidade e emite sinais para o “dono” (player/inimigo) reagir. Assim, o script do Player não precisa “saber” regras de HP, apenas escuta eventos.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Script Health.gd

Crie um script em res://scripts/components/Health.gd e anexe em um Node chamado Health dentro do Player e do Enemy.

extends Node
class_name Health

signal damaged(amount: int, from: Node)
signal died(from: Node)
signal healed(amount: int)

@export var max_hp: int = 5
@export var invincibility_time: float = 0.6

var hp: int
var invincible: bool = false

# Prevenção de múltiplos hits no mesmo frame (ou no mesmo tick)
var _last_damage_frame: int = -1

func _ready() -> void:
	hp = max_hp

func reset() -> void:
	hp = max_hp
	invincible = false
	_last_damage_frame = -1

func apply_damage(amount: int, from: Node = null) -> bool:
	if amount <= 0:
		return false
	if hp <= 0:
		return false
	if invincible:
		return false

	var current_frame := Engine.get_physics_frames()
	if _last_damage_frame == current_frame:
		return false
	_last_damage_frame = current_frame

	hp = max(hp - amount, 0)
	emit_signal("damaged", amount, from)

	if hp == 0:
		emit_signal("died", from)
		return true

	_start_invincibility()
	return true

func heal(amount: int) -> void:
	if amount <= 0:
		return
	if hp <= 0:
		return
	var old := hp
	hp = min(hp + amount, max_hp)
	var delta := hp - old
	if delta > 0:
		emit_signal("healed", delta)

func _start_invincibility() -> void:
	invincible = true
	# Timer leve sem precisar criar nó na cena
	await get_tree().create_timer(invincibility_time).timeout
	invincible = false

Por que isso resolve problemas comuns?

  • Invencibilidade temporária: impede “derreter” HP ao encostar em inimigo por vários frames seguidos.
  • _last_damage_frame: evita múltiplos hits no mesmo frame caso duas áreas/colisões disparem eventos simultâneos.
  • Sinais: desacopla o componente (HP) do feedback (piscar, som, knockback, morte).

Dano por contato: Hitbox que aplica dano

Conceito

Um inimigo pode ter uma Hitbox (Area2D) que detecta a Hurtbox do jogador. Ao detectar, chama apply_damage no Health do alvo.

Script para a Hitbox do inimigo

No nó Hitbox (Area2D) do inimigo, anexe um script como EnemyHitbox.gd. Ele procura um Health no corpo/área atingida.

extends Area2D

@export var damage: int = 1

func _ready() -> void:
	area_entered.connect(_on_area_entered)
	body_entered.connect(_on_body_entered)

func _try_damage(target: Node) -> void:
	if target == null:
		return
	# Procura Health no próprio alvo ou em filhos
	var health := target.get_node_or_null("Health")
	if health == null:
		health = target.find_child("Health", true, false)
	if health == null:
		return
	health.apply_damage(damage, owner)

func _on_area_entered(area: Area2D) -> void:
	# Se a Hurtbox for uma Area2D, o Health pode estar no pai
	_try_damage(area)
	_try_damage(area.get_parent())

func _on_body_entered(body: Node) -> void:
	_try_damage(body)

Observação: isso é propositalmente flexível para funcionar com Hurtbox como Area2D (com o Health no Player) ou com colisão direta no corpo.

Feedback básico no jogador: piscar, knockback e som

Conceito

O Health emite damaged e died. O Player escuta esses sinais e executa feedback: piscar sprite, empurrão (knockback) e som. Assim, o componente Health continua genérico.

Conectando sinais do Health no Player

No script do Player, pegue referência ao nó Health e conecte os sinais (pode ser via editor ou por código). Exemplo por código:

@onready var health: Health = $Health
@onready var sprite: CanvasItem = $AnimatedSprite2D
@onready var sfx_hit: AudioStreamPlayer2D = $SfxHit

@export var knockback_strength: float = 220.0

func _ready() -> void:
	health.damaged.connect(_on_damaged)
	health.died.connect(_on_died)

func _on_damaged(amount: int, from: Node) -> void:
	_play_hit_feedback(from)

func _on_died(from: Node) -> void:
	# Aqui você pode desabilitar controles, tocar animação, etc.
	queue_free() # ou iniciar respawn do jogador, dependendo do seu jogo

func _play_hit_feedback(from: Node) -> void:
	if sfx_hit:
		sfx_hit.play()
	_apply_knockback(from)
	_start_blink()

Knockback simples

O knockback pode ser um impulso na direção oposta ao agressor. Se o Player for CharacterBody2D, você pode somar na velocity (ou na variável que você usa para movimento).

func _apply_knockback(from: Node) -> void:
	if from == null:
		return
	if not (from is Node2D):
		return
	var dir := (global_position - from.global_position).normalized()
	velocity += dir * knockback_strength

Se você já controla velocity em outro lugar, o ideal é aplicar o knockback como um “extra” que decai com o tempo (por exemplo, usando uma variável knockback_velocity), mas o exemplo acima funciona como versão mínima.

Piscar sprite durante invencibilidade

Uma forma simples é alternar a visibilidade/modulate por um curto período. Como o Health já controla o tempo de invencibilidade, podemos piscar por um tempo parecido. Exemplo:

func _start_blink() -> void:
	# Pisca por um tempo próximo ao da invencibilidade
	var blink_time := health.invincibility_time
	var t := 0.0
	while t < blink_time:
		sprite.visible = not sprite.visible
		await get_tree().create_timer(0.08).timeout
		t += 0.08
	sprite.visible = true

Se preferir manter sempre visível, troque por sprite.modulate.a = 0.4 e volte para 1.0 no final.

Recebendo dano no inimigo e removendo/respawn

Conceito

Inimigos também terão Health. Ao morrer, podemos remover (queue_free) ou respawn após um tempo em um ponto definido. O importante é que a morte seja um evento (sinal) e não uma checagem espalhada pelo código.

Reação do inimigo aos sinais

No script do inimigo:

@onready var health: Health = $Health
@onready var sprite: CanvasItem = $AnimatedSprite2D

@export var respawn_enabled: bool = false
@export var respawn_time: float = 2.0

var _spawn_position: Vector2

func _ready() -> void:
	_spawn_position = global_position
	health.damaged.connect(_on_damaged)
	health.died.connect(_on_died)

func _on_damaged(amount: int, from: Node) -> void:
	# Feedback mínimo no inimigo
	sprite.modulate = Color(1, 0.6, 0.6)
	await get_tree().create_timer(0.08).timeout
	sprite.modulate = Color(1, 1, 1)

func _on_died(from: Node) -> void:
	if respawn_enabled:
		await _respawn()
	else:
		queue_free()

func _respawn() -> void:
	# Desativa temporariamente colisões/visibilidade
	visible = false
	set_physics_process(false)
	set_process(false)
	await get_tree().create_timer(respawn_time).timeout
	global_position = _spawn_position
	health.reset()
	visible = true
	set_physics_process(true)
	set_process(true)

Se o inimigo tiver Hitbox/Hurtbox, você pode também desativar monitoring/monitorable das Areas durante o tempo morto para evitar interações invisíveis.

Regra extra: evitando dano contínuo por contato

Mesmo com invencibilidade, às vezes você quer que o contato cause dano apenas quando “encosta” (entrada) e não enquanto permanece sobreposto. Para isso, prefira usar area_entered/body_entered (como no exemplo) em vez de checar sobreposição em _physics_process.

Se você precisar de dano por permanência (ex.: lava), implemente um tick de dano com timer e respeite invencibilidade ou um cooldown próprio do hazard.

Mini-arena de testes (prática guiada)

1) Montar a arena

  • Crie uma cena Arena.tscn com um Node2D como raiz.
  • Adicione um chão/parede (TileMap ou StaticBody2D) apenas para limitar movimento.
  • Instancie o Player no centro.
  • Instancie 2 ou 3 Enemies em posições diferentes.

2) Configurar HP e dano

  • No Health do Player: max_hp = 5, invincibility_time = 0.6.
  • No Health do Enemy: max_hp = 2, invincibility_time = 0.2 (opcional).
  • No Hitbox do Enemy: damage = 1.

3) Teste 1: contato causa dano uma vez e ativa i-frames

  • Rode a cena e encoste no inimigo.
  • Verifique: HP diminui 1, toca som, sprite pisca, e não perde HP continuamente enquanto estiver encostado (por causa da invencibilidade).

4) Teste 2: prevenção de múltiplos hits no mesmo frame

  • Coloque dois inimigos com Hitbox sobrepostos no mesmo ponto.
  • Encoste nos dois ao mesmo tempo.
  • Verifique: o Player não toma “dano duplo instantâneo” no mesmo frame; no máximo um hit por frame (e depois i-frames).

5) Teste 3: morte e remoção/respawn do inimigo

  • Faça o inimigo receber dano (por exemplo, se você já tem ataque do jogador; caso não tenha, você pode temporariamente chamar $Enemy/Health.apply_damage(1, $Player) via debug ou um botão).
  • Ao chegar em 0 HP, verifique: inimigo some (queue_free) ou respawna após respawn_time se habilitado.

6) Teste 4: knockback

  • Encoste no inimigo vindo de diferentes direções.
  • Verifique: o Player é empurrado para longe do inimigo, sem “puxar” para dentro.

Dicas de depuração rápida

  • Mostre HP no Output: no sinal damaged, faça print("HP:", health.hp) temporariamente.
  • Se o dano não dispara, confira: CollisionShape2D habilitado, camadas/máscaras corretas e se a Area2D está com monitoring ligado.
  • Se o Player fica invisível, garanta que o blink sempre restaura visible = true ao final.

Agora responda o exercício sobre o conteúdo:

Qual é a principal vantagem de usar um componente Health reutilizável que emite sinais (damaged/died/healed) em vez de tratar HP e morte diretamente no script do Player/Inimigo?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

O componente Health centraliza regras de HP/dano, i-frames e bloqueio de múltiplos hits no mesmo frame, enquanto Player e Enemy apenas escutam sinais para executar feedback (som, blink, knockback) e lógica de morte (remover/respawn).

Próximo capitúlo

Godot do Zero: Câmera 2D com Camera2D e limites de fase

Arrow Right Icon
Capa do Ebook gratuito Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript
59%

Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.