Skip to content

Architecture overview

Programmer · 20 min read · No code

XenoCore is one Game Feature plugin that sits on top of Epic's Lyra sample. Lyra supplies the framework: game mode, Experiences, the ability system plumbing, CommonUI menus and the character base classes. XenoCore adds everything RPG by subclassing Lyra's classes and adding components to Lyra's actors, never by editing Lyra. The one idea that makes the rest make sense: party-wide state lives on the player controller, per-character state lives on each pawn, and presentation lives in subsystems. Every system page in this section is one row of that split.

If you are new to the Lyra vocabulary, read the Glossary entries for Experience, Game Feature plugin and Pawn data first.

Designers: what you need from this page

This page is written for programmers. As a designer you need three things from it: the ownership map (which asset holds which setting), the rule that your changes go in your own copies, and the extension points. Everything else is background.

How it fits together

The plugin is Plugins/GameFeatures/XenoCore. It has two C++ modules and one content root:

Part What it is
Source/XenoCoreRuntime All gameplay C++. Loaded in game and editor.
Source/XenoCoreEditor Editor-only tools (animation-layer, camera, map and Niagara helpers). Never ships in a packaged game.
Content/ Every XenoCore asset. Appears in the Content Browser as XenoCore Content.
Config/Tags/XenoCoreTags.ini Gameplay tags that only data uses. Tags that C++ names are declared in XenoCoreGameplayTags.h.

The plugin starts in the Registered state (XenoCore.uplugin, BuiltInInitialFeatureState) and is switched on by an Experience, exactly like Lyra's own ShooterCore.

The Game Feature Data asset

Content/XenoCore (the asset named after the plugin) is the plugin's Game Feature Data. It runs while the feature is active and holds three actions:

Action What it adds
Add Components On every LyraPlayerController: BP_XenoCore_Party (the party), XenoCoreQuestComponent (quests) and XenoCorePartyGaugeComponent (the Party Gauge). On every Controller: BP_XenoCore_Progression (levels, EXP, gold) and LyraInventoryManagerComponent (the shared bag). On every PlayerController, client side only: LyraIndicatorManagerComponent (world indicators).
Add Input Context Mapping IMC_Default (Lyra's) at priority 1 and IMC_XenoCore (XenoCore's inputs) at priority 2.
Add Gameplay Cue Path Registers the plugin's GameplayCues folder (the action's default path, /GameplayCues, relative to the plugin), so GCN_XenoCore_Hit, GCN_XenoCore_Swing and GCN_XenoCore_HealNumber can be found.

It also tells the Asset Manager to scan /XenoCore/Experiences, /XenoCore/Maps and /XenoCore/System/Playlists, which is how the plugin's Experiences become discoverable without you editing the host project's config.

Fails silently: a new cue folder is never searched

Only the plugin's GameplayCues folder is registered. A Gameplay Cue Notify you save anywhere else in XenoCore Content (for example under Battle/GameplayCues) is never found: the cue fires, nothing plays, nothing is logged. Keep cue notifies in GameplayCues, or add a second path to the action.

The Experiences

Experience Used by What it does
Experiences/B_XenoCore_Experience The sample world and L_XenoCore_Gym Sets Default Pawn Data to Game/PawnData_XenoCore_Player; adds the LAS_XenoCore_HUD action set (the HUD layout W_XenoCore_HUDLayout and the number-pop component); grants AbilitySet_XenoCore_Interact to LyraPlayerState (whose ability system XenoCore does not use, so this grant does nothing; the party's Leader Only Ability Set is the grant that works); adds B_XenoCore_TeamSetup (a team creation component) to LyraGameState; enables the XenoCore Game Feature.
Experiences/B_XenoCore_FrontEnd_Experience L_XenoCore_FrontEnd Adds BP_XenoCore_TitleState to the game state, which pushes the boot screens and the title screen.

The ownership map

This is the table to come back to. When you want to know where something is stored, who changes it and who is told about it, find its row.

State Lives on Class (Blueprint) Why there
Roster, active party, leader slot Player controller UXenoCorePartyComponent (Characters/Party/BP_XenoCore_Party) Party-wide, and must survive switching which character you drive.
Levels, EXP, Art Points, learned arts, palettes, skill progress, gold Player controller UXenoCoreProgressionComponent (Game/BP_XenoCore_Progression) Keyed by member definition, so a benched character with no body still has a level.
The bag (all items) Player controller ULyraInventoryManagerComponent Party-wide; Lyra's own item-cost code looks for it on the controller.
Quest records Player controller UXenoCoreQuestComponent Party-wide and save-shaped.
Party Gauge Player controller UXenoCorePartyGaugeComponent Party-wide battle state; not saved. See Party Gauge, downed and revive.
Menu pause, ability-input pump Player controller AXenoCorePlayerController Pumps the pawn's ability system, not the player state's.
Ability system, attributes, active effects Each pawn ULyraAbilitySystemComponent on AXenoCoreCombatantCharacter (party) or AXenoCoreEnemyCharacter (enemies, a separate branch from Lyra's ALyraCharacterWithAbilities) Each character is its own identity; switching leader never moves stats.
Health and death, revive Each pawn UXenoCoreHealthComponent Swapped in for Lyra's health component on every combatant.
Worn equipment Each pawn UXenoCoreEquipmentComponent Equipment spawns meshes, which are per body.
Input binding and camera for the driven body Each playable pawn UXenoCoreHeroComponent Binds the pawn's own ability system; re-binds input when a body is taken back.
Party AI behaviour Each companion's controller AXenoCorePartyMemberController + StateTree ST_XenoCore_PartyMember Carries the team; behaviour is data.
Enemy AI behaviour Each enemy's controller AXenoCoreEnemyController + an enemy StateTree Carries the team, aggro and leash.
Current target The target-mode ability instance UXenoCoreGameplayAbility_TargetMode (GA_XenoCore_TargetMode) One instance per pawn, so the lock and its target share one lifetime.
Dialogue box and line sequencing World UXenoCoreDialogueSubsystem One conversation at a time, driven from the NPC's graph.
"What am I standing next to" and the A prompt World UXenoCoreInteractionSubsystem One owner for one button and one screen corner.
Damage-taken and heal numbers World UXenoCoreDamageNumberSubsystem Listens to Lyra's damage message; draws what Lyra does not.
Battle text (MISS, CRITICAL HIT!, BREAK...) World UXenoCoreBattleNotifySubsystem Can annotate any actor, widget or not.
Loot popup World UXenoCoreLootPopupSubsystem Reacts to a chest being opened.
Party join/leave banner World UXenoCorePartyNoticeSubsystem Waits until the game is unpaused before announcing.
"The battle was won", fanfare and victory emotes World UXenoCoreBattleVictorySubsystem The one place that tells a win from a retreat.
Battle voice lines and banter World UXenoCoreBattleVoiceSubsystem One speaker at a time across the party. See Battle voices.
Map markers World UXenoCoreMapMarkerSubsystem What the minimap draws. See Minimap.
Holding the loading screen until the world is ready World UXenoCoreWorldReadySubsystem Lifts only once bodies are posed and shaders have settled.
Save slots, load order, autosave Game instance UXenoCoreSaveSubsystem Must outlive a map change.
Time of day, weather, tutorial cards seen Game instance UXenoCoreTimeOfDaySubsystem, UXenoCoreWeatherSubsystem, UXenoCoreTutorialSubsystem Survive travel; each is a save participant. See Time of day and weather.
Your own saved state Wherever it lives, as a save participant IXenoCoreSaveParticipant, UXenoCoreSaveParticipantComponent Stored in the save under its own key; XenoCore never needs to know what it is. See Make a system save.
Which widgets and assets C++ opens Project settings UXenoCoreUISettings, UXenoCoreContentSettings A subsystem has no asset of its own to point at a widget. See UI shell and HUD.
Title screen Game state (front end only) UXenoCoreTitleStateComponent (UserInterface/BP_XenoCore_TitleState) Lyra's own front-end shape.

Single-player by design

Several resolvers answer "the party" by taking player controller 0. That assumption is written down in one function per system (UXenoCorePartyComponent::GetPartyComponent, UXenoCoreProgressionComponent::GetPartyProgression, UXenoCoreInventoryStatics::GetPartyInventory). XenoCore does not support multiplayer or split-screen.

One resolver per question

Every "where is X?" question has exactly one function that answers it, and everything else calls that function. Use these from Blueprint rather than searching for components yourself:

Question Ask
Where is the party? Get Party Component
Where are levels, EXP and gold? Get Party Progression
Where is the bag? Get Party Inventory
What is this pawn locked onto? Get Current Target (UXenoCoreTargetingStatics)

Fails silently: asking a companion's own controller

A companion is possessed by an AI controller that has none of the party components. A graph that does Get Controller → Get Component by Class on a companion gets nothing and carries on with empty data. Always use the resolvers above.

Lifecycle

  1. The map loads its Experience. The Experience enables the XenoCore Game Feature, whose Game Feature Data adds the controller components and input contexts.
  2. Lyra's game mode spawns the bootstrap pawn from Default Pawn Data (B_XenoCore_Player). Its hero component binds its ability system and grants its ability sets.
  3. UXenoCorePartyComponent::BeginPlay either restores a save or adds each Starting Roster member, then spawns bodies for the active members around the player and hands the leader's body to the player. See Party.
  4. Each spawned body initialises its own ability system, grants its pawn data's ability sets and applies its statline at its level. See Characters.
  5. Subsystems register their message listeners at world begin play and react from then on.

Change signals are messages

Lyra's own inventory and equipment change messages are sent only from replication callbacks, so in a single-player game they never fire. XenoCore broadcasts its own on the Gameplay Message Subsystem. Each has exactly one broadcaster, and every change goes through that broadcaster. The one safety net is the inventory watcher, which announces bag changes made around the statics.

Channel Sent by Carries
XenoCore.Party.Message.Changed Party component "Something changed" plus the active count. Re-read the component.
XenoCore.Progression.Message.Changed Progression component Levelled count and the EXP just awarded.
XenoCore.Progression.Message.LevelUp Progression component Every member who levelled, with before and after levels.
XenoCore.Inventory.Message.Changed UXenoCoreInventoryStatics (and UXenoCoreInventoryWatchSubsystem for changes made around it) Bag changed.
XenoCore.Equipment.Message.SlotChanged Equipment component A slot changed.
XenoCore.Targeting.Message.TargetChanged Target-mode ability Old and new target.
XenoCore.Battle.Message.Notification Battle notify subsystem Text, colour and target for a battle line.
XenoCore.Enemy.Message.Defeated Enemy character An enemy died.
XenoCore.Battle.Message.Won Battle victory subsystem A battle was won (not fled).
XenoCore.Party.Message.TacticChanged Party component A party tactic changed.
XenoCore.Loot.Message.ChestOpened Treasure chest A chest was opened.
XenoCore.Quest.Message.Changed, .TurnedIn Quest component Quest state; turn-in carries the payout.

The full list and payloads are in Events you can listen to.

Changing the bag directly

Every bag change goes through UXenoCoreInventoryStatics. A change made by calling Lyra's inventory component yourself is caught by UXenoCoreInventoryWatchSubsystem, which sends XenoCore.Inventory.Message.Changed for it a frame later, so menus stay current. It cannot undo one, though: Lyra's Remove Item Instance will remove an item a character is wearing, and the save then loses it. See Items and inventory.

Extending it

The rule: never edit Lyra. Subclass it, add a component, or add a row of data. Lyra's files are the host; if you change them, a Lyra update overwrites your work, and XenoCore's behaviour may change under you.

  • To change a XenoCore system's numbers, edit the Blueprint subclass that holds them (BP_XenoCore_Party, BP_XenoCore_Progression, BP_XenoCoreDamageExecution) or, better, make a child Blueprint of it in Content/MyGame/ and point the Game Feature Data at your child.
  • To change behaviour in C++, subclass the XenoCore class and override its virtuals. See Subclass a mechanic.
  • To add a new system, give it the same shape: party-wide state on a controller component with a static resolver, per-character state on the pawn's ability system, presentation in a world subsystem that listens to a message.

Every public class and struct in XenoCoreRuntime is exported (XENOCORERUNTIME_API), so a C++ subclass in your own module links, including the characters, both AI controllers, the stat set, the damage execution and the equipment component. Only some of Lyra's classes are not exported; see Subclass a mechanic.

Extension points

These are the places built for your additions. Each needs no edit to a XenoCore asset or source file.

To add Use Recipe
A menu entry, or a different menu button Menu Entries or a XenoCoreMenuEntrySet in Project Settings › Game › XenoCore UI Add a menu screen
A replacement for a screen or HUD widget C++ opens The matching XenoCore UI or XenoCore Content setting Restyle the menus
A widget on the HUD An Add Widgets action on a HUD.Slot.XenoCore.* slot Add a HUD element
Something the player presses A on A Blueprint child of AXenoCoreInteractableActor Add an interactable object
Saved state for your own system A XenoCore Save Participant component, or the interface Make a system save
A yes/no question in a screen The Ask Confirm node Add a confirmation popup
An input action for every playable character One row in Game/AbilitySet_XenoCore_Input Add an input action with a prompt
An equipment stat One modifier row on Inventory/GE_XenoCore_EquipmentStats Add an equipment stat
New behaviour in C++ A subclass in your own module Subclass a mechanic

XenoCore needs no edits to Lyra

Where XenoCore needs Lyra content to behave differently, it ships its own copy instead of changing Lyra's: the damage-number cue is GameplayCues/GCNL_XenoCore_DamageTaken (fired by GE_XenoCore_Damage; Lyra's GCNL_Character_DamageTaken is untouched), and keyboard key icons are added at startup from Project Settings › Game › XenoCore UI › HUD › Keyboard Key Icons. A fresh download of Lyra therefore needs nothing re-applied.

Why it's built this way

  • Pawn-hosted ability systems. Lyra keeps a player's ability system on the player state, which would mean every party member shares one identity or needs its own player state. XenoCore uses Lyra's own ALyraCharacterWithAbilities shape (the one Epic's damageable test dummy uses), so a character's stats, arts and effects travel with its body. Switching leader is only possession.
  • Controller components for the party. The same place Lyra's quick bar and item-cost code look. It survives pawn changes and is reachable from anything with a world.
  • Subsystems for presentation. A notification is about the battle, not a pawn, so it has no business being a component on every character.
  • Own messages. Lyra's change messages rely on replication. XenoCore's fire in standalone play, and each has one sender, so a listener never has to guess which of several paths changed something.