Looking into Lyra - Experiences

Introduction

The aim of this is to examine the source code from the Lyra example provided by Epic Games and to try and identify and document practices and approaches for using c++.

Experiences

"Experience" is the term Lyra uses for game modes such as first person, top down etc.

Data Classes

An experience is represented in code by two distinct classes:

  • ULyraExperienceDefinition which holds data properties and references to objects such as the Lyra pawn, actions and action sets
  • ULyraUserFacingExperienceDefinition which is a lighter weight object which holds the IDs (of type FPrimaryAssetId) of maps and experiences together with references to UI widgets.

Because the ULyraUserFacingExperienceDefinition holds IDs and not references it can be loaded without loading the experience to which it refers and so without loading other assets used by that experience. This means (a) it is quick to create the opening experience selection UI and (b) it avoids loading experiences and their assets when those assets may not be required.

Experience Blueprints

Experience blueprints such as B_LyraDefaultExperience extend the c++ class ULyraExperienceDefinition.

The B_LyraDefaultExperience blueprint is data only:

It holds:

  • the pawn class for that experience: TSubclassOf<APawn>
  • default camera mode used by player controlled pawns: TSubclassOf<ULyraCameraMode>
  • input configuration used by player controlled pawns to create input mappings and bind input actions: ULyraInputConfig* InputConfig
  • "action sets" which are (from the c++ comments) "ability sets to grant to this pawn's ability system": TArray<ULyraAbilitySet*>. A ULyraAbilitySet is a non-mutable data asset used to grant gameplay abilities and gameplay effects
  • the mapping of ability tags to use for actions taking by this pawn: ULyraAbilityTagRelationshipMapping* TagRelationshipMapping, i.e. a mapping of how ability tags block or cancel other abilities

To get an idea what is actually stored in an experience object, we can look at it in the debugger. This shows the actions:

And this shows the action sets:

Default Map

When starting with the default map (/Game/System/DefaultEditorMap/L_DefaultEditorOverview), the level contains a blueprint (/Game/System/DefaultEditorMap/B_ExperienceList3D) which creates the display of possible experiences, so the user is presented with UI for choosing which experience they want:

The list created by B_ExperienceList3D is a list of ULyraUserFacingExperienceDefinition. B_ExperienceList3D uses this blueprint:

It includes:

  • a Get Primary Asset Id List node to get the list of Primary Asset Ids which are of type LyraUserFacingExperienceDefinition
  • an Async Load Primary Asset List node to load array of LyraUserFacingExperienceDefinition objects

Using Tools | Search and searching for "LyraUserFacingExperienceDefinition" shows all the data assets of the experience type:

These are the source of the list of data asset ids returned from the Get Primary Asset Id List node.

Async Loading

The full blueprint which executes when the B_ExperienceList3D object gets the BeginPlay event is shown here:

The vertical green line shows where the Async Load Primary Asset List is. Everything to the left side of this line happens when the BeginPlay event is triggered, everything on the right side of the line happens asynchronously. Nothing is waiting on the right side nodes because all they do is populate some UI elements.

Discovery

If configured to do so, Unreal will discover any objects of type "LyraUserFacingExperienceDefinition" which are added to the project. This is done using the Edit | Project Settings... menu option and selecting Asset Manager on the left hand side. The screenshot below shows how the project is configured:

Starting an Experience

In Lyra experiences equate to game modes such as the main menu, the core shooter mode, the top down arena mode etc.

Lyra has a ULyraExperienceManagerComponent component which manages the launching of experiences with methods such as StartExperienceLoad() and OnExperienceFullLoadCompleted()

offset

When an experience is selected from the opening level LyraExperienceManagerComponent::StartExperienceLoad() is called. This method:

  • gets the primary asset id of any ULyraExperienceActionSets associated with the experience
  • identified any asset bundles to load, such as "Client", "Server" or "Equipped"
  • creates a delegate to call OnExperienceLoadComplete() once the async asset loading has completed

Then it starts the async loading of identified assets with these lines:

const TSharedPtr<FStreamableHandle> BundleLoadHandle = 
      AssetManager.ChangeBundleStateForPrimaryAssets( BundleAssetList.Array(), 
                                                      BundlesToLoad, {}, false, 
                                                      FStreamableDelegate(), 
                                                      FStreamableManager::AsyncLoadHighPriority);

const TSharedPtr<FStreamableHandle> RawLoadHandle = 
      AssetManager.LoadAssetList( RawAssetList.Array(), 
                                  FStreamableDelegate(), 
                                  FStreamableManager::AsyncLoadHighPriority, 
                                  TEXT("StartExperienceLoad()"));

Once the load is completed OnExperienceLoadComplete() is called. This:

  • identifies game features which have been loaded in plugins by calling CollectGameFeaturePluginURLs()
  • loads and actives those plugins by calling LoadAndActivateGameFeaturePlugin()

The UGameFeaturesSubsystem subsystem object is used:

  • resolve plugin names such as "ShooterCore" to plugin files such as "../Plugins/GameFeatures/ShooterCore/ShooterCore.uplugin"
  • load files using a call to LoadAndActivateGameFeaturePlugin()

Summary

So what about this approach is beneficial and could be reused on other projects?

  • the data assets are split. An experience object (a LyraExperienceDefinition) is represented in the UI by a corresponding LyraUserFacingExperienceDefinition object. The LyraUserFacingExperienceDefinition object holds only a reference to a picture of the map (in a texture), so presenting the map on the level select screen does not mean loading the actual map and all the assets it contains
  • the data assets hold IDs for the related map and the experience, not references, so the data assets can be loaded without loading the maps
  • the data assets such as maps and objects using in an experience are stored by in the LyraExperienceDefinition and automatically discovered by the engine. This facilitates adding a new game mode without changing any of the Lyra code

References

Epic: Asset Management (https://docs.unrealengine.com)
Epic: Asset Loading Best Practices (https://youtu.be/)
Tom Looman: Async Asset Loading (https://www.tomlooman.com)