Audio y retroalimentación para un juego 2D en Godot

Capítulo 11

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

Por qué el audio y el feedback cambian la “sensación” del juego

En un juego 2D, el audio y la retroalimentación visual (VFX) convierten acciones simples (saltar, recoger, recibir daño) en eventos “con peso”. En Godot, lo habitual es separar: efectos de sonido (SFX) para acciones cortas, música en bucle para ambientación, y feedback visual (partículas, destellos, sacudida de cámara) para reforzar impacto y claridad.

Objetivo del capítulo

  • Reproducir SFX con AudioStreamPlayer (salto, recoger, daño) y música de fondo.
  • Controlar volumen (por buses) y evitar solapamientos molestos.
  • Disparar audio mediante señales y eventos del juego (arquitectura limpia).
  • Añadir mejoras de sensación: partículas simples, sacudida leve de cámara (Camera2D) y pequeños efectos visuales al recoger o recibir daño.

Configuración de audio: buses, música y SFX

1) Crea buses de audio (Master / Music / SFX)

En el panel Audio (bus layout), crea dos buses debajo de Master: Music y SFX. Así podrás controlar volumen por categoría sin tocar cada sonido.

  • Master: volumen global.
  • Music: música de fondo.
  • SFX: efectos (salto, recoger, daño).

En cada AudioStreamPlayer asigna el bus adecuado en la propiedad bus.

2) Nodo de música (un solo reproductor, en bucle)

Crea un nodo dedicado, por ejemplo en una escena de “Audio” o dentro de tu escena principal:

  • MusicPlayer (tipo AudioStreamPlayer)

Configura:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

  • stream: tu pista de música.
  • bus: Music.
  • autoplay: opcional (o lo controlas por código).
  • loop: si el recurso lo permite (según tipo de stream), o activa loop en el import/propiedades del audio.
# MusicPlayer.gd (adjunto a MusicPlayer) - opcional si quieres controlarlo por código
extends AudioStreamPlayer

func play_music(stream_res: AudioStream, from_position := 0.0) -> void:
	if stream != stream_res:
		stream = stream_res
	play(from_position)

SFX con AudioStreamPlayer: salto, recoger, daño

Enfoque recomendado: un “AudioManager” central

En lugar de poner un AudioStreamPlayer por cada escena, crea un gestor que reciba eventos (señales) y reproduzca sonidos. Ventajas: volumen centralizado, prevención de solapamientos, y menos dependencias entre escenas.

1) Crea una escena/nodo AudioManager

Estructura sugerida:

  • AudioManager (Node)
    • SFXPlayer (AudioStreamPlayer)
    • DamagePlayer (AudioStreamPlayer)
    • PickupPlayer (AudioStreamPlayer)
    • JumpPlayer (AudioStreamPlayer)

Asigna a todos el bus SFX. Puedes usar un solo player para todo, pero separar por tipo ayuda a evitar que un sonido “corte” a otro (por ejemplo, daño no debería interrumpir un pickup).

2) Script del AudioManager con prevención de solapamientos

Para evitar “spam” (por ejemplo, recoger muchos ítems en un segundo), aplica una pequeña regla: no reiniciar el mismo sonido si ya está sonando, o imponer un cooldown.

# AudioManager.gd
extends Node

@onready var jump_player: AudioStreamPlayer = $JumpPlayer
@onready var pickup_player: AudioStreamPlayer = $PickupPlayer
@onready var damage_player: AudioStreamPlayer = $DamagePlayer

# Streams (puedes asignarlos desde el inspector con export)
@export var sfx_jump: AudioStream
@export var sfx_pickup: AudioStream
@export var sfx_damage: AudioStream

var _last_pickup_time := 0.0
var _pickup_cooldown := 0.05

func play_jump() -> void:
	if sfx_jump == null:
		return
	# Evita reiniciar si ya está sonando (opcional)
	if jump_player.playing:
		return
	jump_player.stream = sfx_jump
	jump_player.play()

func play_pickup() -> void:
	if sfx_pickup == null:
		return
	var now := Time.get_ticks_msec() / 1000.0
	if now - _last_pickup_time < _pickup_cooldown:
		return
	_last_pickup_time = now
	pickup_player.stream = sfx_pickup
	pickup_player.play()

func play_damage() -> void:
	if sfx_damage == null:
		return
	# El daño suele ser importante: permite reiniciar para que se note
	damage_player.stream = sfx_damage
	damage_player.play()

3) Haz el AudioManager accesible

Dos opciones comunes:

  • Autoload (Singleton): añade AudioManager a Autoload para llamarlo desde cualquier parte: AudioManager.play_jump().
  • Inyectar referencia: pasar una referencia a escenas que lo necesiten (más estricto, menos global).

Para un curso práctico, Autoload suele ser lo más directo.

Control de volumen (buses) y sliders

1) Ajustar volumen por bus desde código

Godot controla volumen en dB. Un slider (0..1) suele mapearse a dB con una conversión para que el cambio sea perceptualmente suave.

# AudioSettings.gd (puede estar en un menú)
extends Node

func set_bus_volume_linear(bus_name: String, linear: float) -> void:
	linear = clamp(linear, 0.0, 1.0)
	var bus_index := AudioServer.get_bus_index(bus_name)
	# Convierte 0..1 a dB (0 => -80dB aprox, 1 => 0dB)
	var db := linear_to_db(linear)
	AudioServer.set_bus_volume_db(bus_index, db)
	AudioServer.set_bus_mute(bus_index, linear <= 0.001)

2) Ejemplo con UI (HSlider)

Si tienes un HSlider para música y otro para SFX, conecta su señal value_changed:

# En el script del menú
func _on_music_slider_value_changed(value: float) -> void:
	set_bus_volume_linear("Music", value)

func _on_sfx_slider_value_changed(value: float) -> void:
	set_bus_volume_linear("SFX", value)

Consejo: guarda estos valores en un recurso/archivo de configuración para persistencia, pero aquí nos centramos en el control en tiempo real.

Disparar audio con señales y eventos del juego

Idea clave: el jugador “emite”, el AudioManager “reproduce”

En vez de que el jugador llame directamente a AudioManager, el jugador emite señales como jumped o took_damage. Luego, un nodo coordinador (o el propio AudioManager) se conecta a esas señales. Esto reduce acoplamiento y facilita cambiar sonidos sin tocar la lógica.

1) Señales en el jugador

# Player.gd
extends CharacterBody2D

signal jumped
signal picked_item
signal took_damage

func do_jump() -> void:
	# ... tu lógica de salto
	emit_signal("jumped")

func on_item_collected() -> void:
	emit_signal("picked_item")

func apply_damage(amount: int) -> void:
	# ... tu lógica de vida
	emit_signal("took_damage")

2) Conectar señales al AudioManager

En una escena principal (por ejemplo, Game) conecta en _ready():

# Game.gd
extends Node

@onready var player = $Player

func _ready() -> void:
	player.jumped.connect(_on_player_jumped)
	player.picked_item.connect(_on_player_picked_item)
	player.took_damage.connect(_on_player_took_damage)

func _on_player_jumped() -> void:
	AudioManager.play_jump()

func _on_player_picked_item() -> void:
	AudioManager.play_pickup()

func _on_player_took_damage() -> void:
	AudioManager.play_damage()

Este patrón también aplica a enemigos, cofres, puertas, etc.: emiten señales y el sistema de audio responde.

Mejoras de sensación: partículas simples

Partículas al recoger ítems

Para un efecto rápido, usa GPUParticles2D (o CPUParticles2D si lo prefieres). Crea una escena reutilizable:

  • PickupVFX (Node2D)
    • Particles (GPUParticles2D)

Configura en el inspector:

  • one_shot: activado
  • emitting: desactivado (lo activas por código)
  • lifetime: 0.2–0.5
  • amount: 10–30
  • direction/spread: para dispersión
# PickupVFX.gd
extends Node2D

@onready var particles: GPUParticles2D = $Particles

func play() -> void:
	particles.restart()
	particles.emitting = true
	# Autodestruir cuando termine
	var t := get_tree().create_timer(particles.lifetime)
	await t.timeout
	queue_free()

Cuando el ítem se recoge, instancia el VFX en la posición del ítem:

# Item.gd (cuando se recoge)
@export var pickup_vfx_scene: PackedScene

func collect() -> void:
	if pickup_vfx_scene:
		var vfx = pickup_vfx_scene.instantiate()
		get_tree().current_scene.add_child(vfx)
		vfx.global_position = global_position
		vfx.play()
	queue_free()

Partículas o destello al recibir daño

Repite el patrón con una escena HitVFX (partículas rojas, pequeñas chispas, etc.) y ejecútala en la posición del jugador cuando emita took_damage.

Sacudida leve de cámara con Camera2D

1) Preparar la cámara

Asegúrate de tener una Camera2D siguiendo al jugador (o como cámara principal). La sacudida se logra aplicando un offset temporal.

2) Script simple de “camera shake”

Adjunta este script a tu Camera2D:

# CameraShake2D.gd
extends Camera2D

var _shake_time := 0.0
var _shake_duration := 0.0
var _shake_strength := 0.0
var _base_offset := Vector2.ZERO

func _ready() -> void:
	_base_offset = offset

func shake(duration: float = 0.12, strength: float = 4.0) -> void:
	_shake_duration = max(_shake_duration, duration)
	_shake_time = _shake_duration
	_shake_strength = max(_shake_strength, strength)

func _process(delta: float) -> void:
	if _shake_time > 0.0:
		_shake_time -= delta
		var t := _shake_time / _shake_duration
		var current_strength := _shake_strength * t
		offset = _base_offset + Vector2(
			randf_range(-current_strength, current_strength),
			randf_range(-current_strength, current_strength)
		)
	else:
		offset = _base_offset
		_shake_strength = 0.0
		_shake_duration = 0.0

3) Disparar la sacudida por evento

Conecta el evento de daño del jugador para que además del sonido, sacuda la cámara:

# Game.gd
@onready var camera: Camera2D = $Camera2D

func _on_player_took_damage() -> void:
	AudioManager.play_damage()
	camera.shake(0.12, 5.0)

Usa valores pequeños: demasiada sacudida marea y resta precisión.

Pequeños efectos visuales al recoger o recibir daño

1) “Flash” rápido del sprite al recibir daño

Un efecto común es un parpadeo (blanco/rojo) breve. Puedes hacerlo con un ShaderMaterial o, más simple, modulando el color del Sprite2D.

# En Player.gd
@onready var sprite: Sprite2D = $Sprite2D

func damage_flash() -> void:
	var original := sprite.modulate
	sprite.modulate = Color(1, 0.6, 0.6)
	await get_tree().create_timer(0.08).timeout
	sprite.modulate = original

Llama a damage_flash() cuando se aplique daño (o al recibir la señal).

2) “Pop” visual al recoger (escala breve)

Para que recoger se sienta satisfactorio, puedes animar un “pop” rápido del ítem antes de desaparecer (o del HUD). Con Tween:

# Item.gd
func pop_and_collect() -> void:
	var tw := create_tween()
	tw.tween_property(self, "scale", scale * 1.2, 0.06).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
	tw.tween_property(self, "scale", Vector2.ZERO, 0.06)
	await tw.finished
	collect()

Checklist práctico: integrar todo sin desorden

ElementoDónde viveCómo se dispara
MúsicaMusicPlayer (bus Music)Al iniciar nivel / cambiar escena
SFX salto/recoger/dañoAudioManager (bus SFX)Señales del jugador/ítems/enemigos
Evitar solapamientosAudioManagerplaying, cooldown, o players separados
PartículasEscenas VFX instanciablesEn el punto del evento (pickup/hit)
Sacudida cámaraCamera2D con scriptEventos de impacto (daño/explosión)
Flash/PopJugador/ÍtemEn la función de daño/recogida

Ahora responde el ejercicio sobre el contenido:

¿Cuál es el beneficio principal de usar un AudioManager central con varios AudioStreamPlayer separados por tipo (salto, recoger, daño) en un juego 2D?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Un gestor central permite manejar SFX de forma ordenada, controlar volumen por categoría y evitar que sonidos se interrumpan o se “spameen” usando players separados, comprobaciones de playing o cooldown.

Siguiente capítulo

Exportación del juego 2D en Godot y verificación final

Arrow Right Icon
Portada de libro electrónico gratuitaGodot desde Cero: Crea tu Primer Juego 2D con GDScript
92%

Godot desde Cero: Crea tu Primer Juego 2D con GDScript

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.