Unreal Engine: Creación de un Sistema de Eventos Globales y Gestor de Misiones con Blueprint
Este tutorial te guiará paso a paso en la creación de un robusto sistema de eventos globales y un gestor de misiones dinámico en Unreal Engine 5 utilizando únicamente Blueprints. Aprenderás a desacoplar la lógica de tu juego, mejorar la escalabilidad y facilitar la adición de nuevas características sin modificar código existente.
🚀 Introducción al Sistema de Eventos Globales y Gestor de Misiones
En el desarrollo de videojuegos, especialmente en títulos con narrativa compleja o un mundo abierto, la gestión de eventos y misiones puede volverse rápidamente abrumadora. Un sistema de eventos globales desacoplado es crucial para mantener tu código organizado, escalable y fácil de mantener. Junto a este, un gestor de misiones robusto nos permitirá crear experiencias de juego dinámicas y ricas en contenido sin enredarnos en dependencias. En este tutorial, nos enfocaremos en Unreal Engine 5 y utilizaremos exclusivamente Blueprints para construir estas dos potentes herramientas.
¿Por qué un Sistema de Eventos Globales? 🤔
Un sistema de eventos globales permite que diferentes partes de tu juego se comuniquen entre sí sin tener un conocimiento directo de la existencia del otro. Imagina que un enemigo muere y necesitas notificar al contador de misiones, al sistema de experiencia del jugador y quizás a un efecto visual en la interfaz de usuario. Sin un sistema de eventos, tendrías que crear referencias directas entre todos estos actores, lo que genera un código fuertemente acoplado y difícil de modificar. Con un sistema de eventos, el enemigo simplemente 'dispara' un evento 'EnemigoMuerto', y cualquier otro sistema interesado en ese evento simplemente lo 'escucha' y reacciona.
¿Por qué un Gestor de Misiones Dinámico? 🎯
Un gestor de misiones dinámico es el cerebro detrás de las tareas y objetivos que el jugador debe completar. Nos permite definir misiones con múltiples objetivos, seguir su progreso, actualizar la interfaz de usuario y recompensar al jugador. Un enfoque dinámico significa que podemos añadir, modificar o eliminar misiones fácilmente sin tener que recompilar o reestructurar gran parte del juego. Esto es especialmente útil para juegos con contenido post-lanzamiento o actualizaciones frecuentes.
🛠️ Configuración Inicial del Proyecto
Antes de sumergirnos en la lógica, asegúrate de tener un proyecto de Unreal Engine 5 abierto. Puedes usar una plantilla en blanco o de terceros. No necesitaremos ningún plugin externo para este tutorial.
Creación de Carpetas Esenciales 📂
Para mantener la organización, crearemos algunas carpetas clave en la carpeta Content:
Blueprints/CoreBlueprints/MissionSystemBlueprints/EventsDataUI(si planeas una interfaz de usuario para las misiones, aunque no la cubriremos en profundidad aquí)
⚡ Construyendo el Sistema de Eventos Globales (Event Dispatcher Centralizado)
Nuestro sistema de eventos se basará en un Blueprint Function Library y una Interface Blueprint para permitir que cualquier actor 'dispare' o 'escuche' eventos. Esto asegura un desacoplamiento máximo.
Paso 1: Interfaz de Eventos (BPI_EventInterface) 🔌
Crearemos una interfaz que cualquier actor que quiera disparar eventos pueda implementar. Esto nos permite llamar a funciones genéricas en esos actores.
-
En
Blueprints/Events, clic derecho ->Blueprint Class->All Classes, buscaBlueprintImplementableInterface. NómbraloBPI_EventInterface. -
Abre
BPI_EventInterface. -
Añade una función:
Execute_NotifyEvent.- Input:
EventName(Name),EventData(Generic Object Reference).
📌 Nota: Usamos `Generic Object Reference` para `EventData` porque nos permite pasar *cualquier* tipo de dato como payload del evento. Luego, al recibirlo, podemos intentar 'cast' a la clase esperada. - Input:
Paso 2: Blueprint Function Library para Eventos (BFL_GlobalEvents) 📖
Este será nuestro corazón del sistema de eventos. Contendrá un Event Dispatcher central que puede ser accedido globalmente.
- En
Blueprints/Events, clic derecho ->Blueprint Class->All Classes, buscaBlueprintFunctionLibrary. NómbraloBFL_GlobalEvents. - Abre
BFL_GlobalEvents. - En la pestaña
Graph(si no está, ve aFile->New Blueprint->Blueprint Function Library), necesitamos una forma de almacenar elEvent Dispatcherde manera persistente y global. Los Function Libraries no tienen estados. La mejor manera de lograr esto es utilizando unGame Instanceo unSubsystem. Optaremos por unSubsystemya que es más moderno y adecuado para servicios globales.
Paso 3: Global Event Subsystem (G_EventSubsystem) ✨
Este Subsystem contendrá el Event Dispatcher real y gestionará las suscripciones y disparos de eventos.
- En
Blueprints/Core, clic derecho ->Blueprint Class->All Classes, buscaGameInstanceSubsystem. NómbraloG_EventSubsystem. - Abre
G_EventSubsystem. - En la pestaña
Variables, añade una nueva variable:- Nombre:
OnGlobalEventTriggered - Tipo:
Event Dispatcher. - Haz clic en el icono de
Pinpara que sea Exposed on Spawn (aunque no lo spawnaremos directamente, es buena práctica). También asegúrate de que seaPublic(ojo, esto es por la forma en que Blueprint maneja los Event Dispatchers, no necesariamente por ser una buena práctica de encapsulación).
- Nombre:
- Dentro de
OnGlobalEventTriggered(en la pestañaDetailsde la variable), añade los siguientesInputs:EventName(Name)EventData(Object Reference)
- Ahora, en el
G_EventSubsystem, crearemos dos funciones para interactuar con este dispatcher de manera más limpia:RegisterForGlobalEvent(Function)- Inputs:
Listener(Object Reference),EventNameToListen(Name) - Implementación: Arrastra
OnGlobalEventTriggeredal gráfico, seleccionaBind Event to OnGlobalEventTriggered. Conecta elListeneral nodo de ejecución y alTargetdelBind Event. Crea unCustom Eventpara conectar alEventdelBind Event, y en esteCustom Eventañade los inputsEventName(Name) yEventData(Object Reference). EsteCustom Eventserá el que se ejecute cuando se dispare el evento global. En esteCustom Event, deberás llamar a la funciónExecute_NotifyEventdelListener(usando la interfazBPI_EventInterface).
- Inputs:
// G_EventSubsystem: Function: RegisterForGlobalEvent
// Inputs: Listener (Object Reference), EventNameToListen (Name)
// Graph:
// -> OnGlobalEventTriggered (Bind Event to)
// -> Target: Listener
// -> Event: CustomEvent_HandleGlobalEvent
//
// CustomEvent_HandleGlobalEvent (Inputs: EventName (Name), EventData (Object Reference))
// -> Branch (Condition: EventName == EventNameToListen)
// -> True:
// -> Cast To BPI_EventInterface (Target: Listener)
// -> Success:
// -> Execute_NotifyEvent (Target: BPI_EventInterface, EventName: EventName, EventData: EventData)
* **`UnregisterFromGlobalEvent` (Function)**
* **Inputs**: `Listener` (Object Reference), `EventNameToListen` (Name)
* **Implementación**: Similar a `Register`, pero usa `Unbind Event`.
// G_EventSubsystem: Function: UnregisterFromGlobalEvent
// Inputs: Listener (Object Reference), EventNameToListen (Name)
// Graph:
// -> OnGlobalEventTriggered (Unbind Event from)
// -> Target: Listener
// -> Event: CustomEvent_HandleGlobalEvent (the same custom event as above)
* **`TriggerGlobalEvent` (Function)**
* **Inputs**: `EventName` (Name), `EventData` (Object Reference)
* **Implementación**: Arrastra `OnGlobalEventTriggered` al gráfico, selecciona `Call OnGlobalEventTriggered`. Conecta los inputs.
// G_EventSubsystem: Function: TriggerGlobalEvent
// Inputs: EventName (Name), EventData (Object Reference)
// Graph:
// -> OnGlobalEventTriggered (Call)
// -> EventName: EventName
// -> EventData: EventData
Paso 4: Configuración del Game Instance ⚙️
Para que nuestro G_EventSubsystem funcione, debemos asegurarnos de que el GameInstance de nuestro proyecto lo reconozca.
- Ve a
Project Settings->Maps & Modes. - En la sección
Game Instance, asegúrate de que tienes unGame Instance Classpersonalizado o crea uno. Si no tienes uno, crea un nuevoBlueprint Classde tipoGameInstanceenBlueprints/Core(ej.BP_MyGameInstance) y configúralo aquí. - No necesitamos hacer nada más en el
Game Instancesi solo usamos Subsystems, ya que Unreal los gestiona automáticamente.
Paso 5: Cómo Usar el Sistema de Eventos Globales 🧑💻
Para disparar un evento:
Cualquier actor que necesite disparar un evento (por ejemplo, un enemigo al morir) hará lo siguiente:
- En su Blueprint, añade
Get Game Instance Subsystemy buscaG_EventSubsystem. - Llama a
TriggerGlobalEventen el Subsystem. - Pasa el
EventName(ej.EnemyDied) y elEventData(ej. una referencia al enemigo que murió, o su ID, o un objeto de datos custom).
// Example: Enemy Blueprint on Death
// -> Get Game Instance Subsystem (Class: G_EventSubsystem)
// -> TriggerGlobalEvent (EventName: EnemyDied, EventData: Self (reference to enemy))
Para escuchar un evento:
Cualquier actor que necesite reaccionar a un evento (por ejemplo, el gestor de misiones, el HUD) hará lo siguiente:
- En su Blueprint, en
Class Settings, añade la interfazBPI_EventInterface. - Implementa el evento
NotifyEvent(clic derecho en el eventoNotifyEventen la pestañaMy Blueprinty seleccionaImplement Event). - En
BeginPlay(o cuando sea apropiado), llama aGet Game Instance SubsystemyRegisterForGlobalEventen el Subsystem, pasandoSelfcomoListenery elEventNameque quieres escuchar (ej.EnemyDied). - Dentro del evento
NotifyEvent, puedes usar unSwitch on Namecon elEventNamepara manejar diferentes tipos de eventos, y luegoCastelEventDataa la clase esperada.
// Example: Mission Manager Blueprint (on BeginPlay)
// -> Get Game Instance Subsystem (Class: G_EventSubsystem)
// -> RegisterForGlobalEvent (Listener: Self, EventNameToListen: EnemyDied)
// Example: Mission Manager Blueprint (on Event NotifyEvent)
// -> Switch on Name (EventName)
// -> Case: EnemyDied
// -> Cast To BP_Enemy (Target: EventData)
// -> Success: Handle enemy death logic
📋 Desarrollando el Gestor de Misiones Dinámico
Ahora que tenemos un sistema de eventos robusto, podemos construir nuestro gestor de misiones sobre él. Usaremos Data Assets para definir nuestras misiones y un Actor Component para gestionar el progreso del jugador en las misiones.
Paso 1: Definición de la Estructura de Datos de la Misión (Structs) 🏗️
Necesitaremos structs para definir los objetivos y la misión en sí.
-
En
Data, clic derecho ->Blueprints->Structure. NómbraloFQuestObjective.- Variables:
ObjectiveID(Name): Identificador único del objetivo (ej.Kill5Goblins).Description(Text): 'Mata 5 Goblins'.CurrentProgress(Integer): Progreso actual del objetivo.RequiredProgress(Integer): Progreso necesario para completar el objetivo.bIsComplete(Boolean): ¿Está completado este objetivo?CompletionEvent(Name): El evento global que completa este objetivo (ej.EnemyDied,ItemCollected).
- Variables:
-
En
Data, clic derecho ->Blueprints->Structure. NómbraloFQuestReward.- Variables:
RewardType(Name): 'XP', 'Gold', 'Item'.Amount(Integer).ItemID(Name) (Opcional, siRewardTypees 'Item').
- Variables:
-
En
Data, clic derecho ->Blueprints->Structure. NómbraloFQuestData.- Variables:
QuestID(Name): Identificador único de la misión.QuestName(Text).QuestDescription(Text).QuestObjectives(Array ofFQuestObjective).QuestRewards(Array ofFQuestReward).bIsMainQuest(Boolean).NextQuestID(Name): Opcional, para encadenar misiones.
- Variables:
Paso 2: Base de Datos de Misiones (Quest Data Asset) 💾
Utilizaremos Data Assets para crear nuestras misiones como activos individuales, fáciles de gestionar.
-
En
Data, clic derecho ->Miscellaneous->Data Asset. -
En el selector de clase, busca
PrimaryDataAsset. NómbraloDA_QuestBase. -
Abre
DA_QuestBasey añade una variable:- Nombre:
QuestData - Tipo:
FQuestData. - Haz clic en el icono de
Eyepara que sea Editable.
- Nombre:
-
Ahora, para crear misiones reales, clic derecho en
DA_QuestBase->Create Child Blueprint Class. Puedes nombrarlosDA_Quest_KillGoblins,DA_Quest_FetchItem, etc.- Abre cada uno de estos hijos y rellena su variable
QuestDatacon los detalles de la misión.
🔥 Importante: Usar Data Assets es crucial para la escalabilidad. Permite que los diseñadores de juego creen y modifiquen misiones sin tocar Blueprints. - Abre cada uno de estos hijos y rellena su variable
Paso 3: Gestor de Misiones (Actor Component) 🧩
Este componente se añadirá al Player Character (o a un Game State) y gestionará las misiones activas y completadas del jugador.
-
En
Blueprints/MissionSystem, clic derecho ->Blueprint Class->ActorComponent. NómbraloBP_QuestManagerComponent. -
Abre
BP_QuestManagerComponent. -
En
Class Settings, añade la interfazBPI_EventInterface. -
Añade las siguientes variables:
ActiveQuests(Map:Name->FQuestData): Misiones que el jugador tiene actualmente.CompletedQuests(Array ofName): IDs de misiones completadas.QuestDataMap(Map:Name->DA_QuestBase Object Reference): Un mapa de todas las misiones disponibles, cargado al inicio.
-
BeginPlayLógica:- Llama a
RegisterForGlobalEventpara todos losCompletionEventposibles que tus misiones puedan tener. Una forma es iterar sobre tuQuestDataMapy registrar cadaCompletionEventúnico. Esto puede ser ineficiente si tienes muchos eventos. Una alternativa es registrarse para un evento genérico y filtrar dentro deNotifyEvent. - Para simplificar, registraremos el componente para eventos comunes como
EnemyDied,ItemCollected, etc., que sabemos que serán disparados por el juego. - Obtén
G_EventSubsystemy regístrate para los eventos que el gestor de misiones necesite escuchar para actualizar los objetivos (ej.EnemyDied,ItemCollected,LocationReached).
- Llama a
// BP_QuestManagerComponent: Event BeginPlay
// -> Get Game Instance Subsystem (Class: G_EventSubsystem)
// -> RegisterForGlobalEvent (Listener: Self, EventNameToListen: EnemyDied)
// -> RegisterForGlobalEvent (Listener: Self, EventNameToListen: ItemCollected)
// -> RegisterForGlobalEvent (Listener: Self, EventNameToListen: LocationReached)
NotifyEventLógica: Aquí es donde procesamos los eventos globales.- Usa un
Switch on Namepara elEventName. - Para cada caso (ej.
EnemyDied):- Itera sobre
ActiveQuests. - Para cada misión activa, itera sobre sus
QuestObjectives. - Si el
CompletionEventdel objetivo coincide con elEventNameactual, y el objetivo no está completo:- Incrementa
CurrentProgressdel objetivo. - Si
CurrentProgress>=RequiredProgress, marcabIsCompletecomoTrue. - Comprueba si todos los objetivos de la misión están completos. Si es así, completa la misión (
CompleteQuestfunction).
- Incrementa
- Itera sobre
- Usa un
// BP_QuestManagerComponent: Event NotifyEvent (Inputs: EventName, EventData)
// -> Switch on Name (EventName)
// -> Case: EnemyDied
// -> ForEachLoop (ActiveQuests)
// -> Break (if QuestData.QuestObjectives found)
// -> ForEachLoop (ActiveQuest.QuestObjectives)
// -> Branch (Condition: Objective.CompletionEvent == EventName AND NOT Objective.bIsComplete)
// -> True:
// -> Increment Objective.CurrentProgress
// -> Branch (Condition: Objective.CurrentProgress >= Objective.RequiredProgress)
// -> True:
// -> Set Objective.bIsComplete = True
// -> CheckIfQuestIsComplete (QuestID)
AcceptQuest(Function)- Input:
QuestID(Name) - Lógica:
- Verifica si la misión ya está activa o completada.
- Si no, busca la misión en
QuestDataMap. - Añade la misión a
ActiveQuests. - Dispara un evento global
QuestAcceptedpara que el HUD u otros sistemas puedan reaccionar.
- Input:
// BP_QuestManagerComponent: Function: AcceptQuest
// Inputs: QuestID (Name)
// Graph:
// -> Get QuestData from QuestDataMap
// -> Add to ActiveQuests map
// -> Get G_EventSubsystem
// -> TriggerGlobalEvent (EventName: QuestAccepted, EventData: QuestID)
CompleteQuest(Function)- Input:
QuestID(Name) - Lógica:
- Mueve la misión de
ActiveQuestsaCompletedQuests. - Aplica las recompensas (requeriría interactuar con un sistema de inventario/XP separado, aquí solo lo simulamos o disparamos un evento).
- Dispara un evento global
QuestCompleted. - Si hay una
NextQuestID, llama aAcceptQuestpara la siguiente misión.
- Mueve la misión de
- Input:
// BP_QuestManagerComponent: Function: CompleteQuest
// Inputs: QuestID (Name)
// Graph:
// -> Remove QuestData from ActiveQuests map
// -> Add QuestID to CompletedQuests array
// -> Get G_EventSubsystem
// -> TriggerGlobalEvent (EventName: QuestCompleted, EventData: QuestID)
// -> Handle rewards (e.g., TriggerGlobalEvent: AddXP, AddItem)
// -> Branch (Has NextQuestID)
// -> True:
// -> AcceptQuest (NextQuestID)
Paso 4: Integración en el Player Character 🚶
- Abre el Blueprint de tu
Player Character(ej.BP_ThirdPersonCharacter). - En la pestaña
Components, haz clic enAddy buscaBP_QuestManagerComponent. Añádelo.
Ahora tu personaje tiene un gestor de misiones activo que puede procesar eventos y gestionar el progreso de las misiones.
💡 Ejemplos de Uso Práctico
Ejemplo 1: Misión de Eliminar Enemigos 👹
- DA_Quest_KillGoblins:
QuestID: KillGoblinsQuest,QuestName: Goblin Hunt,Objective: Kill5Goblins,RequiredProgress: 5,CompletionEvent: EnemyDied. - BP_Enemy: Cuando el enemigo muere, llama a
Get Game Instance Subsystem->TriggerGlobalEvent(EventName: EnemyDied,EventData: Self(referencia al enemigo)). - BP_QuestManagerComponent: Escuchará
EnemyDied. En suNotifyEvent, incrementaráCurrentProgressparaKill5Goblins.
Ejemplo 2: Misión de Recoger Objeto 🍎
- DA_Quest_FetchItem:
QuestID: FetchAppleQuest,QuestName: Find the Golden Apple,Objective: CollectGoldenApple,RequiredProgress: 1,CompletionEvent: ItemCollected. - BP_PickupItem: Cuando el jugador recoge un objeto, llama a
Get Game Instance Subsystem->TriggerGlobalEvent(EventName: ItemCollected,EventData: Self(referencia al objeto recogido o su ID)). - BP_QuestManagerComponent: Escuchará
ItemCollected. En suNotifyEvent, incrementaráCurrentProgressparaCollectGoldenApple.
Ejemplo 3: Misión de Llegar a una Ubicación 🗺️
- DA_Quest_ReachLocation:
QuestID: ExploreCave,QuestName: Enter the Dragon's Lair,Objective: ArriveAtCaveEntrance,RequiredProgress: 1,CompletionEvent: LocationReached. - BP_TriggerVolume_CaveEntrance: Coloca un
Trigger Boxen el mundo. En suOverlapevent con elPlayer Character, llama aGet Game Instance Subsystem->TriggerGlobalEvent(EventName: LocationReached,EventData: Self(referencia al trigger o su ID)). - BP_QuestManagerComponent: Escuchará
LocationReached. En suNotifyEvent, incrementaráCurrentProgressparaArriveAtCaveEntrance.
📈 Mejoras y Consideraciones Avanzadas
Persistencia (Save/Load) 💾
Para que el progreso de las misiones se guarde, necesitarías serializar las variables ActiveQuests y CompletedQuests de tu BP_QuestManagerComponent a un SaveGame object. Luego, al cargar la partida, deserializar y restaurar estos valores.
Interfaz de Usuario (HUD) 🖥️
Para mostrar las misiones en pantalla, tu HUD (widget UMG) debería escuchar los eventos QuestAccepted, QuestUpdated (que podrías disparar después de cada progreso) y QuestCompleted a través del G_EventSubsystem. De esta forma, el HUD se actualiza dinámicamente sin tener una referencia directa al gestor de misiones.
Eventos con Datos Específicos 📨
Actualmente, EventData es un Object Reference genérico. Para eventos que necesitan pasar muchos datos específicos (ej. EnemyKilled podría necesitar el tipo de enemigo, el arma usada, la ubicación, etc.), puedes crear un Blueprint Struct específico para ese evento y pasarlo como EventData. Luego, el receptor puede castear a ese struct.
Prioridad y Condiciones de Misiones ⚖️
Para misiones más complejas, podrías añadir variables a FQuestData como RequiredLevel, PrerequisiteQuestID (para misiones que deben completarse antes de aceptar esta), QuestType (Main, Side, Daily), etc. Esto añadiría más lógica a la función AcceptQuest.
Detalles de los Data Assets 🎨
Ejemplo de configuración de un Data Asset de misión
Aquí tienes un ejemplo de cómo se vería un DA_Quest_KillGoblins configurado:
{
"QuestID": "KillGoblinsQuest",
"QuestName": "Goblin Hunt",
"QuestDescription": "El bosque está plagado de duendes. Elimina 5 para proteger a los aldeanos.",
"QuestObjectives": [
{
"ObjectiveID": "Kill5Goblins",
"Description": "Mata 5 Goblins",
"CurrentProgress": 0,
"RequiredProgress": 5,
"bIsComplete": false,
"CompletionEvent": "EnemyDied"
}
],
"QuestRewards": [
{
"RewardType": "XP",
"Amount": 100
},
{
"RewardType": "Gold",
"Amount": 50
}
],
"bIsMainQuest": false,
"NextQuestID": ""
}
✅ Conclusión
Has construido un sistema de eventos globales desacoplado y un gestor de misiones dinámico en Unreal Engine 5 utilizando Blueprints. Estas herramientas son fundamentales para crear juegos escalables, modulares y fáciles de mantener. El sistema de eventos te permite que diferentes partes de tu juego se comuniquen de forma eficiente, mientras que el gestor de misiones, apoyado en Data Assets, facilita la creación de contenido de misiones de manera flexible y accesible para los diseñadores. ¡Ahora estás listo para crear experiencias de juego mucho más ricas y complejas!
Tutoriales relacionados
- Dominando el Sistema de Animación en Unreal Engine 5: Desde Retargeting hasta Blend Spacesintermediate20 min
- Creación de un Sistema de Lanzamiento de Proyectiles Dirigido con C++ y Blueprint en Unreal Engine 5intermediate20 min
- Unreal Engine: Creación de un Sistema de Carga/Guardado de Partidas con SaveGame y Blueprintintermediate18 min
- Unreal Engine: Creando un Sistema de Ciclismo Día/Noche Dinámico con Blueprintintermediate20 min
- Unreal Engine: Creación de un Sistema de Diálogos Ramificados con Data Tables y Blueprintintermediate15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!