Saltar al contenido principal
Volver atrás

Godot #2: Instanciación y Balas

#godot #gdscript #instances #pooling

Aprende a crear objetos dinámicamente: instanciar balas, rotaciones y el concepto de Object Pooling para optimizar tu juego.

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?

  1. Instanciar Escenas: Usar preload y instantiate para crear balas.
  2. Scene Tree: Entender dónde poner esas balas (add_child) para que no se muevan con la nave.
  3. Local vs Global: Coordenadas para que la bala salga del cañón, no del origen del mundo.
  4. Limpieza: Usar VisibleOnScreenNotifier2D para 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:

Sprite de la Bala (Atlas)

Clic derecho -> Guardar imagen.
Créditos: Lunar Battle Pack por MattWalkden (CC0 1.0).

  1. Crea una Nueva Escena.

  2. Nodo raíz: Area2D (¡Ojo! No queremos físicas complejas como chocarse y rebotar, solo detectar si tocamos algo). Llámalo Bullet.

  3. Hijo: CollisionShape2D (con forma de rectángulo o cápsula pequeña que cubra el sprite).

  4. 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> -> Selecciona New SpriteFrames.
    • Haz clic de nuevo sobre el texto SpriteFrames que 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.

    Rejilla de SpriteFrames

    • 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.
  5. 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:

  1. Añade un script a la bala (bullet.gd) si no lo has hecho.
  2. Selecciona el nodo VisibleOnScreenNotifier2D.
  3. Ve a la pestaña Node (a la derecha, junto al Inspector).
  4. Haz doble clic en la señal screen_exited.
  5. Dale a Connect.
  6. Godot escribirá automáticamente una función llamada _on_visible_on_screen_notifier_2d_screen_exited en 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:

gdscript
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:

  1. Marcas el objeto para borrar.
  2. Godot espera a terminar de dibujar el fotograma actual (Frame).
  3. 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:

  1. Cargar el Plano: preload("res://bullet.tscn").
  2. Instanciar: plano.instantiate(). Construyes el objeto en la memoria RAM, pero aún no existe en el mundo visible.
  3. 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:

  1. if (Condicional): “SI pasa esto… haz esto otro”.
  2. Input: En el capítulo anterior usamos get_vector. Ahora usaremos is_action_pressed (“¿Está la tecla mantenida?”).

Configuración Obligatoria (Input Map): Igual que hicimos con WASD:

  1. Ve a Project > Project Settings > Input Map.
  2. Añade una nueva acción llamada shoot.
  3. 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:

gdscript
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.

gdscript
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_pressed devuelve true mientras mantengas la tecla pulsada. Si quisieras disparar solo una vez por clic (semiautomático), usarías is_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.

gdscript
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:

gdscript
@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

4.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”.

gdscript
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.

gdscript
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_position

Código Final: Player.gd

Si juntamos todas las piezas, así debe quedar tu script completo y funcional:

gdscript
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_position

Ahora 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”.

  1. Abre la escena de Player y añade un nodo hijo Marker2D al nodo principal (también llamado Player).
  2. Llámalo Muzzle (Boquilla).
  3. 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:

gdscript
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_position

Resumen y Retos

Hoy has aprendido uno de los conceptos fundamentales del desarrollo de software: Instanciación (crear objetos dinámicamente).

Repasemos los conceptos clave:

  1. Escenas (.tscn): Son los planos (moldes) de tus objetos.
  2. Instanciar: Convertir esos planos en objetos vivos en el juego (.instantiate()).
  3. Nodos Marker2D: Puntos de referencia invisibles vitales para spawns.
  4. Señales: Cables invisibles para que los objetos hablen (screen_exited).
  5. 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!