Enemics Predictibles són Enemics Avorrits
Fins ara, els nostres enemics només es mouen en línia recta. Funciona, però no impressiona. Els shooters clàssics com Galaga, Gradius o R-Type tenen enemics que ondulen, orbiten i executen patrons coreografiats.
La clau d’aquests moviments són dues funcions matemàtiques: Sinus i Cosinus.
1. El Sinus: L’Ona
El Sinus (sin) és una funció que espera que li passis un angle en radians i retorna valors entre -1 i 1 de forma cíclica.
Això no vol dir que li hagis de passar necessàriament un angle, pots passar-li qualsevol nombre real i et retornarà un valor entre -1 i 1. Per tant, podem passar-li el temps, per exemple, i obtindrem un valor que oscil·la suaument entre -1 i 1.
Si grafiquem sin(x) on x va de 0 a 2π (una volta completa), obtenim una ona:
| Valor de x | sin(x) |
|---|---|
| 0 | 0 |
| π/2 (90°) | 1 (màxim) |
| π (180°) | 0 |
| 3π/2 (270°) | -1 (mínim) |
| 2π (360°) | 0 |
Després de 2π, el cicle es repeteix infinitament.
0.00 rad1.0000.000Per a què serveix en un joc?
Si usem el temps com a entrada del sinus, obtenim un valor que oscil·la suaument:
var offset_y = sin(temps) # Oscil·la entre -1 i 1var offset_y = sin(temps) # Oscil·la entre -1 i 1Usant el valor que retorna sin, i multiplicant-lo per una amplitud, valgui la redundància, ampliem el rang de valors, ja no oscil·la entre -1 i 1, sinó entre amplitud negatiu i amplitud positiu. Per tant, si volem que es mogui en un rang entre -50 a 50 píxels, multiplicarem per 50.
var offset_y = sin(temps) * 50 # Oscil·la entre -50 i 50 píxelsvar offset_y = sin(temps) * 50 # Oscil·la entre -50 i 50 píxels2. Un Nou Enemic: L’Helicòpter
Deixarem el nostre avió bàsic (enemy_plane.gd) com està, movent-se recte. Per a aquests nous patrons, crearem un nou enemic, l’Helicòpter, del qual farem 2 versions:
- Ona: Es mou fent onades.
- Òrbita: Gira en cercles perfectes.

Clic dret -> Desa imatge.
Crèdits: Assets Free Laser Bullets Pack 2020 per Wenrexa (CC0 1.0).
Pas 1: L’Helicòpter d’Ona
- Crea una nova escena, afegeix el node
Area2Di anomena’lEnemyWave, afegeix també unSprite2Damb l’sprite de l’helicòpter i unCollisionShape2D. - Afegeix-li un script anomenat
enemy_wave.gdalEnemyWave. - Desa l’escena com
enemy_wave.tscn.
L’Script de l’Ona
Volem que aquest enemic onduli verticalment mentre avança.
Per començar, copia i enganxa tot el codi de enemy_plane.gd dins el teu nou script enemy_wave.gd. A partir d’aquesta base, anirem afegint els canvis pas a pas.
1. Variables de Control
Necessitem definir quant es mou l’ona (amplitud) i com de ràpid (freqüència).
@export var wave_amplitude = 100.0 # Alçada de l'ona (en píxels)
@export var wave_frequency = 3.0 # Velocitat de l'oscil·lació@export var wave_amplitude = 100.0 # Alçada de l'ona (en píxels)
@export var wave_frequency = 3.0 # Velocitat de l'oscil·lacióTambé necessitem rastrejar el temps (per alimentar la funció sin) i recordar la nostra alçada inicial (per oscil·lar al voltant d’ella, no anar-nos-en volant).
var time = 0.0 # Acumulador de temps (rellotge intern)
var start_y = 0.0 # La nostra "línia base" a l'eix Yvar time = 0.0 # Acumulador de temps (rellotge intern)
var start_y = 0.0 # La nostra "línia base" a l'eix Y2. Desar la posició inicial
Al _ready(), hem de desar la posició Y inicial de l’enemic.
Per què? Perquè la funció sin() oscil·la al voltant de 0 (puja a +1, baixa a -1). Si apliquéssim això directament a position.y, l’enemic saltaria a la part superior de la pantalla (coordenada Y=0).
En desar start_y, establim un nou “centre” per a l’oscil·lació. Així, l’enemic oscil·larà cap amunt i cap avall relatiu a on el vam col·locar a l’editor.
func _ready():
start_y = position.y # Desem l'alçada on el vam posar a l'editor
area_entered.connect(_on_area_entered)func _ready():
start_y = position.y # Desem l'alçada on el vam posar a l'editor
area_entered.connect(_on_area_entered)3. El Moviment (La Fórmula)
Ves a la teva funció _process(delta). Busca el bloc else (que s’executa quan l’enemic no està morint) i reemplaça el seu contingut per aquest:
func _process(delta):
if is_dying:
# ... (deixar com estava o veure script final) ...
else:
# 1. ACUMULAR TEMPS
time += delta
# 2. MOVIMENT HORITZONTAL (Mantenim el de sempre)
position.x -= speed * delta
# 3. MOVIMENT VERTICAL (La novetat)
# Y = Centre + sin(temps * freqüència) * amplitud
position.y = start_y + sin(time * wave_frequency) * wave_amplitudefunc _process(delta):
if is_dying:
# ... (deixar com estava o veure script final) ...
else:
# 1. ACUMULAR TEMPS
time += delta
# 2. MOVIMENT HORITZONTAL (Mantenim el de sempre)
position.x -= speed * delta
# 3. MOVIMENT VERTICAL (La novetat)
# Y = Centre + sin(temps * freqüència) * amplitud
position.y = start_y + sin(time * wave_frequency) * wave_amplitudeL’Script Complet
Ajuntant tot això amb el codi base de dany i mort que ja teníem de l’avió:
extends Area2D
@export var speed = 200
@export var hp = 3
@export var wave_amplitude = 100.0
@export var wave_frequency = 3.0
var is_dying = false
var fall_speed = 0.0
var time = 0.0
var start_y = 0.0
func _ready():
start_y = position.y
area_entered.connect(_on_area_entered)
func _process(delta):
if is_dying:
fall_speed += 500 * delta
position.y += fall_speed * delta
position.x -= speed * 0.5 * delta
else:
time += delta
position.x -= speed * delta
# La fórmula de l'ona
position.y = start_y + sin(time * wave_frequency) * wave_amplitude
# 1. start_y farà que parteixi de la posició original o relativa.
# 2. usem el temps en sin() com a valor d'angle, el que ens retornarà
# valors entre -1 i 1.
# 3. Ho multipliquem per la freqüència per donar-li velocitat.
# 4. Al resultat li podem, a més, multiplicar l'amplitud per donar-li mida.
# **Recorda** l'ordre de les operacions (multiplicació abans que la funció).
func _on_area_entered(area):
if is_dying:
return
hp -= 1
area.queue_free()
hit_flash()
if hp <= 0:
die()
func hit_flash():
var sprite = $Sprite2D
var shader_material = sprite.material
if shader_material is ShaderMaterial:
shader_material.set_shader_parameter("flash_active", true)
await get_tree().create_timer(0.1).timeout
shader_material.set_shader_parameter("flash_active", false)
func die():
is_dying = true
$CollisionShape2D.set_deferred("disabled", true)
await get_tree().create_timer(1.0).timeout
queue_free()extends Area2D
@export var speed = 200
@export var hp = 3
@export var wave_amplitude = 100.0
@export var wave_frequency = 3.0
var is_dying = false
var fall_speed = 0.0
var time = 0.0
var start_y = 0.0
func _ready():
start_y = position.y
area_entered.connect(_on_area_entered)
func _process(delta):
if is_dying:
fall_speed += 500 * delta
position.y += fall_speed * delta
position.x -= speed * 0.5 * delta
else:
time += delta
position.x -= speed * delta
# La fórmula de l'ona
position.y = start_y + sin(time * wave_frequency) * wave_amplitude
# 1. start_y farà que parteixi de la posició original o relativa.
# 2. usem el temps en sin() com a valor d'angle, el que ens retornarà
# valors entre -1 i 1.
# 3. Ho multipliquem per la freqüència per donar-li velocitat.
# 4. Al resultat li podem, a més, multiplicar l'amplitud per donar-li mida.
# **Recorda** l'ordre de les operacions (multiplicació abans que la funció).
func _on_area_entered(area):
if is_dying:
return
hp -= 1
area.queue_free()
hit_flash()
if hp <= 0:
die()
func hit_flash():
var sprite = $Sprite2D
var shader_material = sprite.material
if shader_material is ShaderMaterial:
shader_material.set_shader_parameter("flash_active", true)
await get_tree().create_timer(0.1).timeout
shader_material.set_shader_parameter("flash_active", false)
func die():
is_dying = true
$CollisionShape2D.set_deferred("disabled", true)
await get_tree().create_timer(1.0).timeout
queue_free()3. Moviment Circular (Òrbita)
Para aconseguir un moviment circular, necessitem presentar al germà del Sinus: el Cosinus.
El Cosinus (cos) és idèntic al Sinus, però en comptes d’oscil·lar a l’eix Y, es mou a l’eix X.
| Valor de x | sin(x) (Eix Y) | cos(x) (Eix X) |
|---|---|---|
| 0 | 0 | 1 |
| π/2 | 1 | 0 |
| π | 0 | -1 |
Per què és important? Cos sempre està desfasat respecte a Sen en 90º, així que mai valen 0 alhora. Quan sin val 0, cos val 1. Quan cos val 0, sin val 1. Aquesta alternança suau crea el moviment continu del cercle.
Implementació Pràctica
Per a aquests enemics orbitals, seguirem usant el nostre Helicòpter. ⚠️ Segueix aquests passos, o si no sense voler podries veure’t editant l’script que no toca si no tens cura.
- Duplica l’escena
enemy_wave.tscni anomena-laenemy_orbital.tscn. - Canvia el nom del node pare
EnemyWaveaEnemyOrbital. - Duplica l’script
enemy_wave.gd, anomena’lenemy_orbital.gdi assigna’l al nodeEnemyOrbital.
Ara, editem enemy_orbital.gd pas a pas:
1. Variables: D’Ona a Cercle
Com hem copiat l’script de l’Ona (enemy_wave.gd), tenim variables que ja no serveixen.
Elimina aquestes variables:
speed(no volem que avanci)wave_amplitudeiwave_frequency(són de l’ona)start_y(usarem vector complet)fall_speed(usarem inèrcia vectorial)
Mantingues aquestes variables:
hp(la vida)is_dying(estat)time(rellotge intern)
Afegeix les noves variables pel cercle:
orbit_radius: Radi del cercle.orbit_speed: Velocitat angular.velocity: Per calcular la inèrcia (substitueix afall_speed).center: Punt central de l’òrbita (substitueix astart_y).
A partir d’aquí veuràs fàcilment quins blocs de codi substituir perquè en eliminar les variables, ara l’editor et marcarà errors.
@export var orbit_radius = 100.0
@export var orbit_speed = 2.0
var velocity = Vector2.ZERO
var center = Vector2.ZERO@export var orbit_radius = 100.0
@export var orbit_speed = 2.0
var velocity = Vector2.ZERO
var center = Vector2.ZERO2. Ready: Desar el Centre
A l’Ona desàvem start_y (float), però el Cercle necessita un centre 2D (X i Y).
func _ready():
center = position # Desem la posició (X, Y) inicial com a centre
area_entered.connect(_on_area_entered)func _ready():
center = position # Desem la posició (X, Y) inicial com a centre
area_entered.connect(_on_area_entered)3. Process: El Gir
Ves a el teu _process(delta).
- Dins del
if is_dying: Reemplaça la lògica de caiguda simple per aquesta d’inèrcia (necessària perquè ara ens movem en 2D). - Dins del
else: Reemplaça tota la lògica de l’ona per la del cercle.
He numerat els blocs de codi perquè puguis seguir millor la lògica que seguirà.
func _process(delta):
if is_dying:
# 3. FASE MORT: Usem la inèrcia calculeada
# Ja no calculem 'cos/sin'. Usem l'última 'velocity' coneguda
# per seguir movent-nos en aquesta direcció, i li sumem gravetat.
velocity.y += 1000 * delta
position += velocity * delta
else:
# 1. FASE VIVA: Desem posició abans de moure'ns
var prev_pos = position
time += delta
# 2. MOVIMENT CIRCULAR
position.x = center.x + cos(time * orbit_speed) * orbit_radius
position.y = center.y + sin(time * orbit_speed) * orbit_radius
# CÀLCUL D'INÈRCIA (Per usar-la quan mori)
# Com movem 'position' a mà, Godot no sap a quina velocitat anem.
# La calculem nosaltres: Velocitat = Espai / Temps
velocity = (position - prev_pos) / deltafunc _process(delta):
if is_dying:
# 3. FASE MORT: Usem la inèrcia calculeada
# Ja no calculem 'cos/sin'. Usem l'última 'velocity' coneguda
# per seguir movent-nos en aquesta direcció, i li sumem gravetat.
velocity.y += 1000 * delta
position += velocity * delta
else:
# 1. FASE VIVA: Desem posició abans de moure'ns
var prev_pos = position
time += delta
# 2. MOVIMENT CIRCULAR
position.x = center.x + cos(time * orbit_speed) * orbit_radius
position.y = center.y + sin(time * orbit_speed) * orbit_radius
# CÀLCUL D'INÈRCIA (Per usar-la quan mori)
# Com movem 'position' a mà, Godot no sap a quina velocitat anem.
# La calculem nosaltres: Velocitat = Espai / Temps
velocity = (position - prev_pos) / deltaSi bé jo t’estic deixant anar el codi, és aconsellable que cada cop que escriguis o enganxis codi intentis entendre què fa aquest codi i per què l’incloem.
L’Script Complet (Orbit)
extends Area2D
@export var hp = 3
@export var orbit_radius = 100.0
@export var orbit_speed = 2.0
var is_dying = false
var time = 0.0
var velocity = Vector2.ZERO
var center = Vector2.ZERO
func _ready():
center = position
area_entered.connect(_on_area_entered)
func _process(delta):
if is_dying:
velocity.y += 1000 * delta
position += velocity * delta
else:
var prev_pos = position
time += delta
position.x = center.x + cos(time * orbit_speed) * orbit_radius
position.y = center.y + sin(time * orbit_speed) * orbit_radius
velocity = (position - prev_pos) / delta
func _on_area_entered(area):
if is_dying:
return
hp -= 1
area.queue_free()
hit_flash()
if hp <= 0:
die()
func hit_flash():
var sprite = $Sprite2D
var shader_material = sprite.material
if shader_material is ShaderMaterial:
shader_material.set_shader_parameter("flash_active", true)
await get_tree().create_timer(0.1).timeout
shader_material.set_shader_parameter("flash_active", false)
func die():
is_dying = true
$CollisionShape2D.set_deferred("disabled", true)
await get_tree().create_timer(1.0).timeout
queue_free()extends Area2D
@export var hp = 3
@export var orbit_radius = 100.0
@export var orbit_speed = 2.0
var is_dying = false
var time = 0.0
var velocity = Vector2.ZERO
var center = Vector2.ZERO
func _ready():
center = position
area_entered.connect(_on_area_entered)
func _process(delta):
if is_dying:
velocity.y += 1000 * delta
position += velocity * delta
else:
var prev_pos = position
time += delta
position.x = center.x + cos(time * orbit_speed) * orbit_radius
position.y = center.y + sin(time * orbit_speed) * orbit_radius
velocity = (position - prev_pos) / delta
func _on_area_entered(area):
if is_dying:
return
hp -= 1
area.queue_free()
hit_flash()
if hp <= 0:
die()
func hit_flash():
var sprite = $Sprite2D
var shader_material = sprite.material
if shader_material is ShaderMaterial:
shader_material.set_shader_parameter("flash_active", true)
await get_tree().create_timer(0.1).timeout
shader_material.set_shader_parameter("flash_active", false)
func die():
is_dying = true
$CollisionShape2D.set_deferred("disabled", true)
await get_tree().create_timer(1.0).timeout
queue_free()Aquest enemic es queda girant al lloc. En el futur, usarem això per crear esquadrons que entren i es queden orbitant en formació.
4. Taula de Referència
| Patró | Fórmula X | Fórmula Y |
|---|---|---|
| Ona vertical | x -= speed * delta | start_y + sin(t * freq) * amp |
| Cercle | centre_x + cos(t) * radi | centre_y + sin(t) * radi |
Diferents patrons extra que podries realitzar amb les funcions trigonomètriques:
| Patró | Fórmula X | Fórmula Y |
|---|---|---|
| Ona horitzontal | start_x + sin(t * freq) * amp | y -= speed * delta |
| El·lipse | centre_x + cos(t) * radi_x | centre_y + sin(t) * radi_y |
| Espiral | centre_x + cos(t) * (radi + t*k) | centre_y + sin(t) * (radi + t*k) |
Provant al teu Món
- Obre
world.tscn. - Arrossega
enemy_wave.tscnoenemy_orbital.tscnal viewport.
Jo com a detall “molón” he girat els helicòpters -30º perquè es vegin més naturals.

Tingues en compte que trobaràs 2 problemes: els enemics moren en xocar i a més no fan el hit_flash correctament. Això ho solucionarem en el pròxim capítol.
- Executa (F5) i observa.
Experimenta amb diferents valors:
wave_amplitude = 100,wave_frequency = 2→ Ondulació lenta i àmplia.wave_amplitude = 30,wave_frequency = 8→ Vibració ràpida i curta.
Repassem l’après
sin(x): Funció que oscil·la entre -1 i 1. Ideal per moviment ondulatori.cos(x): Igual que sin, però desfasat 90°.- Temps com entrada: Usar
time += deltaper alimentar les funcions. - Amplitud: Multiplicador que defineix el rang del moviment.
- Freqüència: Multiplicador que defineix la velocitat del cicle.
- Estats: Combinar patrons amb
matchi temporitzadors.
En el pròxim capítol, aprendrem a crear Formacions de Combat: grups d’enemics que es mouen coordinadament.