Ensamblar sistemas de gameplay como “bloques”
Una mecánica de gameplay rara vez es un solo Blueprint: normalmente es un conjunto de sistemas que colaboran (salud/daño, energía, inventario/contador, condiciones de victoria/derrota y UI). La clave para que el proyecto escale es separar responsabilidades y reutilizar lógica mediante Actor Components. Un Actor (Jugador, Enemigo, Hazard) “tiene” componentes; cada componente encapsula una parte del comportamiento y expone eventos/funciones para integrarse con el resto.
Objetivo del capítulo
- Construir un sistema de Salud/Daño reutilizable con un Actor Component.
- Crear un hazard/enemigo que aplica daño por contacto (instantáneo o por intervalo).
- Reflejar la vida en un HUD y actualizarlo mediante eventos, sin depender de Tick.
- Integrar un contador simple (inventario/coleccionables) y condiciones de victoria/derrota basadas en eventos.
Diseño con Actor Components: separar responsabilidades
Piensa en un componente como una “caja negra” con:
- Estado: variables internas (por ejemplo, VidaActual).
- API: funciones públicas (ApplyDamage, Heal, GetHealthPercent).
- Eventos: dispatchers que notifican cambios (OnHealthChanged, OnDeath).
Ventajas prácticas:
- Reutilizas el mismo componente en jugador, enemigos y objetos destructibles.
- La UI no necesita “preguntar” cada frame; se actualiza cuando ocurre un cambio.
- Las reglas de victoria/derrota se conectan a eventos (muerte, contador completado).
Componente reutilizable: BP_HealthComponent
1) Crear el Actor Component
Crea un Blueprint de tipo Actor Component llamado BP_HealthComponent.
2) Variables recomendadas
MaxHealth(Float, default 100)CurrentHealth(Float)bIsDead(Bool)InvincibilityTime(Float, opcional)LastDamageTime(Float, opcional)
En BeginPlay del componente, inicializa CurrentHealth = MaxHealth y bIsDead = false.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
3) Event Dispatchers (para UI y gameplay)
Agrega dos dispatchers:
OnHealthChangedcon parámetros:NewHealth(Float),Delta(Float),InstigatorActor(Actor)OnDeathcon parámetros:KillerActor(Actor)
4) Funciones públicas
Crea estas funciones en el componente:
ApplyDamage(DamageAmount: float, InstigatorActor: Actor)Heal(HealAmount: float)GetHealthPercent() -> float
5) Implementación de ApplyDamage (paso a paso)
- Si
bIsDeades true, salir. - Clamp del daño:
Damage = Max(DamageAmount, 0). - Calcular nueva vida:
NewHealth = Clamp(CurrentHealth - Damage, 0, MaxHealth). Delta = NewHealth - CurrentHealth(será negativo).- Asignar
CurrentHealth = NewHealth. - Lanzar
OnHealthChangedpasandoCurrentHealth,DeltaeInstigatorActor. - Si
CurrentHealth <= 0ybIsDeadbIsDead = true y lanzarOnDeath.
En Blueprints, esto se traduce a una secuencia de nodos con Branch, Float - Float, Clamp (float) y llamadas a Call OnHealthChanged / Call OnDeath.
6) Implementación de Heal
- Si
bIsDeades true, decidir si permites revivir (en principiantes: no). NewHealth = Clamp(CurrentHealth + HealAmount, 0, MaxHealth)Delta = NewHealth - CurrentHealth(positivo)- Asignar y disparar
OnHealthChanged(InstigatorActor puede ser null o el dueño).
7) GetHealthPercent
Devuelve CurrentHealth / MaxHealth (maneja MaxHealth == 0 devolviendo 0).
Integración en el jugador: sin Tick para UI
1) Añadir el componente al jugador
En el Blueprint del jugador (por ejemplo BP_PlayerCharacter), agrega el componente BP_HealthComponent desde el panel de Components.
2) Crear el HUD (Widget) con barra de vida
Crea un Widget Blueprint WBP_HUD con:
- Un
ProgressBarllamadoPB_Health. - (Opcional) Un
TextBlockpara mostrar valores numéricos.
En el jugador o PlayerController, crea el widget en BeginPlay y añádelo al viewport (esto ya lo habrás hecho en capítulos anteriores; aquí nos enfocamos en la actualización por eventos).
3) Conectar eventos del componente a la UI
Hay dos patrones comunes. El más directo para principiantes: el jugador crea el HUD y se suscribe a los eventos del componente.
- En
BP_PlayerCharacter, guarda una referencia aWBP_HUD(variableHUDRef). - Tras crear el widget, obtén referencia al
HealthComponent. - Usa
Bind Event to OnHealthChangeddel componente. - Crea un
Custom Event(por ejemploHandleHealthChanged) con la misma firma:NewHealth,Delta,InstigatorActor. - Dentro de
HandleHealthChanged, calcula el porcentaje:NewHealth / MaxHealth(puedes llamar aGetHealthPercent) y llama una función del widget (por ejemploSetHealthPercent).
En el widget WBP_HUD, crea una función SetHealthPercent(Percent: float) que haga PB_Health.SetPercent(Percent). Así, la UI se actualiza únicamente cuando cambia la vida, sin Tick.
4) Manejar muerte (derrota) por evento
En el jugador, también enlaza OnDeath:
Bind Event to OnDeath→HandlePlayerDeath- En
HandlePlayerDeath: deshabilita input, reproduce animación/efecto, y notifica al sistema de GameMode/Manager que la partida terminó en derrota.
Caso práctico: hazard/enemigo que aplica daño por contacto o por intervalo
Construiremos un Actor llamado BP_DamageHazard (puede ser una trampa, fuego, ácido, etc.). La lógica será configurable: daño instantáneo al entrar o daño periódico mientras permanezca dentro.
1) Crear BP_DamageHazard
Componentes sugeridos:
StaticMesh(visual)BoxCollisionoCapsuleCollision(zona de daño)
Variables:
DamageAmount(Float, por ejemplo 10)bDamageOverTime(Bool)DamageInterval(Float, por ejemplo 1.0)ActorsInside(Array de Actor, opcional si quieres soportar múltiples)DamageTimerHandle(TimerHandle)
2) Aplicar daño instantáneo (al entrar)
- En el
BoxCollision, usa el eventoOnComponentBeginOverlap. - Del
OtherActor, intenta obtener el componente de salud:Get Component by Class (BP_HealthComponent). - Si existe, llama
ApplyDamage(DamageAmount, Self).
Esto hace que cualquier actor con BP_HealthComponent sea compatible (jugador, enemigo, NPC), sin hardcodear clases.
3) Daño por intervalo (mientras permanece dentro) usando Timers
Para daño periódico, evita Tick y usa un Timer.
- En
OnComponentBeginOverlap:
- Si
bDamageOverTimees true, agregaOtherActoraActorsInside(si no está). - Si el timer no está activo, inicia un timer repetitivo:
Set Timer by Function NameoSet Timer by Eventcon intervaloDamageInterval.
- Crea una función/evento
DealDamageTickque:
- Itere
ActorsInside. - Para cada actor válido, obtenga
BP_HealthComponenty llameApplyDamage(DamageAmount, Self). - Remueva actores inválidos (destroyed) del array.
- En
OnComponentEndOverlap:
- Remueve
OtherActordeActorsInside. - Si el array queda vacío, limpia el timer:
Clear and Invalidate Timer.
Con esto, el hazard aplica daño cada X segundos solo cuando hay alguien dentro, sin costo constante por frame.
4) Variante: enemigo que daña por contacto con cooldown
Si quieres que el enemigo aplique daño al tocar pero con enfriamiento (para no “derretir” al jugador en un solo frame), puedes:
- Guardar un
Map<Actor, float>de último tiempo de daño (o un array paralelo). - Al overlap, comparar
GetGameTimeInSecondscon el último daño y aplicar si pasó el cooldown.
En proyectos principiantes, el enfoque de timer por intervalo suele ser más simple y legible.
Contador simple (inventario mínimo) y condiciones de victoria
Un “inventario simple” para principiantes suele ser un contador de objetos recolectados (monedas, llaves, piezas). La idea es la misma que con salud: estado + evento.
1) Componente BP_CollectibleCounterComponent (opcional pero recomendado)
Crea un Actor Component BP_CollectibleCounterComponent con:
CurrentCount(Int)TargetCount(Int)- Dispatcher
OnCountChanged(NewCount:int) - Dispatcher
OnTargetReached()
Función Add(int Amount):
CurrentCount += Amount- Disparar
OnCountChanged - Si
CurrentCount >= TargetCountdispararOnTargetReached(una sola vez si quieres, usando un bool).
2) Recolectable BP_Collectible
En el actor recolectable:
- Al ser recogido, obtiene del jugador el componente
BP_CollectibleCounterComponenty llamaAdd(1). - Luego se destruye o se desactiva.
3) UI del contador sin Tick
En el jugador, enlaza OnCountChanged para actualizar un TextBlock del HUD (por ejemplo “3/10”). Igual que con salud: la UI se actualiza por evento.
4) Condición de victoria
En lugar de revisar constantemente si ya se llegó al objetivo, enlaza OnTargetReached a una acción de victoria:
- Mostrar pantalla de victoria
- Deshabilitar input
- Detener spawns o timers
Condiciones de derrota: salud a cero
La derrota se activa naturalmente desde BP_HealthComponent con OnDeath. Esto evita que el GameMode tenga que “preguntar” cada frame por la vida del jugador.
Patrón recomendado: un “manager” escucha eventos
Si quieres mantener el jugador limpio, puedes crear un actor BP_GameplayManager (o usar GameMode) que:
- Obtiene referencia al jugador al iniciar.
- Se suscribe a
HealthComponent.OnDeathyCounterComponent.OnTargetReached. - Decide si termina en victoria o derrota.
Así, el jugador solo se ocupa de ser jugador; el manager se ocupa de reglas de partida.
Tabla de “quién hace qué” (responsabilidades)
| Elemento | Responsabilidad | Cómo se comunica |
|---|---|---|
| BP_HealthComponent | Gestionar vida, aplicar daño/curación, emitir cambios y muerte | Dispatchers OnHealthChanged / OnDeath |
| BP_DamageHazard | Detectar presencia y aplicar daño (instantáneo o por intervalo) | Llama ApplyDamage en el componente del otro actor |
| WBP_HUD | Mostrar vida y contador | Funciones SetHealthPercent / SetCountText llamadas desde eventos |
| BP_CollectibleCounterComponent | Contar objetos y emitir cambios/objetivo cumplido | Dispatchers OnCountChanged / OnTargetReached |
| Manager/GameMode | Reglas de victoria/derrota | Se suscribe a eventos y ejecuta fin de partida |
Checklist de depuración (cuando “no funciona”)
- ¿El actor que recibe daño realmente tiene
BP_HealthComponentagregado? - ¿El hazard está llamando
Get Component by ClasssobreOtherActor(no sobre sí mismo)? - ¿El Timer se inicia solo cuando hay actores dentro y se limpia al salir?
- ¿El HUD está guardado en una variable (referencia válida) antes de llamar funciones?
- ¿Los eventos están correctamente “Bind” (suscritos) una sola vez (por ejemplo en BeginPlay)?
- ¿La barra de vida usa valores 0..1 (Percent) y no 0..100?