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(tipoAudioStreamPlayer)
Configura:
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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
AudioManagera 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: activadoemitting: desactivado (lo activas por código)lifetime: 0.2–0.5amount: 10–30direction/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
| Elemento | Dónde vive | Cómo se dispara |
|---|---|---|
| Música | MusicPlayer (bus Music) | Al iniciar nivel / cambiar escena |
| SFX salto/recoger/daño | AudioManager (bus SFX) | Señales del jugador/ítems/enemigos |
| Evitar solapamientos | AudioManager | playing, cooldown, o players separados |
| Partículas | Escenas VFX instanciables | En el punto del evento (pickup/hit) |
| Sacudida cámara | Camera2D con script | Eventos de impacto (daño/explosión) |
| Flash/Pop | Jugador/Ítem | En la función de daño/recogida |