Guide: Adding different hero classes to LYRA

patternfall2
Wed, 04 Dec 2024 02:25:59 +0000
Programming & Scripting

Lyra is as amazing as it is difficult to work with, and this is a solution that’s taken me far too long to develop.

I wanted to let players switch hero classes at runtime with an entirely new class, inventory/equipment, abilities, effects, and cues. Lyra will fight you hard if you don’t do it properly as it has multiple listeners that try to ensure the pawn state remains consistent even with poor network conditions. The good news is that if you know how to do it right, it’s easy!

Note: This will require creating a simple C++ function in the LyraPlayerState class. There’s no way around it, as too many of the necessary parts are not exposed to blueprint.

Lyra’s player pawns are set by a LyraPawnData variable inside the LyraPlayerState class. For an example, see “Default Pawn Data” in a Lyra Experience Definition. It contains the pawn’s class, abilities, tag relationships, input configuration, and camera. Upon pawn reset, the game automatically initializes the pawn with these elements. What the game does NOT do well is provide a way to effectively “uninitialize” the pawn so players can switch to a new one. To that end, this function will take in a new PawnData, unset the existing pawn’s abilities, replace the old PawnData with the new, add the new data’s abilities back in, and then restart the controller.

LyraPlayerState.h

public:
	UFUNCTION(BlueprintCallable, Category = "Lyra|PlayerState")
	void ForceNewPawnData(ULyraPawnData* NewData);

LyraPlayerState.cpp

#include "Inventory/LyraInventoryManagerComponent.h"
#include "Equipment/LyraQuickBarComponent.h"

void ALyraPlayerState::ForceNewPawnData(ULyraPawnData* NewData)
{
	if (NewData)
	{
		// (1) Clear abilities, cues, effects, quick bar, and inventory
		if (AbilitySystemComponent)
		{
			// Clear abilities
			for (const FGameplayAbilitySpec& AbilitySpec : AbilitySystemComponent->GetActivatableAbilities())
			{
				AbilitySystemComponent->ClearAbility(AbilitySpec.Handle);
			}

			// Clear cues
			AbilitySystemComponent->RemoveAllGameplayCues();

			// Clear effects
			FGameplayEffectQuery Query;
			AbilitySystemComponent->RemoveActiveEffects(Query);
		}

			// Clear quick bar
			if (ULyraQuickBarComponent* QuickBar = GetLyraPlayerController()->FindComponentByClass<ULyraQuickBarComponent>())
			{
				for (int32 SlotIndex = 0; SlotIndex < QuickBar->GetSlots().Num(); ++SlotIndex)
				{
					QuickBar->RemoveItemFromSlot(SlotIndex);
				}
			}

			// Clear inventory
			if (ULyraInventoryManagerComponent* InventoryManager = GetLyraPlayerController()->FindComponentByClass<ULyraInventoryManagerComponent>())
			{
				TArray<ULyraInventoryItemInstance*> AllItems = InventoryManager->GetAllItems();
				for (ULyraInventoryItemInstance* ItemInstance : AllItems)
				{
					InventoryManager->RemoveItemInstance(ItemInstance);
				}
			}

		// (2) Set new pawn data
		PawnData = NewData;

		// (3) Kill old pawn
		if (AController* Controller = GetOwningController())
		{
			if (APawn* CurrentPawn = Controller->GetPawn())
			{
				CurrentPawn->Destroy();
			}
		}

		// (4) Load abilities of the new pawn into this PlayerState's ASC
		for (const ULyraAbilitySet* AbilitySet : PawnData->AbilitySets)
		{
			if (AbilitySet)
			{
				AbilitySet->GiveToAbilitySystem(AbilitySystemComponent, nullptr);
			}
		}

		// (5) Load abilities from the current experience's action sets
		if (ULyraExperienceManagerComponent* ExperienceManager = GetWorld()->GetGameState()->FindComponentByClass<ULyraExperienceManagerComponent>())
		{
			if (const ULyraExperienceDefinition* CurrentExperience = ExperienceManager->GetCurrentExperienceChecked())
			{
				for (const UGameFeatureAction* Action : CurrentExperience->Actions)
				{
					if (const UGameFeatureAction_AddAbilities* AddAbilitiesAction = Cast<UGameFeatureAction_AddAbilities>(Action))
					{
						for (const FGameFeatureAbilitiesEntry& Entry : AddAbilitiesAction->AbilitiesList)
						{
							for (const FLyraAbilityGrant& Ability : Entry.GrantedAbilities)
							{
								if (!Ability.AbilityType.IsNull())
								{
									FGameplayAbilitySpec NewAbilitySpec(Ability.AbilityType.LoadSynchronous());
									AbilitySystemComponent->GiveAbility(NewAbilitySpec);
								}
							}
							for (const TSoftObjectPtr<const ULyraAbilitySet>& SetPtr : Entry.GrantedAbilitySets)
							{
								if (const ULyraAbilitySet* Set = SetPtr.Get())
								{
									Set->GiveToAbilitySystem(CastChecked<ULyraAbilitySystemComponent>(AbilitySystemComponent), nullptr);
								}
							}
						}
					}
				}
			}
		}

		// (6) Restart controller to spawn and possess new pawn
		if (AController* Controller = GetOwningController())
		{
			if (AGameModeBase* GameMode = GetWorld()->GetAuthGameMode<AGameModeBase>())
			{
				GameMode->RestartPlayer(Controller);
			}
		}
	}
	else
	{
		PawnData = nullptr;
		PawnData = Cast<ALyraGameMode>(GetWorld()->GetAuthGameMode())->GetPawnDataForController(GetLyraPlayerController());
	}
}

This function is blueprint accessible so you can add the node to a Character Selection UI.

2 posts - 2 participants