GameFeature in Lyra (2)

Jangmanbo·2025년 6월 3일

Expereince가 활성화될 때 GameFeature를 어떻게 자동으로 활성화할까?

먼저 LyraExperienceDefinition 에셋 > GameFeatureToEnable에 GameFeature를 추가

ULyraExperienceManagerComponnent

  • StartExperienceLoad: Experience 에셋을 로드
  • OnExperienceLoadComplete: CurrentExperienceGameFeaturesToEnable를 로드, 활성화
  • OnExperienceFullLoadCompleted: GameFeature가 모두 로드됐을 때 호출



GameFeature마다 InputConfig 변경

LyraHeroComponent가 추가된 Default 캐릭터 B_Hero_Default

ShooterCore 플러그인에서는 이 B_Hero_Default를 상속받은 B_Hero_Shooter_Mannequin을 사용하므로 PawnData에서 설정


인풋은 LyraHeroComponent에서 관리한다 참고
그런데 B_Hero_Shooter_Mannequin>LyraHeroComponent을 보면 DefaultInputConfig가 비어있다

전에 배웠던 LyraHeroComponent>DefaultInputConfig에 InputConfig를 다 넣어두는 방식은 좀 하드코딩 (결국에는 현재 모드에 따라 조건문을 넣어서 골라내야 함)

따라서 GameFeature의 Action을 통해 동적으로 InputConfig를 변경해보자

LyraExperienceDefiniton에서 ActionSet/Action을 들고 있다
(ActionSet은 그냥 Action 리스트, 한 번에 관리하기 위함이라 별 차이는 없다)

Action은 아까 얘기한 ULyraExperienceManagerComponnent::OnExperienceFullLoadCompleted에서 실행한다

void ULyraExperienceManagerComponent::OnExperienceFullLoadCompleted()
{
	// ...
    
    // GameFeatureSubSystem이 GameFeatureAction을 관리
    // GameFeatureSubSystem은 EngineSubSystem이기 때문에 어떤 GameInstance에 액션을 바인드해야하는지를 알아야 함 (동일 엔진에 여러 GameInstance가 있을 수 있기 때문)
    // 따라서 액션을 활성화할 때 WorldContext를 넘겨준다
	FGameFeatureActivatingContext Context;

	const FWorldContext* ExistingWorldContext = GEngine->GetWorldContextFromWorld(GetWorld());
	if (ExistingWorldContext)
	{
    	// WorldContextHandle 세팅
		Context.SetRequiredWorldContextHandle(ExistingWorldContext->ContextHandle);
	}

	auto ActivateListOfActions = [&Context](const TArray<UGameFeatureAction*>& ActionList)
	{
		for (UGameFeatureAction* Action : ActionList)
		{
			if (Action != nullptr)
			{
				// 액션을 등록, 로드, 활성화
				Action->OnGameFeatureRegistering();
				Action->OnGameFeatureLoading();
				Action->OnGameFeatureActivating(Context);
			}
		}
	};

	// CurrentExperience에서 설정한 ActionSet, Action들을 모두 활성화
	ActivateListOfActions(CurrentExperience->Actions);
	for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
	{
		if (ActionSet != nullptr)
		{
			ActivateListOfActions(ActionSet->Actions);
		}
	}

	// ...
}

Lyra의 GameFeatureAction

UGameFeatureAction_WorldActionBase을 제외한 액션들은 모두 UGameFeatureAction_WorldActionBase을 상속받았다.

UCLASS(Abstract)
class UGameFeatureAction_WorldActionBase : public UGameFeatureAction
{
	GENERATED_BODY()

public:
	//~ Begin UGameFeatureAction interface
    // ULyraExperienceManagerComponent::OnExperienceFullLoadCompleted에서 설명했던 이유로 WorldContext를 인자로 받음
	virtual void OnGameFeatureActivating(FGameFeatureActivatingContext& Context) override;
	virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
	//~ End UGameFeatureAction interface

private:
	void HandleGameInstanceStart(UGameInstance* GameInstance, FGameFeatureStateChangeContext ChangeContext);

	/** Override with the action-specific logic */
	virtual void AddToWorld(const FWorldContext& WorldContext, const FGameFeatureStateChangeContext& ChangeContext) PURE_VIRTUAL(UGameFeatureAction_WorldActionBase::AddToWorld,);

private:
	TMap<FGameFeatureStateChangeContext, FDelegateHandle> GameInstanceStartHandles;
};

PURE_VIRTUAL

  • 순수 가상 함수 선언 매크로

UGameFeatureAction_AddInputConfig

void UGameFeatureAction_AddInputConfig::AddToWorld(const FWorldContext& WorldContext, const FGameFeatureStateChangeContext& ChangeContext)
{
	UWorld* World = WorldContext.World();
	UGameInstance* GameInstance = WorldContext.OwningGameInstance;
	FPerContextData& ActiveData = ContextData.FindOrAdd(ChangeContext);
	
	if (GameInstance && World && World->IsGameWorld())
	{
		if (UGameFrameworkComponentManager* ComponentMan = UGameInstance::GetSubsystem<UGameFrameworkComponentManager>(GameInstance))
		{
			UGameFrameworkComponentManager::FExtensionHandlerDelegate AddConfigDelegate =
				UGameFrameworkComponentManager::FExtensionHandlerDelegate::CreateUObject(this, &ThisClass::HandlePawnExtension, ChangeContext);
			
            // 모든 Pawn에 미리 ExtensionHandler 등록
            // 이벤트 호출되면 HandlePawnExtension에서 InputConfig를 Add/Remove 한다
			TSharedPtr<FComponentRequestHandle> ExtensionRequestHandle = ComponentMan->AddExtensionHandler(APawn::StaticClass(), AddConfigDelegate);
			ActiveData.ExtensionRequestHandles.Add(ExtensionRequestHandle);
		}
	}
}

void UGameFeatureAction_AddInputConfig::HandlePawnExtension(AActor* Actor, FName EventName, FGameFeatureStateChangeContext ChangeContext)
{
	APawn* AsPawn = CastChecked<APawn>(Actor);
	FPerContextData& ActiveData = ContextData.FindOrAdd(ChangeContext);

	if (EventName == UGameFrameworkComponentManager::NAME_ExtensionAdded || EventName == ULyraHeroComponent::NAME_BindInputsNow)
	{
		AddInputConfig(AsPawn, ActiveData);
	}
	else if (EventName == UGameFrameworkComponentManager::NAME_ExtensionRemoved || EventName == UGameFrameworkComponentManager::NAME_ReceiverRemoved)
	{
		RemoveInputConfig(AsPawn, ActiveData);
	}
}

그런데 ExtensionHandler 등록 시점에 플레이어 폰이 아직 생성되기 전이었다면, 플레이어 폰은 InputConfig를 추가하지 못한다.

void ULyraHeroComponent::InitializePlayerInput(UInputComponent* PlayerInputComponent)
{
	// ...
 
	UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(const_cast<APlayerController*>(PC), NAME_BindInputsNow);
	UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(const_cast<APawn*>(Pawn), NAME_BindInputsNow);
}

그래서 폰이 생성되고 액션을 바인드하는 시점인 LyraHeroComponent::InitializePlayerInput에서 직접 ExtensionEvent를 보낸다.

0개의 댓글