Lyra: Unresolved external symbols for protected virtual void functions after extending Lyra class?

I am on the newer side for C++ in UE, so this may be a simple beginner misunderstanding.

I have a custom GameFeature Plugin, TUGG.

I want to add extend a base game Lyra class, ULyraGameplayAbility_FromEquipment
to add my own version of
ULyraGameplayAbility_RangedWeapon
with my own implementation.

To start, I have just copy/replaced all names from “Lyra” to “TUGG”. Everything has been renamed correctly for it to build.

I have added “LyraGame” to PrivateDependencyModuleNames, and it appears the compiler can find the files correctly, but for some reason on build, I am getting many unresolved external symbol errors that appear to reference the parent classes of ULyraGameplayAbility_FromEquipment.

This copy/paste works successfully from the game source folder, adjacent to the default Lyra one.

So what is happening? Why can I not reference base game classes from my custom GameFeature Module? Can GameFeatures not reference the base game? What is the correct way for me to extend base game Lyra classes?

UPDATE 1: Potentially relevant warning when creating the class:

Not sure what this means yet. Will update if I find out.

Here are the base classes in question:

ULyraGameplayAbility_FromEquipment .h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "AbilitySystem/Abilities/LyraGameplayAbility.h"

#include "LyraGameplayAbility_FromEquipment.generated.h"

class ULyraEquipmentInstance;
class ULyraInventoryItemInstance;

/**
 * ULyraGameplayAbility_FromEquipment
 *
 * An ability granted by and associated with an equipment instance
 */
UCLASS()
class ULyraGameplayAbility_FromEquipment : public ULyraGameplayAbility
{
	GENERATED_BODY()

public:

	ULyraGameplayAbility_FromEquipment(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

	UFUNCTION(BlueprintCallable, Category="Lyra|Ability")
	ULyraEquipmentInstance* GetAssociatedEquipment() const;

	UFUNCTION(BlueprintCallable, Category = "Lyra|Ability")
	ULyraInventoryItemInstance* GetAssociatedItem() const;

#if WITH_EDITOR
	virtual EDataValidationResult IsDataValid(TArray<FText>& ValidationErrors) override;
#endif

};

ULyraGameplayAbility_RangedWeapon.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Equipment/LyraGameplayAbility_FromEquipment.h"

#include "LyraGameplayAbility_RangedWeapon.generated.h"

class ULyraRangedWeaponInstance;
class APawn;

/** Defines where an ability starts its trace from and where it should face */
UENUM(BlueprintType)
enum class ELyraAbilityTargetingSource : uint8
{
	// From the player's camera towards camera focus
	CameraTowardsFocus,
	// From the pawn's center, in the pawn's orientation
	PawnForward,
	// From the pawn's center, oriented towards camera focus
	PawnTowardsFocus,
	// From the weapon's muzzle or location, in the pawn's orientation
	WeaponForward,
	// From the weapon's muzzle or location, towards camera focus
	WeaponTowardsFocus,
	// Custom blueprint-specified source location
	Custom
};



/**
 * ULyraGameplayAbility_RangedWeapon
 *
 * An ability granted by and associated with a ranged weapon instance
 */
UCLASS()
class ULyraGameplayAbility_RangedWeapon : public ULyraGameplayAbility_FromEquipment
{
	GENERATED_BODY()

public:

	ULyraGameplayAbility_RangedWeapon(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

	UFUNCTION(BlueprintCallable, Category="Lyra|Ability")
	ULyraRangedWeaponInstance* GetWeaponInstance() const;

	//~UGameplayAbility interface
	virtual bool CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags = nullptr, const FGameplayTagContainer* TargetTags = nullptr, OUT FGameplayTagContainer* OptionalRelevantTags = nullptr) const override;
	virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
	virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override;
	//~End of UGameplayAbility interface

protected:
	struct FRangedWeaponFiringInput
	{
		// Start of the trace
		FVector StartTrace;

		// End of the trace if aim were perfect
		FVector EndAim;

		// The direction of the trace if aim were perfect
		FVector AimDir;

		// The weapon instance / source of weapon data
		ULyraRangedWeaponInstance* WeaponData = nullptr;

		// Can we play bullet FX for hits during this trace
		bool bCanPlayBulletFX = false;

		FRangedWeaponFiringInput()
			: StartTrace(ForceInitToZero)
			, EndAim(ForceInitToZero)
			, AimDir(ForceInitToZero)
		{
		}
	};

protected:
	static int32 FindFirstPawnHitResult(const TArray<FHitResult>& HitResults);

	// Does a single weapon trace, either sweeping or ray depending on if SweepRadius is above zero
	FHitResult WeaponTrace(const FVector& StartTrace, const FVector& EndTrace, float SweepRadius, bool bIsSimulated, OUT TArray<FHitResult>& OutHitResults) const;

	// Wrapper around WeaponTrace to handle trying to do a ray trace before falling back to a sweep trace if there were no hits and SweepRadius is above zero 
	FHitResult DoSingleBulletTrace(const FVector& StartTrace, const FVector& EndTrace, float SweepRadius, bool bIsSimulated, OUT TArray<FHitResult>& OutHits) const;

	// Traces all of the bullets in a single cartridge
	void TraceBulletsInCartridge(const FRangedWeaponFiringInput& InputData, OUT TArray<FHitResult>& OutHits);

	virtual void AddAdditionalTraceIgnoreActors(FCollisionQueryParams& TraceParams) const;

	// Determine the trace channel to use for the weapon trace(s)
	virtual ECollisionChannel DetermineTraceChannel(FCollisionQueryParams& TraceParams, bool bIsSimulated) const;

	void PerformLocalTargeting(OUT TArray<FHitResult>& OutHits);

	FVector GetWeaponTargetingSourceLocation() const;
	FTransform GetTargetingTransform(APawn* SourcePawn, ELyraAbilityTargetingSource Source) const;

	void OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& InData, FGameplayTag ApplicationTag);

	UFUNCTION(BlueprintCallable)
	void StartRangedWeaponTargeting();

	// Called when target data is ready
	UFUNCTION(BlueprintImplementableEvent)
	void OnRangedWeaponTargetDataReady(const FGameplayAbilityTargetDataHandle& TargetData);

private:
	FDelegateHandle OnTargetDataReadyCallbackDelegateHandle;
};

2 Likes

Okay so I solved this problem. Here is a detailed solution in hopes of helping someone else.

Please note as I said in the first line, I am newer to C++ for UE. You may know better than me if you’re reading this in the future.

So the problem was, the claim “Lyra is meant to be built on top of” does not mean that "Lyra is meant to be built on top of, modified and directly extended. Here is a break down of why I say this.

PROBLEM
I wanted to totally replace the weapon ballistics for Lyra, but keep everything else.

  1. I intend to replace the bloom with patterns.
  2. I needed to create a custom class to calculate the new traces.
  3. By examining the current system, I determined that Lyras classes (mentioned above) ULyraGameplayAbility_FromEquipment and ULyraGameplayAbility_RangedWeapon are the ones I need to modify or extend.
  4. While adding my class, UTUGGGameplayAbility_RangedWeapon alongside the Lyra version in my main project folder works fine, I am going to be moving Lyra into many different plugins as a part of my workflow to reuse these features in future prototypes.
  5. When I move my custom Ranged Weapon class (UTUGGGameplayAbility_RangedWeapon) outside of the project source into a plugin, I got 198 unresolved errors, featured in the above image.

THE SOLUTION - All 3 steps are required!

  1. Add all required (but not explicitly stated) plugins/modules (including your game module) as public or private dependencies in your Plugin.Build.cs. Here is the relevant section from mine.

		PublicDependencyModuleNames.AddRange(
			new string[]
			{
				"Core",
				"LyraGame",			 //REQUIRED TO EXTEND LyraGameplayAbility_RangedComponent
				"ModularGameplay",   //REQUIRED BY LYRA TO EXTEND LyraGameplayAbility_RangedComponent
				"CommonGame",		 //REQUIRED BY LYRA TO EXTEND LyraGameplayAbility_RangedComponent
				"GameplayAbilities", //REQUIRED BY LYRA TO EXTEND LyraGameplayAbility_RangedComponent
				"GameplayTags",		 //REQUIRED BY LYRA TO EXTEND LyraGameplayAbility_RangedComponent
				"AIModule",			 //REQUIRED BY LYRA TO EXTEND LyraGameplayAbility_RangedComponent
				"GameplayTasks"		 //REQUIRED BY LYRA TO EXTEND LyraGameplayAbility_RangedComponent
			}
			);
			
		
		PrivateDependencyModuleNames.AddRange(
			new string[]
			{
				"CoreUObject",
				"Engine",
				"Slate",
				"SlateCore",
				"LyraGame",	       //REQUIRED TO EXTEND LyraGameplayAbility_RangedComponent
			}
			);
  1. Add all required plugins to your .uplugin. Here is mine.

	"Plugins": [
		{
			"Name": "CommonGame",
			"Enabled": true
		},
		{
			"Name": "ModularGameplay",
			"Enabled": true
		},
		{
			"Name": "GameplayAbilities",
			"Enabled": true
		}
	]
  1. For each error, look at the unresolved symbol. Here are a whole bunch of mine from the mine post.

    You can read some of the following from the image above:
unresolved external symbol "protected: virtual void __cded ULyraGameplayAbility::RangedWeapon <more irrelevant text>

unresolved external symbol "protected: virtual void __cded ULyraGameplayAbility::OnGiveAbility<more irrelevant text>"

By reading these lines, you can reasonably see 2 things.

  1. Something about Lyra’s “GameplayAbilities” is missing; SOLUTION PART 1: the GameplayAbilities dependency needed to be added to my plugin.Build.cs.
  2. ULyraGameplayAbility itself may not be “ModuleExported” as the second error screenshot shows. SOLUTION PART 2: You have to manually add each required/related class to the LYRAGAME_API. Here is an example of me modifying the original Lyra class.
class LYRAGAME_API ULyraGameplayAbility_RangedWeapon : public ULyraGameplayAbility_FromEquipment

LYRAGAME_API is the key thing to add to get it to work.

Finally, once you have followed these steps, added the modules/plugins to you Build.cs by examining the unresolved symbols, added needed plugins to your Plugin.uplugin, and finally added LYRAGAME_API to all of the needed classes, enums, structs and others that the errors tell you it cannot resolve, your project should compile as expected.

It is my novice opinion that if Lyra is meant to built upon, everything should be included in the API so it can be extended or replaced appropriately, but perhaps there is a reason they are not included, but I have no issues with this solution at the moment, so please feel free to enlighten me!

I hope this helps someone.

7 Likes

Great write up and the missing “LYRAGAME_API” part is kind a slip-up on Epic’s part.

1 Like

Agree the write-up is good.

Epic doesn’t want to expose more than they really have to though, so I don’t fault them for not exporting everything.

It’s trivial to merge API exports so it’s no problem for Epic or custom engine devs to deal with it. Just maintain 2 branches – 1 being the official Epic version of Lyra, and 2nd being your patches to it, like API exports.

Those plugins are dependencies because they have features that ability supports, that is true.

However, if you have any amount of custom C++ in the project at all, you’ll have those same dependencies yourself anyway, so the “REQUIRED BY LYRA TO EXTEND” is really a misleading comment to put next to them.

They’re required by C++ code that implement these features. Lyra or otherwise.

Other than that, nice work with this post. These are definitely some common issues people will have when trying to extend Lyra C++. Thanks, and good job explaining them. :+1:

1 Like

Does that mean those lines in three steps except "LyraGame" is required for every GameFeaturePlugin (to use classes in main game source code)? Or how should I know what dependencies do I need for a random GFP since IDE is not complaining? It would be great if there’s some documentation about it.

Btw Xist you’ve made great tutorial on the C++ side of Lyra, many thanks. I think you could suggest people post issues and solutions (or links to this forum) in your GitHub repo issue or discussion . There really should be one place to gather content specifically for Lyra, instead of just Googling and digging in forum posts and issue trackers amongst unrelated contents.

GFP “A” will depend on GFP “B” if any of the assets in “A” reference assets that are in “B”. I’m pretty sure when you try to save an asset in GFP “A” it will complain with error messages if the asset refers to assets in “B”, unless “B” is listed as an explicit dependency of “A”.

You can check to see which GFPs are dependencies of “A” by looking at the GameFeatureData asset (Plugins/GameFeatures/A/Content/A.uasset)

I would love to see such a resource. I continue to hope that Epic releases Lyra on GitHub or some other similar service, including forums, etc.

Please Epic! :smile: