Written for Unreal Engine 5.3 C++

This is the second chapter in the Lyra Deep Dive series.

Lyra introduces the concept of “experiences” which essentially are modular game modes. In this chapter, we’ll walk through various data assets which define a Lyra Experience.

Lyra Deep Dive Series

What is an Experience?

In Lyra, an experience is an extensible and modular combination of a Game Mode and Game State that can be asynchronously loaded and switched at runtime.

For example, in a typical shooter game, game types will be implemented as different experiences. Lyra follows this pattern by having the ShooterCore game implement the Elimination and Control game types as standalone experiences.

A banner demonstrating the first-person shooter experience

Since experiences are completely modular, they don’t even need to be in the same genre! Lyra demonstrates this by having one of the experiences completely transform the game into a top-down party game called Exploder.

A banner demonstrating the top-down Exploder experience

Most of the code related to Lyra Experiences are found in the /LyraGame/GameModes/ directory.

Plugins

The Lyra Experiences system is driven by the combination of the Game Features and Modular Gameplay plugins.

With the Game Features plugin, experiences are contained as standalone game feature plugins and loaded on demand. The Modular Gameplay plugin allows experiences to add components to actors, modify game state, add data sources, and much more.

Both plugins are commonly used together to make actors extensible via plugins and avoid coupling actors with features.

Game Features

With the Game Features plugin, the game can dynamically load and unload plugins at runtime. Game feature plugins can even be placed in separate standalone chunks to be distributed as downloadable content (DLC).

When you enable the Game Features plugin for your project and restart the editor, you’ll see a couple of new plugin templates.

Screenshot of two additional plugin templates: Game Feature (Content Only) and Game Feature (with C++).

These templates include the required UGameFeatureData data asset for your standalone game feature. This data asset describes what actions to perform when the feature is loaded and how to find additional primary data assets within the plugin. Keep in mind that all game feature plugins must go in the /Plugins/GameFeatures/ directory to be detected.

The default set of available actions are: Add Components, Add Cheats, Add Data Registry, and Add Data Registry Source. This can be extended with custom actions. In fact, Lyra has custom actions such as Add Widget, Add Input Binding, and more in the /LyraGame/GameFeatures/ directory. We’ll take a deeper look at these in a future chapter.

You can control the initial state of a game feature by editing the plugin in the Plugins window. In Lyra, all game features have the initial state of Registered. This is because with the Lyra Experiences system, the feature will be loaded only when the server selects the experience.

Screenshot of ShooterCore game feature plugin in Lyra with the initial state set to registered.

Modular Gameplay

The Modular Gameplay plugin enables actors to register themselves as receivers for components and senders of custom extension events.

Actors you want to make modular will need to register themselves with the UGameFrameworkComponentManager. With the AddGameFrameworkComponentReceiver function, the actor notifies the Modular Gameplay subsystem that it is accepting new actor components. This is typically done in the PreInitializeComponents function of the actor.

void AMyModularActor::PreInitializeComponents()
{
    Super::PreInitializeComponents();
    UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);
}

Actors also need to unregister themselves when they’re no longer accepting components. This is typically done in the EndPlay function of the actor.

void AMyModularActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);
    Super::EndPlay(EndPlayReason);
}

Modular actors create custom extension points by broadcasting custom events to any object subscribed to the actor class via UGameFrameworkComponentManager.

Most modular actors will want to send the GameActorReady event in BeginPlay so that extensions can execute code only when the actor is active.

void AMyModularActor::BeginPlay()
{
    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);
    Super::BeginPlay();
}

To create an extension, call AddExtensionHandler in the UGameFrameworkComponentManager subsystem. This will subscribe to all actors of the desired class for extension events. It must be an actor subclass and not the root AActor class, which means you cannot subscribe to every single actor. This is intentionally checked by the Modular Gameplay plugin to prevent significant performance impact to the game.

// This snippet is placed where you want to start handling actor extensions.
if (UGameFrameworkComponentManager* ComponentManager = UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance))
{			
    TSharedPtr<FComponentRequestHandle> ExtensionRequestHandle = ComponentManager->AddExtensionHandler(
        AMyModularActor::StaticClass(),
        UGameFrameworkComponentManager::FExtensionHandlerDelegate::CreateUObject(this, &ThisClass::HandleActorExtension));
}

Store ExtensionRequestHandle somewhere. Call Unregister on it to stop listening for extension events.

In the handler delegate, you typically would check the event name and execute code on the actor if it’s an event you wish to handle.

void UMyActorExtension::HandleActorExtension(AActor* Actor, FName EventName)
{
    if (EventName == UGameFrameworkComponentManager::NAME_GameActorReady)
    {
        // Do something when the actor is ready.
    }
    
    // Handle other extension events here.
}

Modular Gameplay Actors

The ModularGameplayActors plugin contains subclasses of the standard gameplay framework actors that are registered for extension via the Modular Gameplay plugin. While you can always register directly with UGameFrameworkComponentManager in your actors, this plugin makes it so that you don’t need to do it manually.

These actors are provided by this plugin:

  • AModularAIController
  • AModularCharacter
  • AModularGameModeBase
  • AModularGameMode
  • AModularGameStateBase
  • AModularGameState
  • AModularPawn
  • AModularPlayerController
  • AModularPlayerState

This plugin does not come with Unreal Engine out of the box. You will need to extract it from the Lyra source code.

Experience Definition

An Experience Definition describes what game features need to be enabled and what actions to perform in order to implement the experience.

To create an experience, create a blueprint based on ULyraExperienceDefinition in a directory that’s scanned by the Asset Manager.

Keep in mind that you cannot subclass from another experience blueprint if you want to create a similar experience. Let’s say you have a blueprint ShooterGame that’s subclassed from ULyraExperienceDefinition. You cannot have other blueprints, for example, let’s call them EliminationGame and CaptureTheFlagGame, to have ShooterGame as the parent class.

Instead, you should use composition via Action Sets. Both EliminationGame and CaptureTheFlagGame in the example above should be standalone experiences and reference an action set that implements the standard experience.

The experience definition has the following properties:

Property Description
GameFeaturesToEnable A list of game feature plugins to load when this experience is loaded.
DefaultPawnData A ULyraPawnData object that contains data needed to create a player pawn (i.e., pawn class, abilities, input config, and camera mode).
Actions A list of Game Feature actions to execute when the experience is loaded.
ActionSets A list of action sets (discussed in the next section) to compose into this experience.

Action Sets

Common game feature actions and plugins can be specified in an Action Set (ULyraExperienceActionSet).

It would be cumbersome to keep all experiences in sync during development, so a standard set of game feature actions are defined in an action set to be reused by multiple experiences.

In Lyra, both the Elimination and Control experiences are based on the same input, actor components, and HUD widgets. For this reason, they are specified in the LAS_ShooterGameSharedInput, LAS_ShooterGame_StandardComponents, and LAS_ShooterGame_StandardHUD action sets and referenced in the ActionSets list in both experience blueprints. You can see this for yourself in /Plugins/GameFeatures/ShooterCore/Content/Experiences/.

Since action sets are meant to be “merged” into experiences, they have some of the same properties found in experience definitions:

Property Description
GameFeaturesToEnable A list of game feature plugins to load when the owning experience is loaded.
Actions A list of Game Feature actions to execute when the owning experience is loaded.

Asset Manager

The Asset Manager is used to discover and stream assets. Primary assets can be detected and loaded by the asset manager while secondary assets are referenced by (and loaded along with) primary assets.

By default, the asset manager scans for levels only. Enabling the Game Features plugin will prompt you to add the GameFeatureData to the list of asset types to scan.

In Lyra, the asset manager is configured to also scan for experience definitions and action sets as shown below:

Screenshot of the asset manager settings in project settings.

As you can see in the next screenshot, only experiences in the /Game/System/Experiences/ directory will be found. You can either include individual assets with the Specific Assets property or add more directories to search.

Screenshot of the asset manager settings for Lyra Experience Definition showing only one directory that's scanned.

Game features extend this via the GameFeatureData asset. You can add more asset types to scan and indicate which directories to look inside to find them. For example, the ShooterCore game feature data indicates that experience definitions can also be found under /Experiences/ and /System/Experiences/ directories within the plugin.

Screenshot of the asset manager settings in the Game Feature Data for the ShooterCore plugin

Lyra uses ULyraAssetManager (in /LyraGame/System/) which is subclassed from the base UAssetManager class. The Lyra Asset Manager implements thread-safe asset loading functions and handles initial game load.

Next Steps

In the next chapter, we will explore the lifecycle of experiences including how they are applied to all players, loaded, and executed.

Read Chapter 3: Experience Lifecycle ❭