Instanciació: L’art de crear coses del no-res
En la part anterior vam moure la nostra nau. Ara toca que la nau faci alguna cosa més que passejar: Disparar.
Què aprendrem?
- Instanciar Escenes: Usar
preloadiinstantiateper crear bales. - Scene Tree: Entendre on posar aquestes bales (
add_child) perquè no es moguin amb la nau. - Local vs Global: Coordenades perquè la bala surti del canó, no de l’origen del món.
- Neteja: Usar
VisibleOnScreenNotifier2Dper esborrar bales i no fondre la RAM.
1. Recepta: La Bala (bullet.tscn)
Abans de disparar res, necessitem alguna cosa per disparar. Crearem una bala que sàpiga moure’s i morir quan surti de la pantalla.
Igual que vam fer amb la nau, si vols pots descarregar aquesta imatge per a la bala:

Clic dret -> Desa la imatge.
Crèdits: Lunar Battle Pack per MattWalkden (CC0 1.0).
-
Crea una Nova Escena.
-
Node arrel:
Area2D(Compte! No volem físiques complexes com xocar i rebotar, només detectar si toquem alguna cosa). Anomena’lBullet(Bala). -
Fill:
CollisionShape2D(amb forma de rectangle o càpsula petita que cobreixi l’sprite). -
Fill:
AnimatedSprite2D: [NOU] Li donarem vida! No usarem una imatge estàtica.- A l’Inspector, busca la propietat
Sprite Frames-> Prem on posa<empty>-> SeleccionaNew SpriteFrames. - Fes clic de nou sobre el text
SpriteFramesque acabes de crear per obrir el panell inferior d’edició. - Al panell inferior, busca la icona de graella (“Add frames from sprite sheet”) i prem-la.

- Selecciona l’arxiu
shot-atlas.png. - A la finestra emergent veuràs la teva imatge. Configura les divisions: Horitzontal: 3, Vertical: 1.
- Selecciona els 3 quadres (fes clic o arrossega) per il·luminar-los i prem Add 3 frames.
- Configuració de l’Animació:
- Busca el botó “Animation Looping” (icona de fletxes giratòries) al panell esquerre i DESACTIVA’L. Volem que creixi un cop i es quedi gran, no que palpiti.
- Canvia FPS (Speed) a 12. Com que són només 3 frames, això farà que “creixi” súper ràpid (en 0.25 segons).
- Finalment, a l’esquerra de “Animation Looping” del node
AnimatedSprite2D, activa el botó “Autoplay on Load”. Això assegura que l’animació arrenqui sola en disparar.
- A l’Inspector, busca la propietat
-
Fill:
VisibleOnScreenNotifier2D: Aquest node és màgic. Ens avisa quan l’objecte surt de la pantalla.- Assegura’t que el rectangle rosa d’aquest node cobreixi tot l’sprite.
1.1 Neteja Automàtica (Senyals)
Abans de programar el moviment, ens assegurarem que la bala s’esborri en sortir de pantalla. Per això usarem un Senyal.
El node VisibleOnScreenNotifier2D emet el senyal screen_exited quan surt de la pantalla.
Nosaltres (l’script) connectarem aquest senyal a una funció per reaccionar.
Passos per connectar el cable:
- Afegeix un script a la bala (
bullet.gd) si no ho has fet. - Selecciona el node
VisibleOnScreenNotifier2D. - Ves a la pestanya Node (a la dreta, vora l’Inspector).
- Fes doble clic al senyal
screen_exited. - Dóna-li a Connect.
- Godot escriurà automàticament una funció anomenada
_on_visible_on_screen_notifier_2d_screen_exitedal teu script.
Usar el panell Node és la “via ràpida” o de prototipatge. És visual i excel·lent per començar. No obstant això, en projectes seriosos o per a lògiques complexes, solem connectar els senyals per Codi per tenir major control i que res depengui de clics a l’editor.
Per a aquesta bala senzilla, el mètode visual és perfecte. En el proper capítol aprendrem la forma per codi (“La forma Professional”) quan creem els enemics.
Ara que tenim la connexió, completem el codi:
extends Area2D
var speed = 1500
func _physics_process(delta: float) -> void:
position.x += speed * delta
func _on_visible_on_screen_notifier_2d_screen_exited() -> void:
queue_free()extends Area2D
var speed = 1500
func _physics_process(delta: float) -> void:
position.x += speed * delta
func _on_visible_on_screen_notifier_2d_screen_exited() -> void:
queue_free()Desglossament de l’Script:
1. extends Area2D
Heretem d’Area2D perquè només necessitem detectar si la bala “toca” alguna cosa. No necessitem rebots físics (RigidBody) ni moviment complex de personatge (CharacterBody).
2. position.x += speed * delta
Volem que la bala avanci sola cap a la dreta (X positius). Per fer-ho, sumem velocitat a la seva posició a cada fotograma. Multipliquem per speed per definir la rapidesa i per delta per assegurar que el moviment sigui fluid i consistent en qualsevol ordinador (evitant que vagi més ràpid si tens més FPS).
3. _on_..._screen_exited()
Les bales infinites consumeixen memòria fins que el joc explota. Necessitem detectar quan la bala surt de la càmera i deixa de ser útil. Usem el senyal del node VisibleOnScreenNotifier2D per avisar-nos i cridem a queue_free() per eliminar-la netament.
Nota sobre Rendiment (El mite de
queue_free):queue_free()li diu a la RAM: “Esborra això i allibera espai”. I en disparar de nou: “Busca espai i assigna memòria”. En un joc normal, no passa res. Però en un Bullet Hell amb 5.000 bales, fer això constantment causaria estrebades (lag) perquè el processador se saturaria netejant brossa. Solució futura: En capítols avançats veurem Object Pooling (reciclar bales en lloc d’esborrar-les). Per ara,queue_free()és perfecte.
Què significa queue_free()?
Traduït literal: “Posar en cua per alliberar”.
En programació de videojocs, esborrar alguna cosa mentre s’està usant és perillós (pot tancar el joc de cop).
queue_free() és la forma segura de fer-ho:
- Marques l’objecte per esborrar.
- Godot espera a acabar de dibuixar el fotograma actual (Frame).
- En el moment segur (“Idle time”), esborra l’objecte de la memòria.
Usa sempre queue_free() en lloc de free() per evitar errors.
2. El Plànol i l’Objecte (PackedScene)
A Godot, hi ha una distinció fonamental:
.tscn(L’Escena): És el muntatge original. Defineix quins nodes el componen i com funcionen.- La Instància: És una còpia viva d’aquella escena. El joc crea aquestes còpies per usar-les (una còpia per al jugador, 10 còpies per a bales, etc.).
Quan jugues, interactues amb aquestes còpies.
Per disparar, necessitem crear una nova còpia de l’escena bullet.tscn cada cop que prems espai.
El procés de construcció
En GDScript, crear alguna cosa nova té 3 passos:
- Carregar el Plànol:
preload("res://bullet.tscn"). - Instanciar:
plano.instantiate(). Construeixes l’objecte a la memòria RAM, però encara no existeix al món visible. - Afegir a l’Arbre:
add_child(bala). Aquí col·loques l’objecte a l’univers del joc.
Per què preload i no load?
Godot té dues formes de carregar recursos:
preload("ruta"): Carrega l’escena quan el joc arrenca. És més ràpid en execució perquè ja està a la memòria, però consumeix RAM des de l’inici.load("ruta"): Carrega l’escena sota demanda, en el moment exacte que s’executa aquella línia. Estalvia RAM inicial, però pot causar estrebades (micro-aturades) si el recurs és pesat.
Per a bales (arxius petits que usarem constantment), preload és l’elecció correcta.
3. Implementant el Tret
Com preguntem al joc?
Per disparar, necessitem que l’script prengui decisions. Usem dues eines noves:
if(Condicional): “SI passa això… fes això altre”.Input: En el capítol anterior vam usarget_vector. Ara usaremis_action_pressed(“Està la tecla mantinguda?”).
Configuració Obligatòria (Input Map): Igual que vam fer amb WASD:
- Ves a Project > Project Settings > Input Map.
- Afegeix una nova acció anomenada
shoot(disparar). - Assigna-li la Barra Espaiadora i, si vols, el Clic Esquerre del Ratolí (Mouse Left Button) o el botó A/X del Comandament.
Pas 1: Carregar la munició
Per disparar bales, primer hem de dir-li a l’script quina és l’escena de la bala. Afegim aquesta línia al principi de l’script, al costat de les altres variables:
extends CharacterBody2D
@export var speed = 400
# "preload" carrega el plànol a memòria RAM en iniciar el joc
var bullet_scene = preload("res://scenes/bullet.tscn")extends CharacterBody2D
@export var speed = 400
# "preload" carrega el plànol a memòria RAM en iniciar el joc
var bullet_scene = preload("res://scenes/bullet.tscn")Pas 2: Prémer el gallet (Input)
Ara anem al _physics_process i preguntem si el jugador està polsant el botó de disparar.
func _physics_process(delta: float) -> void:
# ... codi de moviment ...
# Si es MANTÉ premuda la tecla "shoot"
if Input.is_action_pressed("shoot"):
shoot()func _physics_process(delta: float) -> void:
# ... codi de moviment ...
# Si es MANTÉ premuda la tecla "shoot"
if Input.is_action_pressed("shoot"):
shoot()Nota:
is_action_pressedretornatruementre mantinguis la tecla premuda. Si volguessis disparar només un cop per clic (semiautomàtic), usariesis_action_just_pressed. Per a aquest joc volem foc automàtic.
Pas 3: Foc! (La funció bàsica)
Creem la funció shoot() al final de l’arxiu. Aquí passa la màgia d’instanciar.
func shoot():
# 1. Crear una còpia de la bala a memòria
var bala = bullet_scene.instantiate()
# 2. Afegir-la al món visible (com a filla nostra)
add_child(bala)func shoot():
# 1. Crear una còpia de la bala a memòria
var bala = bullet_scene.instantiate()
# 2. Afegir-la al món visible (com a filla nostra)
add_child(bala)El Problema 1: Tret sense control
Si executes el joc ara i mantens premut espai, veuràs que surten milers de bales.
Per què? Perquè _physics_process s’executa 60 cops per segon.
Estàs creant 60 bales cada segon. Això destruirà el rendiment i la jugabilitat.
Solució: Necessitem un Cooldown (Temps d’espera entre trets).
El Problema 2: Les bales es mouen amb la nau
Si et mous mentre dispares, veuràs que les bales es mouen amb tu.
Per què? Perquè vas usar add_child(bala).
En ser filla de la nau, la bala hereta la teva posició i rotació. És a dir, portes les bales enganxades al cos!
Solució: La bala ha de ser filla del Món, no de la Nau.
Pas 4: Controlar el caos (Cooldown i Jerarquies)
Para arreglar els 2 problemes necessitem millorar el nostre script pas a pas.
4.1. Variables Noves
Necessitem memòria per saber quant temps ha passat i si podem disparar. Afegeix això al principi:
@export var fire_rate = 0.125 # Temps entre bales (0.125s = 8 bales/seg)
var can_shoot = true # L'arma està llesta?
var shoot_timer = 0.0 # Comptador de temps@export var fire_rate = 0.125 # Temps entre bales (0.125s = 8 bales/seg)
var can_shoot = true # L'arma està llesta?
var shoot_timer = 0.0 # Comptador de temps4.2. El Comptador (La Lògica)
Al _physics_process, necessitem que algú compti el temps cap enrere quan l’arma s’està “refredant”.
func _physics_process(delta: float) -> void:
# ... aqui va el moviment ...
# GESTIÓ DEL COOLDOWN
if not can_shoot:
shoot_timer -= delta # Restem el temps que ha passat
if shoot_timer <= 0.0:
can_shoot = true # Recarregada!
if Input.is_action_pressed("shoot") and can_shoot: # <-- afegeix "and can_shoot" al
# final per controlar la ràtio de tret.
shoot()func _physics_process(delta: float) -> void:
# ... aqui va el moviment ...
# GESTIÓ DEL COOLDOWN
if not can_shoot:
shoot_timer -= delta # Restem el temps que ha passat
if shoot_timer <= 0.0:
can_shoot = true # Recarregada!
if Input.is_action_pressed("shoot") and can_shoot: # <-- afegeix "and can_shoot" al
# final per controlar la ràtio de tret.
shoot()4.3. El Tret Millorat
Actualitzem la funció shoot(). Ara, a més de disparar, ha de bloquejar l’arma i iniciar el comptador.
I el més important: Arreglar la jerarquia.
func shoot():
# 1. Bloquegem l'arma
can_shoot = false
shoot_timer = fire_rate
# 2. Instanciem
var bala = bullet_scene.instantiate()
# 3. SOLUCIÓ AL CANGUR:
# En lloc d'add_child(bala), la donem en adopció al PARE (el Món)
get_parent().add_child(bala)
# 4. Important: Com que ara és independent, hem de dir-li "Ves a on sóc jo"
bala.global_position = global_positionfunc shoot():
# 1. Bloquegem l'arma
can_shoot = false
shoot_timer = fire_rate
# 2. Instanciem
var bala = bullet_scene.instantiate()
# 3. SOLUCIÓ AL CANGUR:
# En lloc d'add_child(bala), la donem en adopció al PARE (el Món)
get_parent().add_child(bala)
# 4. Important: Com que ara és independent, hem de dir-li "Ves a on sóc jo"
bala.global_position = global_positionCodi Final: Player.gd
Si ajuntem totes les peces, així ha de quedar el teu script complet i funcional:
extends CharacterBody2D
@export var speed = 400
@export var fire_rate = 0.125
var bullet_scene = preload("res://scenes/bullet.tscn")
var can_shoot = true
var shoot_timer = 0.0
func _physics_process(delta: float) -> void:
# 1. Moviment
var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed
move_and_slide()
# 2. Lògica del Cooldown
if not can_shoot:
shoot_timer -= delta
if shoot_timer <= 0.0:
can_shoot = true
# 3. Gallet (ara comprovem "can_shoot")
if Input.is_action_pressed("shoot") and can_shoot:
shoot()
func shoot():
can_shoot = false
shoot_timer = fire_rate
var bala = bullet_scene.instantiate()
get_parent().add_child(bala)
bala.global_position = global_positionextends CharacterBody2D
@export var speed = 400
@export var fire_rate = 0.125
var bullet_scene = preload("res://scenes/bullet.tscn")
var can_shoot = true
var shoot_timer = 0.0
func _physics_process(delta: float) -> void:
# 1. Moviment
var direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed
move_and_slide()
# 2. Lògica del Cooldown
if not can_shoot:
shoot_timer -= delta
if shoot_timer <= 0.0:
can_shoot = true
# 3. Gallet (ara comprovem "can_shoot")
if Input.is_action_pressed("shoot") and can_shoot:
shoot()
func shoot():
can_shoot = false
shoot_timer = fire_rate
var bala = bullet_scene.instantiate()
get_parent().add_child(bala)
bala.global_position = global_positionAra sí. Tenim un control de ritme precís i les bales són independents.
4. Ajustant la Posició de Sortida (Marker2D)
Ara mateix la bala surt del centre de la nau (la seva panxa). Queda lleig. Per arreglar-ho, usarem un node invisible que serveixi de “referència”.
- Obre l’escena de Player i afegeix un node fill
Marker2Dal node principal (també anomenat Player). - Anomena’l
Muzzle(Boquilla). - Mou-lo visualment fins a la punta del canó (o on vulguis que surtin les bales).
Ara actualitzem el codi per usar aquest marcador:
func shoot():
can_shoot = false
shoot_timer = fire_rate
var bala = bullet_scene.instantiate()
get_parent().add_child(bala)
# Usem la posició del Muzzle
bala.global_position = $Muzzle.global_positionfunc shoot():
can_shoot = false
shoot_timer = fire_rate
var bala = bullet_scene.instantiate()
get_parent().add_child(bala)
# Usem la posició del Muzzle
bala.global_position = $Muzzle.global_positionResum i Reptes
Avui has après un dels conceptes fonamentals del desenvolupament de software: Instanciació (crear objectes dinàmicament).
Repassem els conceptes clau:
- Escenes (
.tscn): Són els plànols (motlles) dels teus objectes. - Instanciar: Convertir aquests plànols en objectes vius en el joc (
.instantiate()). - Nodes Marker2D: Punts de referència invisibles vitals per a
spawns. - Senyals: Cables invisibles perquè els objectes parlin (
screen_exited). queue_free(): La forma segura d’esborrar brossa de la memòria.
En el proper capítol, convertirem aquest camp de tir buit en una batalla real. Crearem Enemics que gestionen la seva pròpia vida i detecten col·lisions. Prepara la munició!