Lyra Integration
Advanced Weapon Mechanics Integration Guide
📘 Overview
This guide explains how to integrate the Adaptive Recoil & Spread system into Lyra Starter Game's weapon framework. The plugin replaces Lyra's native spread calculations while adding new recoil and spread mechanics through component-based architecture.
⚠️ Requirements
Unreal Engine 5.1+ with Lyra Starter Game project
Visual Studio 2022 with C++ UE5 development tools
Basic understanding of Lyra's Ability System (GAS)
🛠️ Before You Begin
Complete Plugin Installation: Ensure you've successfully installed the plugin following our Installation Guide.
Backup Your Project: Recommended for first-time integrations (
File → Save All with Derived Data
)
🧩 Integration Workflow
🔌 Section 1: Plugin Setup for Lyra
Follow our comprehensive Installation Guide to:
Install the plugin in Lyra's
/Plugins/
directoryRegenerate project files
Right-click → LyraStarterGame.uplugin
Select Generate Visual Studio Project Files
Configure build dependencies:
Open LyraGame.build.cs under the Sources/LyraGame folder
Add "AdaptiveRecoilSpread" module to PrivateDependencyModuleNames array.
PrivateDependencyModuleNames.AddRange(
new string[] {
"InputCore",
"Slate",
"SlateCore",
"RenderCore",
"DeveloperSettings",
"EnhancedInput",
"NetCore",
"RHI",
"Projects",
"Gauntlet",
"UMG",
"CommonUI",
"CommonInput",
"GameSettings",
"CommonGame",
"CommonUser",
"GameSubtitles",
"GameplayMessageRuntime",
"AudioMixer",
"NetworkReplayStreaming",
"UIExtension",
"ClientPilot",
"AudioModulation",
"EngineSettings",
"DTLSHandlerComponent",
"Json",
"AdaptiveRecoilSpread" // Add ARS plugin to modules
}
);
Compile the Lyra project with settings
Set "LyraGame" as your startup project
Solution Configuration: "Development Editor"
Solution Platforms: "Win64" (Depends on your platforms)
Verify successful compilation
🎯 Section 2: Implementing Recoil and Spread Management
The ARSRecoilSpreadManager
component handles real-time recoil/spread calculations and network synchronization. In Lyra, this must be added to controllers rather than pawns/characters to ensure proper multiplayer replication. Follow the implementation steps below:
Blueprint Configuration (Data Asset)
Open ShooterCore Data Asset
Navigate to:
/All/Plugins/ShooterCore Content/ → ShooterCore
Note: You must enable 'Show Plugin Content' to see ShooterCore Content in Favorites.

Locate Component Setup
Go to: Actions → Add Components → Component List
Add New Component Entry
Click
+ Add
in the Component List arrayConfigure as:
Actor Class: Controller Component Class: ARSRecoilSpreadManager Client Component: True Server Component: True
Critical Configuration Details
Actor Class
Controller
Ensures component exists for each player
Client/Server
Both True
Enables prediction and authority reconciliation
Spawn Priority
500+
Ensures creation before weapon components
⚙️ Section 3: Weapon Equipment Instance Modification
Overview
Lyra uses ULyraRangedWeaponInstance
to represents weapons and define their behaviors. This section modifies this class to:
Sync patterns with
ARSRecoilSpreadManager
Replace Lyra's native spread calculations
Implementation Steps
1. Header File Modifications (ULyraRangedWeaponInstance.h
)
ULyraRangedWeaponInstance.h
)// Source/LyraGame/Weapons/LyraRangedWeaponInstance.h
#pragma once
#include "LyraWeaponInstance.h"
#include "AbilitySystem/LyraAbilitySourceInterface.h"
#include "Curves/CurveFloat.h"
#include "LyraRangedWeaponInstance.generated.h"
class UARSRecoilSpreadManager;
class UARSSpreadPattern;
class UARSRecoilPattern;
class UPhysicalMaterial;
/**
* ULyraRangedWeaponInstance
*
* A piece of equipment representing a ranged weapon spawned and applied to a pawn
*/
UCLASS()
class LYRAGAME_API ULyraRangedWeaponInstance : public ULyraWeaponInstance, public ILyraAbilitySourceInterface
{
GENERATED_BODY()
public:
ULyraRangedWeaponInstance(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
virtual void PostInitProperties() override;
//~ULyraEquipmentInstance interface
virtual void OnEquipped() override;
virtual void OnUnequipped() override;
//~End of ULyraEquipmentInstance interface
//~ILyraAbilitySourceInterface interface
virtual float GetDistanceAttenuation(float Distance, const FGameplayTagContainer* SourceTags = nullptr, const FGameplayTagContainer* TargetTags = nullptr) const override;
virtual float GetPhysicalMaterialAttenuation(const UPhysicalMaterial* PhysicalMaterial, const FGameplayTagContainer* SourceTags = nullptr, const FGameplayTagContainer* TargetTags = nullptr) const override;
//~End of ILyraAbilitySourceInterface interface
public:
int32 GetBulletsPerCartridge() const;
float GetMaxDamageRange() const;
float GetBulletTraceSweepRadius() const;
// Add recoil and spread for next shot
virtual void ApplyRecoilAndSpread();
/** Returns the current spread angle (in degrees, diametrical) */
float GetCalculatedSpreadAngle() const;
float GetCalculatedSpreadAngleMultiplier() const;
float GetSpreadExponent() const;
bool HasFirstShotAccuracy() const;
UARSRecoilSpreadManager* GetRecoilSpreadManager() const;
protected:
// Number of bullets to fire in a single cartridge (typically 1, but may be more for shotguns)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon Config", meta = (ClampMin = "1"))
int32 BulletsPerCartridge = 1;
// The maximum distance at which this weapon can deal damage
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon Config", meta=(ForceUnits=cm))
float MaxDamageRange = 25000.0f;
// The radius for bullet traces sweep spheres (0.0 will result in a line trace)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon Config", meta=(ForceUnits=cm), meta = (ClampMin = "0.0"))
float BulletTraceSweepRadius = 0.0f;
/**
* A curve that maps the distance (in cm) to a multiplier on the base damage from the associated gameplay effect.
* If there is no data in this curve, then the weapon is assumed to have no falloff with distance.
*/
UPROPERTY(EditAnywhere, Category = "Weapon")
FRuntimeFloatCurve DistanceDamageFalloff;
/**
* List of special tags that affect how damage is dealt.
* These tags will be compared to tags in the physical material of the thing being hit.
* If more than one tag is present, the multipliers will be combined multiplicatively.
*/
UPROPERTY(EditAnywhere, Category = "Weapon")
TMap<FGameplayTag, float> MaterialDamageMultiplier;
// Enable/disable recoil mechanics for weapon
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon|Recoil")
bool bEnableRecoil = true;
/*
* Recoil pattern for shooting
* For change or get pattern in runtime use GetRecoilPattern/SetRecoilPattern function
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon|Recoil")
TObjectPtr<UARSRecoilPattern> RecoilPattern;
// Enable/disable spread mechanics for weapon
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Weapon|Spread")
bool bEnableSpread = true;
/*
* Spread pattern for shooting
* For change or get pattern in runtime use GetSpreadPattern/SetSpreadPattern function
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Weapon|Spread")
TObjectPtr<UARSSpreadPattern> SpreadPattern;
/**
* Cached reference to the RecoilSpreadManager component.
*/
UPROPERTY()
mutable TWeakObjectPtr<UARSRecoilSpreadManager> CachedRecoilSpreadManager;
};
2. Source File Updates (LyraRangedWeaponInstance.cpp
)
LyraRangedWeaponInstance.cpp
)// Source/LyraGame/Weapons/LyraRangedWeaponInstance.cpp
#include "LyraRangedWeaponInstance.h"
#include "AbilitySystemComponent.h"
#include "LyraLogChannels.h"
#include "NativeGameplayTags.h"
#include "Components/ARSRecoilSpreadManager.h"
#include "GameFramework/Pawn.h"
#include "Physics/PhysicalMaterialWithTags.h"
#include "Weapons/LyraWeaponInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(LyraRangedWeaponInstance)
UE_DEFINE_GAMEPLAY_TAG_STATIC(TAG_Lyra_Weapon_SteadyAimingCamera, "Lyra.Weapon.SteadyAimingCamera");
ULyraRangedWeaponInstance::ULyraRangedWeaponInstance(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
void ULyraRangedWeaponInstance::PostInitProperties()
{
Super::PostInitProperties();
}
void ULyraRangedWeaponInstance::OnEquipped()
{
Super::OnEquipped();
if (UARSRecoilSpreadManager* RecoilSpreadManager = GetRecoilSpreadManager())
{
RecoilSpreadManager->SetSpreadPattern(SpreadPattern);
RecoilSpreadManager->SetRecoilPattern(RecoilPattern);
}
}
void ULyraRangedWeaponInstance::OnUnequipped()
{
if (UARSRecoilSpreadManager* RecoilSpreadManager = GetRecoilSpreadManager())
{
RecoilSpreadManager->SetSpreadPattern(nullptr);
RecoilSpreadManager->SetRecoilPattern(nullptr);
}
Super::OnUnequipped();
}
float ULyraRangedWeaponInstance::GetDistanceAttenuation(float Distance, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags) const
{
const FRichCurve* Curve = DistanceDamageFalloff.GetRichCurveConst();
return Curve->HasAnyData() ? Curve->Eval(Distance) : 1.0f;
}
float ULyraRangedWeaponInstance::GetPhysicalMaterialAttenuation(const UPhysicalMaterial* PhysicalMaterial, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags) const
{
float CombinedMultiplier = 1.0f;
if (const UPhysicalMaterialWithTags* PhysMatWithTags = Cast<const UPhysicalMaterialWithTags>(PhysicalMaterial))
{
for (const FGameplayTag MaterialTag : PhysMatWithTags->Tags)
{
if (const float* pTagMultiplier = MaterialDamageMultiplier.Find(MaterialTag))
{
CombinedMultiplier *= *pTagMultiplier;
}
}
}
return CombinedMultiplier;
}
int32 ULyraRangedWeaponInstance::GetBulletsPerCartridge() const
{
return BulletsPerCartridge;
}
float ULyraRangedWeaponInstance::GetMaxDamageRange() const
{
return MaxDamageRange;
}
float ULyraRangedWeaponInstance::GetBulletTraceSweepRadius() const
{
return BulletTraceSweepRadius;
}
void ULyraRangedWeaponInstance::ApplyRecoilAndSpread()
{
const APawn* Pawn = GetPawn();
const AController* Controller = Pawn->GetController();
if (UARSRecoilSpreadManager* RecoilSpreadManager = GetRecoilSpreadManager())
{
RecoilSpreadManager->Fire();
}
else
{
UE_LOG(LogLyraWeaponInstance, Error, TEXT("RecoilSpreadManager could not be found!"));
}
}
float ULyraRangedWeaponInstance::GetCalculatedSpreadAngle() const
{
if (UARSRecoilSpreadManager* RecoilSpreadManager = GetRecoilSpreadManager())
{
return RecoilSpreadManager->GetCurrentSpreadAngle();
}
return 0.0f;
}
float ULyraRangedWeaponInstance::GetCalculatedSpreadAngleMultiplier() const
{
if (UARSRecoilSpreadManager* RecoilSpreadManager = GetRecoilSpreadManager())
{
return RecoilSpreadManager->GetCalculatedSpreadAngleMultiplier();
}
return 1.0f;
}
float ULyraRangedWeaponInstance::GetSpreadExponent() const
{
if (UARSRecoilSpreadManager* RecoilSpreadManager = GetRecoilSpreadManager())
{
return RecoilSpreadManager->GetSpreadExponent();
}
return 1.0f;
}
bool ULyraRangedWeaponInstance::HasFirstShotAccuracy() const
{
if (UARSRecoilSpreadManager* RecoilSpreadManager = GetRecoilSpreadManager())
{
return RecoilSpreadManager->HasFirstShotAccuracy();
}
return false;
}
UARSRecoilSpreadManager* ULyraRangedWeaponInstance::GetRecoilSpreadManager() const
{
if (!CachedRecoilSpreadManager.IsValid())
{
if (const APawn* Pawn = GetPawn())
{
if (const AController* Controller = Pawn->GetController())
{
CachedRecoilSpreadManager = Controller->FindComponentByClass<UARSRecoilSpreadManager>();
}
}
}
return CachedRecoilSpreadManager.Get();
}
📈 Section 4: Optimizing Weapon State Management
Disabling Legacy Spread Updates in Lyra
Why This Matters
The ULyraWeaponStateComponent
originally handled spread calculations through continuous ticking. Our Adaptive Recoil & Spread System makes this obsolete. This modification:
✅ Improves Performance: Eliminates unnecessary tick overhead
✅ Prevents Conflicts: Ensures single source of truth for weapon mechanics
✅ Maintains Compatibility: Preserves non-spread related functionality
Implementation Steps
1. Header File Modifications (LyraWeaponStateComponent.h
)
LyraWeaponStateComponent.h
)// Source/LyraGame/Weapons/LyraWeaponStateComponent.h
// Tracks weapon state and recent confirmed hit markers to display on screen
UCLASS()
class ULyraWeaponStateComponent : public UControllerComponent
{
GENERATED_BODY()
public:
ULyraWeaponStateComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
// Comment or remove TickComponent function declaration
// virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
};
2. Source File Updates (LyraWeaponStateComponent.cpp
)
LyraWeaponStateComponent.cpp
)// Source/LyraGame/Weapons/LyraWeaponStateComponent.cpp
ULyraWeaponStateComponent::ULyraWeaponStateComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
SetIsReplicatedByDefault(true);
// Disable ticking for better performance
PrimaryComponentTick.bStartWithTickEnabled = false;
PrimaryComponentTick.bCanEverTick = true;
}
// Remove or comment entire TickComponent function
// void ULyraWeaponStateComponent::TickComponent(...) { ... }
🔥 Section 5: Modernizing Weapon Fire Mechanics
Integrating Adaptive Recoil & Spread with Lyra's Gameplay Abilities
Architectural Overview
The ULyraGameplayAbility_RangedWeapon
ability drives weapon firing logic. We modify ULyraGameplayAbility_RangedWeapon
ability to synchronize with our recoil/spread system through three critical phases:
Initialize recoil patterns on ability start (
ActivateAbility
)Apply per-shot recoil/spread calculations (
OnTargetDataReadyCallback
)Handle post-fire recovery (
EndAbility
)
Source File Updates (LyraGameplayAbility_RangedWeapon.cpp
)
LyraGameplayAbility_RangedWeapon.cpp
)// Source/LyraGame/Weapons/LyraGameplayAbility_RangedWeapon.cpp
#include "LyraGameplayAbility_RangedWeapon.h"
#include "AbilitySystemComponent.h"
#include "AIController.h"
#include "DrawDebugHelpers.h"
#include "LyraLogChannels.h"
#include "LyraRangedWeaponInstance.h"
#include "NativeGameplayTags.h"
#include "AbilitySystem/LyraGameplayAbilityTargetData_SingleTargetHit.h"
#include "Components/ARSRecoilSpreadManager.h" // Add RecoilSpreadManager component
#include "Physics/LyraCollisionChannels.h"
#include "Weapons/LyraWeaponStateComponent.h"
void ULyraGameplayAbility_RangedWeapon::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
// Bind target data callback
UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get();
check(MyAbilityComponent);
OnTargetDataReadyCallbackDelegateHandle = MyAbilityComponent->AbilityTargetDataSetDelegate(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()).AddUObject(this, &ThisClass::OnTargetDataReadyCallback);
// Update the last firing time
ULyraRangedWeaponInstance* WeaponData = GetWeaponInstance();
check(WeaponData);
WeaponData->UpdateFiringTime();
// Begin firing sequence for recoil
AController* Controller = GetControllerFromActorInfo();
UARSRecoilSpreadManager* RecoilSpreadManager = Controller ? Controller->FindComponentByClass<UARSRecoilSpreadManager>() : nullptr;
if (RecoilSpreadManager)
{
RecoilSpreadManager->BeginFiring();
}
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
}
void ULyraGameplayAbility_RangedWeapon::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
if (IsEndAbilityValid(Handle, ActorInfo))
{
if (ScopeLockCount > 0)
{
WaitingToExecute.Add(FPostLockDelegate::CreateUObject(this, &ThisClass::EndAbility, Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled));
return;
}
UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get();
check(MyAbilityComponent);
// When ability ends, consume target data and remove delegate
MyAbilityComponent->AbilityTargetDataSetDelegate(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()).Remove(OnTargetDataReadyCallbackDelegateHandle);
MyAbilityComponent->ConsumeClientReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey());
// End firing sequence for recoil
AController* Controller = GetControllerFromActorInfo();
UARSRecoilSpreadManager* RecoilSpreadManager = Controller ? Controller->FindComponentByClass<UARSRecoilSpreadManager>() : nullptr;
if (RecoilSpreadManager)
{
RecoilSpreadManager->EndFiring();
}
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
}
void ULyraGameplayAbility_RangedWeapon::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& InData, FGameplayTag ApplicationTag)
{
UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get();
check(MyAbilityComponent);
if (const FGameplayAbilitySpec* AbilitySpec = MyAbilityComponent->FindAbilitySpecFromHandle(CurrentSpecHandle))
{
FScopedPredictionWindow ScopedPrediction(MyAbilityComponent);
// Take ownership of the target data to make sure no callbacks into game code invalidate it out from under us
FGameplayAbilityTargetDataHandle LocalTargetDataHandle(MoveTemp(const_cast<FGameplayAbilityTargetDataHandle&>(InData)));
const bool bShouldNotifyServer = CurrentActorInfo->IsLocallyControlled() && !CurrentActorInfo->IsNetAuthority();
if (bShouldNotifyServer)
{
MyAbilityComponent->CallServerSetReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey(), LocalTargetDataHandle, ApplicationTag, MyAbilityComponent->ScopedPredictionKey);
}
const bool bIsTargetDataValid = true;
bool bProjectileWeapon = false;
#if WITH_SERVER_CODE
if (!bProjectileWeapon)
{
if (AController* Controller = GetControllerFromActorInfo())
{
if (Controller->GetLocalRole() == ROLE_Authority)
{
// Confirm hit markers
if (ULyraWeaponStateComponent* WeaponStateComponent = Controller->FindComponentByClass<ULyraWeaponStateComponent>())
{
TArray<uint8> HitReplaces;
for (uint8 i = 0; (i < LocalTargetDataHandle.Num()) && (i < 255); ++i)
{
if (FGameplayAbilityTargetData_SingleTargetHit* SingleTargetHit = static_cast<FGameplayAbilityTargetData_SingleTargetHit*>(LocalTargetDataHandle.Get(i)))
{
if (SingleTargetHit->bHitReplaced)
{
HitReplaces.Add(i);
}
}
}
WeaponStateComponent->ClientConfirmTargetData(LocalTargetDataHandle.UniqueId, bIsTargetDataValid, HitReplaces);
}
}
}
}
#endif //WITH_SERVER_CODE
// See if we still have ammo
if (bIsTargetDataValid && CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo))
{
// We fired the weapon, add recoil/spread
ULyraRangedWeaponInstance* WeaponInstance = GetWeaponInstance();
check(WeaponInstance);
// Apply Recoil and Spread to weapon
WeaponInstance->ApplyRecoilAndSpread();
// Let the blueprint do stuff like apply effects to the targets
OnRangedWeaponTargetDataReady(LocalTargetDataHandle);
}
else
{
UE_LOG(LogLyraAbilitySystem, Warning, TEXT("Weapon ability %s failed to commit (bIsTargetDataValid=%d)"), *GetPathName(), bIsTargetDataValid ? 1 : 0);
K2_EndAbility();
}
}
// We've processed the data
MyAbilityComponent->ConsumeClientReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey());
}
✅ Section 6: Final Compilation & Validation
Completing the Integration Process
Build Instructions
Follow this sequence to ensure error-free compilation:
Clean Existing Build
# Visual Studio Build → Clean Solution # Manual Cleanup Delete /Binaries/ and /Intermediate/ folders
Regenerate Project Files Right-click
.uproject
→ Generate Visual Studio Project FilesFull Rebuild
Build → Build Solution (Ctrl+Shift+B)
Verify in Editor
Open Lyra project
Check for warnings in Message Log (Window → Developer Tools → Message Log)
🧠 Section 7: Configuring Weapon Patterns
Creating & Assigning Recoil/Spread Behaviors
Now you can create Recoil/Spread patterns for weapons and add them to corresponding weapon equipment instances. Check out Recoil and Spread patterns to learn pattern creation workflow.
Pattern Design Best Practices
Example Configurations
Assault Rifle
Fast vertical climb Moderate horizontal sway
0.5° base +0.2°/shot
Shotgun
Single sharp kick Slow recovery
5° base +1.5°/shot
Sniper Rifle
Minimal vertical Random horizontal
0.1° base No burst penalty
🎉 Integration Complete!
Your Lyra project is now equipped with AAA-grade recoil and spread mechanics, powered by Adaptive Recoil & Spread. You can now explore, customize, and scale your weapon systems with full confidence.
Need help or want to go further?
Join our Discord Community for support, feedback, and discussions.
🧪 Test the Full Experience
To experience the full potential of ARS in a real-world production context, we’ve prepared a dedicated demo project:
🎮 ARS_Lyra_Demo
A fully playable, ready-to-build Lyra-based example showcasing advanced recoil & spread behaviors, and debugging tools. → Download ARS_Lyra_Demo
💻 ARS_Lyra_Project (Source Code)
The complete source code for the demo project. Study or extend the implementation in your own workflow. → Download and view ARS_Lyra_Project
Last updated