Ficha 02 · Godot intermédio
Exercício 1 · Inimigo com patrulha
Cria scene "Enemy.tscn" (CharacterBody2D) que anda para a frente e para trás entre 2 marcadores. Quando bate numa parede inverte direcção.
Resposta:
enemy.gd:
extends CharacterBody2D
const SPEED = 100
const GRAVITY = 980
var direction = 1 # 1 ou -1
func _physics_process(delta):
if not is_on_floor():
velocity.y += GRAVITY * delta
velocity.x = SPEED * direction
var collision = move_and_collide(velocity * delta)
if collision:
# Se bateu em parede (não chão), inverte
if abs(collision.get_normal().x) > 0.5:
direction *= -1
else:
move_and_slide()
Ou (simples) usar RayCast2D para detectar parede/borda:
@onready var ray = $RayCast2D
func _physics_process(delta):
if ray.is_colliding():
direction *= -1
ray.target_position.x *= -1
velocity.x = SPEED * direction
move_and_slide()
Exercício 2 · Spawn de inimigos com Timer
Cria spawner que gera um inimigo a cada 3 segundos em posição aleatória entre x=100 e x=900.
Resposta:
Spawner (Node2D)
└── Timer
spawner.gd:
extends Node2D
const EnemyScene = preload("res://enemy.tscn")
@onready var timer = $Timer
func _ready():
timer.wait_time = 3.0
timer.timeout.connect(_spawn)
timer.start()
func _spawn():
var e = EnemyScene.instantiate()
e.position = Vector2(randf_range(100, 900), 100)
get_parent().add_child(e)
Exercício 3 · Player com vidas e morte
Adiciona ao Player:
- 3 vidas (variável).
- Sinal vida_mudou(nova).
- Função sofrer_dano() que decrementa.
- Quando vidas = 0, mostra "Game Over" na UI.
Resposta:
player.gd:
extends CharacterBody2D
signal vida_mudou(nova)
signal morreu
var vidas = 3
func sofrer_dano(d = 1):
vidas -= d
vida_mudou.emit(vidas)
if vidas <= 0:
morreu.emit()
# Desactivar input
set_physics_process(false)
$AnimatedSprite2D.play("die")
ui.gd:
extends CanvasLayer
@onready var vidas_label = $VidasLabel
@onready var game_over = $GameOverPanel
func _ready():
var player = get_node("/root/World/Player")
player.vida_mudou.connect(_atualizar_vidas)
player.morreu.connect(_game_over)
game_over.hide()
_atualizar_vidas(player.vidas)
func _atualizar_vidas(n):
vidas_label.text = "Vidas: " + str(n)
func _game_over():
game_over.show()
Exercício 4 · Coletável dá vida
Cria scene "HealthCoin.tscn" (Area2D) que ao ser apanhada dá +1 vida ao player. Reutiliza scripts anteriores.
Resposta:
health_coin.gd:
extends Area2D
func _ready():
body_entered.connect(_on_body_entered)
func _on_body_entered(body):
if body.has_method("sofrer_dano"): # é um player-like
body.vidas += 1
body.vida_mudou.emit(body.vidas)
queue_free()
Para evitar exceder máximo:
const VIDA_MAX = 5
func _on_body_entered(body):
if body.vidas < VIDA_MAX:
body.vidas += 1
body.vida_mudou.emit(body.vidas)
queue_free()
Exercício 5 · Áudio (efeitos sonoros)
Adiciona ao player: - Som de pulo (AudioStreamPlayer). - Som de morte.
Toca os sons nos momentos certos.
Resposta:
Estrutura:
Player (CharacterBody2D)
├── AnimatedSprite2D
├── CollisionShape2D
├── PuloSfx (AudioStreamPlayer) # stream = pulo.ogg
└── MorteSfx (AudioStreamPlayer) # stream = morte.ogg
player.gd:
@onready var pulo_sfx = $PuloSfx
@onready var morte_sfx = $MorteSfx
func _physics_process(delta):
# ...
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
pulo_sfx.play()
# ...
func sofrer_dano(d = 1):
vidas -= d
if vidas <= 0:
morte_sfx.play()
# ...
Sons gratuitos: freesound.org.
Exercício 6 · Música de fundo com loop
Adiciona AudioStreamPlayer com música .ogg em loop, que toca desde o início e baixa volume com tecla M.
Resposta:
- Importar
musica.oggpara o projecto. - Selecionar o ficheiro → Import tab → ✓ Loop, depois Reimport.
- Adicionar AudioStreamPlayer à cena, arrastar o .ogg para Stream.
- ✓ Autoplay.
world.gd:
extends Node2D
@onready var musica = $Musica
var muted = false
func _unhandled_input(event):
if event.is_action_pressed("mute"): # tecla M no Input Map
muted = !muted
musica.volume_db = -80 if muted else 0
Adicionar action "mute" no Input Map → tecla M.
Exercício 7 · Save / Load com JSON
Cria save com score, vidas e nível actual. Botão "Save" guarda, botão "Load" carrega.
Resposta:
game.gd (autoload):
extends Node
const SAVE_PATH = "user://savegame.json"
var score = 0
var vidas = 3
var nivel = 1
func save_game():
var data = {
"score": score,
"vidas": vidas,
"nivel": nivel
}
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(data))
file.close()
print("Guardado!")
func load_game():
if not FileAccess.file_exists(SAVE_PATH):
print("Sem save")
return
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
file.close()
if data:
score = data["score"]
vidas = data["vidas"]
nivel = data["nivel"]
print("Carregado: score=", score, " vidas=", vidas)
UI:
$SaveBtn.pressed.connect(Game.save_game)
$LoadBtn.pressed.connect(Game.load_game)
Exercício 8 · Export para Web (HTML5)
Faz export do projecto para HTML5 e demonstra a correr no browser (pode ser localmente com python3 -m http.server).
Resposta:
-
Editor → Manage Export Templates → Download (uma vez).
-
Project → Export → Add… → Web.
-
Export Path:
build/web/index.html. -
Click "Export Project" (sem Debug).
-
Servir localmente (browsers não correm file:// para WASM):
bash cd build/web python3 -m http.server 8000 -
Abrir http://localhost:8000 — o jogo corre no browser!
-
Para publicar:
- itch.io: zip de
build/web/, upload como "HTML" project. - GitHub Pages: commit a
build/web/numa branchgh-pages. - Vercel/Netlify: drag-and-drop da pasta.
Limitações HTML5: - Sem alguns formatos de áudio. - Performance pior que nativo. - Tamanho download (~10MB+ comum).
Mas distribuir é trivial e funciona em qualquer dispositivo com browser.