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 typeFPrimaryAssetId
) 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*>
. AULyraAbilitySet
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()
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 correspondingLyraUserFacingExperienceDefinition
object. TheLyraUserFacingExperienceDefinition
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)
Feedback
Please leave any feedback about this article here