Instanciación: El arte de crear cosas de la nada
En la parte anterior movimos nuestra nave. Ahora toca que la nave haga algo más que pasear: Disparar.
¿Qué vamos a aprender?
- Instanciar Escenas: Usar
preloadyinstantiatepara crear balas. - Scene Tree: Entender dónde poner esas balas (
add_child) para que no se muevan con la nave. - Local vs Global: Coordenadas para que la bala salga del cañón, no del origen del mundo.
- Limpieza: Usar
VisibleOnScreenNotifier2Dpara borrar balas y no fundir la RAM.
1. Receta: La Bala (bullet.tscn)
Antes de disparar nada, necesitamos algo que disparar. Vamos a crear una bala que sepa moverse y morir cuando salga de la pantalla.
Igual que hicimos con la nave, si quieres puedes descargar esta imagen para la bala:

Clic derecho -> Guardar imagen.
Créditos: Lunar Battle Pack por MattWalkden (CC0 1.0).
-
Crea una Nueva Escena.
-
Nodo raíz:
Area2D(¡Ojo! No queremos físicas complejas como chocarse y rebotar, solo detectar si tocamos algo). LlámaloBullet. -
Hijo:
CollisionShape2D(con forma de rectángulo o cápsula pequeña que cubra el sprite). -
Hijo:
AnimatedSprite2D: [NUEVO] ¡Vamos a darle vida! No usaremos una imagen estática.- En el Inspector, busca la propiedad
Sprite Frames-> Pulsa donde pone<empty>-> SeleccionaNew SpriteFrames. - Haz clic de nuevo sobre el texto
SpriteFramesque acabas de crear para abrir el panel inferior de edición. - En el panel inferior, busca el icono de rejilla (“Add frames from sprite sheet”) y púlsalo.

- Selecciona el archivo
shot-atlas.png. - En la ventana emergente verás tu imagen. Configura las divisiones: Horizontal: 3, Vertical: 1.
- Selecciona los 3 cuadros (haz clic o arrastra) para iluminarlos y pulsa Add 3 frames.
- Configuración de la Animación:
- Busca el botón “Animation Looping” (icono de flechas giratorias) en el panel izquierdo y DESACTÍVALO. Queremos que crezca una vez y se quede grande, no que palpite.
- Cambia FPS (Speed) a 12. Como son solo 3 frames, esto hará que “crezca” súper rápido (en 0.25 segundos).
- Finalmente, a la izquierda de “Animation Looping” del nodo
AnimatedSprite2D, activa el botón “Autoplay on Load”. Esto asegura que la animación arranque sola al disparar.
- En el Inspector, busca la propiedad
-
Hijo:
VisibleOnScreenNotifier2D: Este nodo es mágico. Nos avisa cuando el objeto sale de la pantalla.- Asegúrate de que el rectángulo rosa de este nodo cubra todo el sprite.
1.1 Limpieza Automática (Señales)
Antes de programar el movimiento, vamos a asegurarnos de que la bala se borre al salir de pantalla. Para esto usaremos una Señal.
El nodo VisibleOnScreenNotifier2D emite la señal screen_exited cuando sale de la pantalla.
Nosotros (el script) conectaremos esa señal a una función para reaccionar.
Pasos para conectar el cable:
- Añade un script a la bala (
bullet.gd) si no lo has hecho. - Selecciona el nodo
VisibleOnScreenNotifier2D. - Ve a la pestaña Node (a la derecha, junto al Inspector).
- Haz doble clic en la señal
screen_exited. - Dale a Connect.
- Godot escribirá automáticamente una función llamada
_on_visible_on_screen_notifier_2d_screen_exiteden tu script.
Usar el panel Node es la “vía rápida” o de prototipado. Es visual y excelente para empezar. Sin embargo, en proyectos serios o para lógicas complejas, solemos conectar las señales por Código para tener mayor control y que nada dependa de clicks en el editor.
Para esta bala sencilla, el método visual es perfecto. En el próximo capítulo aprenderemos la forma por código (“La forma Profesional”) cuando creemos a los enemigos.
Ahora que tenemos la conexión, completemos el código:
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()Desglose del Script:
1. extends Area2D
Heredamos de Area2D porque solo necesitamos detectar si la bala “toca” algo. No necesitamos rebotes físicos (RigidBody) ni movimiento complejo de personaje (CharacterBody).
2. position.x += speed * delta
Queremos que la bala avance sola hacia la derecha (X positivo). Para ello, sumamos velocidad a su posición en cada fotograma. Multiplicamos por speed para definir la rapidez y por delta para asegurar que el movimiento sea fluido y consistente en cualquier ordenador (evitando que vaya más rápido si tienes más FPS).
3. _on_..._screen_exited()
Las balas infinitas consumen memoria hasta que el juego explota. Necesitamos detectar cuándo la bala sale de la cámara y deja de ser útil. Usamos la señal del nodo VisibleOnScreenNotifier2D para avisarnos y llamamos a queue_free() para eliminarla limpiamente.
Nota sobre Rendimiento (El mito de
queue_free):queue_free()le dice a la RAM: “Borra esto y libera espacio”. Y al disparar de nuevo: “Busca espacio y asigna memoria”. En un juego normal, no pasa nada. Pero en un Bullet Hell con 5.000 balas, hacer esto constantemente causaría tirones (lag) porque el procesador se saturaría limpiando basura. Solución futura: En capítulos avanzados veremos Object Pooling (reciclar balas en lugar de borrarlas). Por ahora,queue_free()es perfecto.
¿Qué significa queue_free()?
Traducido literal: “Poner en cola para liberar”.
En programación de videojuegos, borrar algo mientras se está usando es peligroso (puede cerrar el juego de golpe).
queue_free() es la forma segura de hacerlo:
- Marcas el objeto para borrar.
- Godot espera a terminar de dibujar el fotograma actual (Frame).
- En el momento seguro (“Idle time”), borra el objeto de la memoria.
Usa siempre queue_free() en lugar de free() para evitar errores.
2. El Plano y el Objeto (PackedScene)
En Godot, hay una distinción fundamental:
.tscn(La Escena): Es el montaje original. Define qué nodos lo componen y cómo funcionan.- La Instancia: Es una copia viva de esa escena. El juego crea estas copias para usarlas (una copia para el jugador, 10 copias para balas, etc.).
Cuando juegas, interactúas con estas copias.
Para disparar, necesitamos crear una nueva copia de la escena bullet.tscn cada vez que pulsas espacio.
El proceso de construcción
En GDScript, crear algo nuevo tiene 3 pasos:
- Cargar el Plano:
preload("res://bullet.tscn"). - Instanciar:
plano.instantiate(). Construyes el objeto en la memoria RAM, pero aún no existe en el mundo visible. - Añadir al Árbol:
add_child(bala). Aquí colocas el objeto en el universo del juego.
¿Por qué preload y no load?
Godot tiene dos formas de cargar recursos:
preload("ruta"): Carga la escena cuando el juego arranca. Es más rápido en ejecución porque ya está en memoria, pero consume RAM desde el inicio.load("ruta"): Carga la escena bajo demanda, en el momento exacto que se ejecuta esa línea. Ahorra RAM inicial, pero puede causar tirones (micro-parones) si el recurso es pesado.
Para balas (archivos pequeños que usaremos constantemente), preload es la elección correcta.
3. Implementando el Disparo
¿Cómo preguntamos al juego?
Para disparar, necesitamos que el script tome decisiones. Usamos dos herramientas nuevas:
if(Condicional): “SI pasa esto… haz esto otro”.Input: En el capítulo anterior usamosget_vector. Ahora usaremosis_action_pressed(“¿Está la tecla mantenida?”).
Configuración Obligatoria (Input Map): Igual que hicimos con WASD:
- Ve a Project > Project Settings > Input Map.
- Añade una nueva acción llamada
shoot. - Asígnale la Barra Espaciadora y, si quieres, el Clic Izquierdo del Ratón (Mouse Left Button) o el botón A/X del Mando.
Paso 1: Cargar la munición
Para disparar balas, primero debemos decirle al script cuál es la escena de la bala. Añadimos esta línea al principio del script, junto a las otras variables:
extends CharacterBody2D
@export var speed = 400
# "preload" carga el plano en memoria RAM al iniciar el juego
var bullet_scene = preload("res://scenes/bullet.tscn")extends CharacterBody2D
@export var speed = 400
# "preload" carga el plano en memoria RAM al iniciar el juego
var bullet_scene = preload("res://scenes/bullet.tscn")Paso 2: Apretar el gatillo (Input)
Ahora vamos al _physics_process y preguntamos si el jugador está pulsando el botón de disparo.
func _physics_process(delta: float) -> void:
# ... código de movimiento ...
# Si se MANTIENE pulsada la tecla "shoot"
if Input.is_action_pressed("shoot"):
shoot()func _physics_process(delta: float) -> void:
# ... código de movimiento ...
# Si se MANTIENE pulsada la tecla "shoot"
if Input.is_action_pressed("shoot"):
shoot()Nota:
is_action_presseddevuelvetruemientras mantengas la tecla pulsada. Si quisieras disparar solo una vez por clic (semiautomático), usaríasis_action_just_pressed. Para este juego queremos fuego automático.
Paso 3: ¡Fuego! (La función básica)
Creamos la función shoot() al final del archivo. Aquí ocurre la magia de instanciar.
func shoot():
# 1. Crear una copia de la bala en memoria
var bala = bullet_scene.instantiate()
# 2. Añadirla al mundo visible (como hija nuestra)
add_child(bala)func shoot():
# 1. Crear una copia de la bala en memoria
var bala = bullet_scene.instantiate()
# 2. Añadirla al mundo visible (como hija nuestra)
add_child(bala)El Problema 1: Disparo sin control
Si ejecutas el juego ahora y mantienes pulsado espacio, verás que salen miles de balas.
¿Por qué? Porque _physics_process se ejecuta 60 veces por segundo.
Estás creando 60 balas cada segundo. Eso destruirá el rendimiento y la jugabilidad.
Solución: Necesitamos un Cooldown (Tiempo de espera entre disparos).
El Problema 2: Las balas se mueven con la nave
Si te mueves mientras disparas, verás que las balas se mueven contigo.
¿Por qué? Porque usaste add_child(bala).
Al ser hija de la nave, la bala hereda tu posición y rotación. Es decir, ¡llevas las balas pegadas al cuerpo!
Solución: La bala debe ser hija del Mundo, no de la Nave.
Paso 4: Controlar el caos (Cooldown y Jerarquías)
Para arreglar los 2 problemas necesitamos mejorar nuestro script paso a paso.
4.1. Variables Nuevas
Necesitamos memoria para saber cuánto tiempo ha pasado y si podemos disparar. Añade esto al principio:
@export var fire_rate = 0.125 # Tiempo entre balas (0.125s = 8 balas/seg)
var can_shoot = true # ¿El arma está lista?
var shoot_timer = 0.0 # Contador de tiempo@export var fire_rate = 0.125 # Tiempo entre balas (0.125s = 8 balas/seg)
var can_shoot = true # ¿El arma está lista?
var shoot_timer = 0.0 # Contador de tiempo4.2. El Contador (La Lógica)
En el _physics_process, necesitamos que alguien cuente el tiempo hacia atrás cuando el arma se está “enfriando”.
func _physics_process(delta: float) -> void:
# ... aqui va el movimiento ...
# GESTIÓN DEL COOLDOWN
if not can_shoot:
shoot_timer -= delta # Restamos el tiempo que ha pasado
if shoot_timer <= 0.0:
can_shoot = true # ¡Recargada!
if Input.is_action_pressed("shoot") and can_shoot: # <-- añade "and can_shoot" al
# final para controlar el ratio de disparo.
shoot()func _physics_process(delta: float) -> void:
# ... aqui va el movimiento ...
# GESTIÓN DEL COOLDOWN
if not can_shoot:
shoot_timer -= delta # Restamos el tiempo que ha pasado
if shoot_timer <= 0.0:
can_shoot = true # ¡Recargada!
if Input.is_action_pressed("shoot") and can_shoot: # <-- añade "and can_shoot" al
# final para controlar el ratio de disparo.
shoot()4.3. El Disparo Mejorado
Actualizamos la función shoot(). Ahora, además de disparar, debe bloquear el arma e iniciar el contador.
Y lo más importante: Arreglar la jerarquía.
func shoot():
# 1. Bloqueamos el arma
can_shoot = false
shoot_timer = fire_rate
# 2. Instanciamos
var bala = bullet_scene.instantiate()
# 3. SOLUCIÓN AL CANGURO:
# En lugar de add_child(bala), la damos en adopción al PADRE (el Mundo)
get_parent().add_child(bala)
# 4. Importante: Como ahora es independiente, debemos decirle "Ves a donde estoy yo"
bala.global_position = global_positionfunc shoot():
# 1. Bloqueamos el arma
can_shoot = false
shoot_timer = fire_rate
# 2. Instanciamos
var bala = bullet_scene.instantiate()
# 3. SOLUCIÓN AL CANGURO:
# En lugar de add_child(bala), la damos en adopción al PADRE (el Mundo)
get_parent().add_child(bala)
# 4. Importante: Como ahora es independiente, debemos decirle "Ves a donde estoy yo"
bala.global_position = global_positionCódigo Final: Player.gd
Si juntamos todas las piezas, así debe quedar tu script completo y 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. Movimiento
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. Gatillo (ahora comprobamos "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. Movimiento
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. Gatillo (ahora comprobamos "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_positionAhora sí. Tenemos un control de ritmo preciso y las balas son independientes.
4. Ajustando la Posición de Salida (Marker2D)
Ahora mismo la bala sale del centro de la nave (su barriga). Queda feo. Para arreglarlo, usaremos un nodo invisible que sirva de “referencia”.
- Abre la escena de Player y añade un nodo hijo
Marker2Dal nodo principal (también llamado Player). - Llámalo
Muzzle(Boquilla). - Muévelo visualmente hasta la punta del cañón (o donde quieras que salgan las balas).
Ahora actualizamos el código para usar este marcador:
func shoot():
can_shoot = false
shoot_timer = fire_rate
var bala = bullet_scene.instantiate()
get_parent().add_child(bala)
# Usamos la posición 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)
# Usamos la posición del Muzzle
bala.global_position = $Muzzle.global_positionResumen y Retos
Hoy has aprendido uno de los conceptos fundamentales del desarrollo de software: Instanciación (crear objetos dinámicamente).
Repasemos los conceptos clave:
- Escenas (
.tscn): Son los planos (moldes) de tus objetos. - Instanciar: Convertir esos planos en objetos vivos en el juego (
.instantiate()). - Nodos Marker2D: Puntos de referencia invisibles vitales para
spawns. - Señales: Cables invisibles para que los objetos hablen (
screen_exited). queue_free(): La forma segura de borrar basura de la memoria.
En el próximo capítulo, convertiremos este campo de tiro vacío en una batalla real. Crearemos Enemigos que gestionan su propia vida y detectan colisiones. ¡Prepara la munición!