tutoriales.com

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.

Intermedio20 min de lectura12 views
Reportar error

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

💡 Consejo: Think of it as a broadcast radio station. The station (event system) broadcasts messages, and anyone with a radio (event listener) can tune in without needing to know who sent the original message.

¿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/Core
  • Blueprints/MissionSystem
  • Blueprints/Events
  • Data
  • UI (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.

  1. En Blueprints/Events, clic derecho -> Blueprint Class -> All Classes, busca BlueprintImplementableInterface. Nómbralo BPI_EventInterface.

  2. Abre BPI_EventInterface.

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

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.

  1. En Blueprints/Events, clic derecho -> Blueprint Class -> All Classes, busca BlueprintFunctionLibrary. Nómbralo BFL_GlobalEvents.
  2. Abre BFL_GlobalEvents.
  3. En la pestaña Graph (si no está, ve a File -> New Blueprint -> Blueprint Function Library), necesitamos una forma de almacenar el Event Dispatcher de manera persistente y global. Los Function Libraries no tienen estados. La mejor manera de lograr esto es utilizando un Game Instance o un Subsystem. Optaremos por un Subsystem ya 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.

  1. En Blueprints/Core, clic derecho -> Blueprint Class -> All Classes, busca GameInstanceSubsystem. Nómbralo G_EventSubsystem.
  2. Abre G_EventSubsystem.
  3. En la pestaña Variables, añade una nueva variable:
    • Nombre: OnGlobalEventTriggered
    • Tipo: Event Dispatcher.
    • Haz clic en el icono de Pin para que sea Exposed on Spawn (aunque no lo spawnaremos directamente, es buena práctica). También asegúrate de que sea Public (ojo, esto es por la forma en que Blueprint maneja los Event Dispatchers, no necesariamente por ser una buena práctica de encapsulación).
  4. Dentro de OnGlobalEventTriggered (en la pestaña Details de la variable), añade los siguientes Inputs:
    • EventName (Name)
    • EventData (Object Reference)
OnGlobalEventTriggered EVENT DISPATCHER EventName Name EventData Object Reference
  1. 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 OnGlobalEventTriggered al gráfico, selecciona Bind Event to OnGlobalEventTriggered. Conecta el Listener al nodo de ejecución y al Target del Bind Event. Crea un Custom Event para conectar al Event del Bind Event, y en este Custom Event añade los inputs EventName (Name) y EventData (Object Reference). Este Custom Event será el que se ejecute cuando se dispare el evento global. En este Custom Event, deberás llamar a la función Execute_NotifyEvent del Listener (usando la interfaz BPI_EventInterface).
// 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.

  1. Ve a Project Settings -> Maps & Modes.
  2. En la sección Game Instance, asegúrate de que tienes un Game Instance Class personalizado o crea uno. Si no tienes uno, crea un nuevo Blueprint Class de tipo GameInstance en Blueprints/Core (ej. BP_MyGameInstance) y configúralo aquí.
  3. No necesitamos hacer nada más en el Game Instance si 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:

  1. En su Blueprint, añade Get Game Instance Subsystem y busca G_EventSubsystem.
  2. Llama a TriggerGlobalEvent en el Subsystem.
  3. Pasa el EventName (ej. EnemyDied) y el EventData (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:

  1. En su Blueprint, en Class Settings, añade la interfaz BPI_EventInterface.
  2. Implementa el evento NotifyEvent (clic derecho en el evento NotifyEvent en la pestaña My Blueprint y selecciona Implement Event).
  3. En BeginPlay (o cuando sea apropiado), llama a Get Game Instance Subsystem y RegisterForGlobalEvent en el Subsystem, pasando Self como Listener y el EventName que quieres escuchar (ej. EnemyDied).
  4. Dentro del evento NotifyEvent, puedes usar un Switch on Name con el EventName para manejar diferentes tipos de eventos, y luego Cast el EventData a 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
⚠️ Advertencia: Recuerda desregistrar los eventos (`UnregisterFromGlobalEvent`) cuando el actor ya no sea relevante (ej. en `EndPlay`) para evitar *memory leaks* o comportamientos inesperados en actores destruidos.

📋 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í.

  1. En Data, clic derecho -> Blueprints -> Structure. Nómbralo FQuestObjective.

    • 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).
  2. En Data, clic derecho -> Blueprints -> Structure. Nómbralo FQuestReward.

    • Variables:
      • RewardType (Name): 'XP', 'Gold', 'Item'.
      • Amount (Integer).
      • ItemID (Name) (Opcional, si RewardType es 'Item').
  3. En Data, clic derecho -> Blueprints -> Structure. Nómbralo FQuestData.

    • Variables:
      • QuestID (Name): Identificador único de la misión.
      • QuestName (Text).
      • QuestDescription (Text).
      • QuestObjectives (Array of FQuestObjective).
      • QuestRewards (Array of FQuestReward).
      • bIsMainQuest (Boolean).
      • NextQuestID (Name): Opcional, para encadenar misiones.
FQuestData FString QuestID; FText Nombre; TArray<FQuestObjective> Objetivos; TArray<FQuestReward> Recompensas; 1..* 1..* FQuestObjective FString ObjectiveID; EObjectiveType Tipo; int32 CantidadRequerida; FText Descripcion; FQuestReward FString RewardID; ERewardType Tipo; int32 Cantidad; FName ItemID;

Paso 2: Base de Datos de Misiones (Quest Data Asset) 💾

Utilizaremos Data Assets para crear nuestras misiones como activos individuales, fáciles de gestionar.

  1. En Data, clic derecho -> Miscellaneous -> Data Asset.

  2. En el selector de clase, busca PrimaryDataAsset. Nómbralo DA_QuestBase.

  3. Abre DA_QuestBase y añade una variable:

    • Nombre: QuestData
    • Tipo: FQuestData.
    • Haz clic en el icono de Eye para que sea Editable.
  4. Ahora, para crear misiones reales, clic derecho en DA_QuestBase -> Create Child Blueprint Class. Puedes nombrarlos DA_Quest_KillGoblins, DA_Quest_FetchItem, etc.

    • Abre cada uno de estos hijos y rellena su variable QuestData con 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.

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.

  1. En Blueprints/MissionSystem, clic derecho -> Blueprint Class -> ActorComponent. Nómbralo BP_QuestManagerComponent.

  2. Abre BP_QuestManagerComponent.

  3. En Class Settings, añade la interfaz BPI_EventInterface.

  4. Añade las siguientes variables:

    • ActiveQuests (Map: Name -> FQuestData): Misiones que el jugador tiene actualmente.
    • CompletedQuests (Array of Name): IDs de misiones completadas.
    • QuestDataMap (Map: Name -> DA_QuestBase Object Reference): Un mapa de todas las misiones disponibles, cargado al inicio.
  5. BeginPlay Lógica:

    • Llama a RegisterForGlobalEvent para todos los CompletionEvent posibles que tus misiones puedan tener. Una forma es iterar sobre tu QuestDataMap y registrar cada CompletionEvent único. Esto puede ser ineficiente si tienes muchos eventos. Una alternativa es registrarse para un evento genérico y filtrar dentro de NotifyEvent.
    • Para simplificar, registraremos el componente para eventos comunes como EnemyDied, ItemCollected, etc., que sabemos que serán disparados por el juego.
    • Obtén G_EventSubsystem y regístrate para los eventos que el gestor de misiones necesite escuchar para actualizar los objetivos (ej. EnemyDied, ItemCollected, LocationReached).
// 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)
  1. NotifyEvent Lógica: Aquí es donde procesamos los eventos globales.
    • Usa un Switch on Name para el EventName.
    • Para cada caso (ej. EnemyDied):
      • Itera sobre ActiveQuests.
      • Para cada misión activa, itera sobre sus QuestObjectives.
      • Si el CompletionEvent del objetivo coincide con el EventName actual, y el objetivo no está completo:
        • Incrementa CurrentProgress del objetivo.
        • Si CurrentProgress >= RequiredProgress, marca bIsComplete como True.
        • Comprueba si todos los objetivos de la misión están completos. Si es así, completa la misión (CompleteQuest function).
// 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)
  1. 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 QuestAccepted para que el HUD u otros sistemas puedan reaccionar.
// BP_QuestManagerComponent: Function: AcceptQuest
// Inputs: QuestID (Name)
// Graph:
// -> Get QuestData from QuestDataMap
// -> Add to ActiveQuests map
// -> Get G_EventSubsystem
//    -> TriggerGlobalEvent (EventName: QuestAccepted, EventData: QuestID)
  1. CompleteQuest (Function)
    • Input: QuestID (Name)
    • Lógica:
      • Mueve la misión de ActiveQuests a CompletedQuests.
      • 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 a AcceptQuest para la siguiente misión.
// 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 🚶

  1. Abre el Blueprint de tu Player Character (ej. BP_ThirdPersonCharacter).
  2. En la pestaña Components, haz clic en Add y busca BP_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 👹

  1. DA_Quest_KillGoblins: QuestID: KillGoblinsQuest, QuestName: Goblin Hunt, Objective: Kill5Goblins, RequiredProgress: 5, CompletionEvent: EnemyDied.
  2. BP_Enemy: Cuando el enemigo muere, llama a Get Game Instance Subsystem -> TriggerGlobalEvent (EventName: EnemyDied, EventData: Self (referencia al enemigo)).
  3. BP_QuestManagerComponent: Escuchará EnemyDied. En su NotifyEvent, incrementará CurrentProgress para Kill5Goblins.

Ejemplo 2: Misión de Recoger Objeto 🍎

  1. DA_Quest_FetchItem: QuestID: FetchAppleQuest, QuestName: Find the Golden Apple, Objective: CollectGoldenApple, RequiredProgress: 1, CompletionEvent: ItemCollected.
  2. 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)).
  3. BP_QuestManagerComponent: Escuchará ItemCollected. En su NotifyEvent, incrementará CurrentProgress para CollectGoldenApple.

Ejemplo 3: Misión de Llegar a una Ubicación 🗺️

  1. DA_Quest_ReachLocation: QuestID: ExploreCave, QuestName: Enter the Dragon's Lair, Objective: ArriveAtCaveEntrance, RequiredProgress: 1, CompletionEvent: LocationReached.
  2. BP_TriggerVolume_CaveEntrance: Coloca un Trigger Box en el mundo. En su Overlap event con el Player Character, llama a Get Game Instance Subsystem -> TriggerGlobalEvent (EventName: LocationReached, EventData: Self (referencia al trigger o su ID)).
  3. BP_QuestManagerComponent: Escuchará LocationReached. En su NotifyEvent, incrementará CurrentProgress para ArriveAtCaveEntrance.

📈 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": ""
}
⚠️ Advertencia: Un uso excesivo de eventos globales con muchos oyentes puede afectar el rendimiento si no se optimiza bien. Es una buena práctica solo registrarse para los eventos que realmente se necesitan y desregistrarse cuando ya no son relevantes.

✅ 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

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!