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

먼저 LyraExperienceDefinition 에셋 > GameFeatureToEnable에 GameFeature를 추가
ULyraExperienceManagerComponnent
CurrentExperience의 GameFeaturesToEnable를 로드, 활성화
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);
}
}
// ...
}

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
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를 보낸다.