보키_기록용
Lyra에서 Input Mapping Context 살펴보기 (2) 본문
내용이 너무 길어져서 나눔. Lyra에서 Input Mapping Context 살펴보기 (1)
이전 글에서 PMI 방식 중 GameFeatureData를 사용하는 방식을 설명한다.
Enhanced Input System을 위한 Game Feature 추가하기
- ShooterCore, TopDownArena (UGameFeatureData) : 사실상 방식은 같다.
GameFeatureData[ShooterCore]
1. 아래 그림의 빨간줄부분에 들어갈 GameFeatureAction을 상속받은 GameFeatureAction_AddInputConfig를 만든다.
GameFeatureAction_AddInputConfig.h
<hide/>
//GameFeatureAction_AddInputConfig.h
#pragma once
#include "CoreMinimal.h"
#include "GameFeatureAction.h"
#include "Input/DediTestMappableConfigPair.h"
#include "GameFeatureAction_AddInputConfig.generated.h"
/**
*
*/
UCLASS(meta=(DisplayName = "Add Input Config"))
class DEDISERVERTEST_API UGameFeatureAction_AddInputConfig : public UGameFeatureAction
{
GENERATED_BODY()
public:
virtual void OnGameFeatureRegistering() override;
virtual void OnGameFeatureActivating(FGameFeatureActivatingContext& Context) override;
virtual void OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context) override;
virtual void OnGameFeatureUnregistering() override;
TArray<FMappableConfigPair> InputConfigs;
};
GameFeatureAction_AddInputConfig.cpp
<hide/>
//GameFeatureAction_AddInputConfig.cpp
#include "GameFeatures/GameFeatureAction_AddInputConfig.h"
void UGameFeatureAction_AddInputConfig::OnGameFeatureRegistering()
{
Super::OnGameFeatureRegistering();
for (const FMappableConfigPair& Pair : InputConfigs)
{
FMappableConfigPair::RegisterPair(Pair);
}
}
void UGameFeatureAction_AddInputConfig::OnGameFeatureActivating(FGameFeatureActivatingContext& Context)
{
Super::OnGameFeatureActivating(Context);
for (const FMappableConfigPair& Pair : InputConfigs)
{
if (Pair.bShouldActivateAutomatically)
{
FMappableConfigPair::ActivatePair(Pair);
}
}
}
void UGameFeatureAction_AddInputConfig::OnGameFeatureDeactivating(FGameFeatureDeactivatingContext& Context)
{
Super::OnGameFeatureDeactivating(Context);
for (const FMappableConfigPair& Pair : InputConfigs)
{
FMappableConfigPair::DeactivatePair(Pair);
}
}
void UGameFeatureAction_AddInputConfig::OnGameFeatureUnregistering()
{
Super::OnGameFeatureUnregistering();
for (const FMappableConfigPair& Pair : InputConfigs)
{
FMappableConfigPair::UnregisterPair(Pair);
}
}
2. 하다보면 #include "Input/DediTestMappableConfigPair.h"가 없다고 뜰텐데, 해당 파일을 만든다. TArray<FMappableConfigPair> InputConfigs;에서 FMappableConfigPair가 정의된 파일이다.
DediTestMappableConfigPair.h
<hide/>
//DediTestMappableConfigPair.h
#pragma once
#include "CoreMinimal.h"
#include "CommonInput/Public/CommonInputBaseTypes.h"
#include "GameplayTagsManager.h"
#include "DediTestMappableConfigPair.generated.h"
class UPlayerMappableInputConfig;
/** A container to organize loaded player mappable configs to their CommonUI input type */
USTRUCT(BlueprintType)
struct FLoadedMappableConfigPair
{
GENERATED_BODY()
FLoadedMappableConfigPair() = default;
FLoadedMappableConfigPair(const UPlayerMappableInputConfig* InConfig, ECommonInputType InType, const bool InIsActive)
: Config(InConfig)
, Type(InType)
, bIsActive(InIsActive)
{}
/** The player mappable input config that should be applied to the Enhanced Input subsystem */
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
const UPlayerMappableInputConfig* Config = nullptr;
/** The type of device that this mapping config should be applied to */
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
ECommonInputType Type = ECommonInputType::Count;
/** If this config is currently active. A config is marked as active when it's owning GFA is active */
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
bool bIsActive = false;
};
/** A container to organize potentially unloaded player mappable configs to their CommonUI input type */
USTRUCT()
struct FMappableConfigPair
{
GENERATED_BODY()
FMappableConfigPair() = default;
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UPlayerMappableInputConfig> Config;
/**
* The type of config that this is. Useful for filtering out configs by the current input device
* for things like the settings screen, or if you only want to apply this config when a certain
* input type is being used.
*/
UPROPERTY(EditAnywhere)
ECommonInputType Type = ECommonInputType::Count;
/**
* Container of platform traits that must be set in order for this input to be activated.
*
* If the platform does not have one of the traits specified it can still be registered, but cannot
* be activated.
*/
UPROPERTY(EditAnywhere)
FGameplayTagContainer DependentPlatformTraits;
/**
* If the current platform has any of these traits, then this config will not be actived.
*/
UPROPERTY(EditAnywhere)
FGameplayTagContainer ExcludedPlatformTraits;
/** If true, then this input config will be activated when it's associated Game Feature is activated.
* This is normally the desirable behavior
*/
UPROPERTY(EditAnywhere)
bool bShouldActivateAutomatically = true;
/** Returns true if this config pair can be activated based on the current platform traits and settings. */
bool CanBeActivated() const;
/**
* Registers the given config mapping with the settings
*/
static bool RegisterPair(const FMappableConfigPair& Pair);
/**
* Activates the given config mapping in the settings. This will also register the mapping
* if it hasn't been yet.
*/
static bool ActivatePair(const FMappableConfigPair& Pair);
static void DeactivatePair(const FMappableConfigPair& Pair);
static void UnregisterPair(const FMappableConfigPair& Pair);
};
DediTestMappableConfigPair.cpp
<hide/>
//DediTestMappableConfigPair.cpp
#include "Input/DediTestMappableConfigPair.h"
#include "System/DediTestAssetManager.h"
#include "Settings/DediTestSettingLocal.h"
#include "CommonUISettings.h"
#include "ICommonUIModule.h"
#include "PlayerMappableInputConfig.h"
bool FMappableConfigPair::CanBeActivated() const
{
const FGameplayTagContainer& PlatformTraits = ICommonUIModule::GetSettings().GetPlatformTraits();
// If the current platform does NOT have all the dependent traits, then don't activate it
if (!DependentPlatformTraits.IsEmpty() && !PlatformTraits.HasAll(DependentPlatformTraits))
{
return false;
}
// If the platform has any of the excluded traits, then we shouldn't activate this config.
if (!ExcludedPlatformTraits.IsEmpty() && PlatformTraits.HasAny(ExcludedPlatformTraits))
{
return false;
}
return true;
}
bool FMappableConfigPair::RegisterPair(const FMappableConfigPair& Pair)
{
UDediTestAssetManager& AssetManager = UDediTestAssetManager::Get();
if (UDediTestSettingLocal* Settings = UDediTestSettingLocal::Get())
{
if(const UPlayerMappableInputConfig* LoadedConfig = AssetManager.GetAsset((Pair.Config)))
{
Settings->RegisterInputConfig(Pair.Type, LoadedConfig, false);
return true;
}
}
return false;
}
bool FMappableConfigPair::ActivatePair(const FMappableConfigPair& Pair)
{
UDediTestAssetManager& AssetManager = UDediTestAssetManager::Get();
// Only activate a pair that has been successfully registered
if (FMappableConfigPair::RegisterPair(Pair) && Pair.CanBeActivated())
{
if (UDediTestSettingLocal* Settings = UDediTestSettingLocal::Get())
{
if (const UPlayerMappableInputConfig* LoadedConfig = AssetManager.GetAsset(Pair.Config))
{
Settings->ActivateInputConfig(LoadedConfig);
return true;
}
}
}
return false;
}
void FMappableConfigPair::DeactivatePair(const FMappableConfigPair& Pair)
{
UDediTestAssetManager& AssetManager = UDediTestAssetManager::Get();
if (UDediTestSettingLocal* Settings = UDediTestSettingLocal::Get())
{
if (const UPlayerMappableInputConfig* LoadedConfig = AssetManager.GetAsset(Pair.Config))
{
Settings->DeactivateInputConfig(LoadedConfig);
}
}
}
void FMappableConfigPair::UnregisterPair(const FMappableConfigPair& Pair)
{
UDediTestAssetManager& AssetManager = UDediTestAssetManager::Get();
if (UDediTestSettingLocal* Settings = UDediTestSettingLocal::Get())
{
if (const UPlayerMappableInputConfig* LoadedConfig = AssetManager.GetAsset(Pair.Config))
{
Settings->UnregisterInputConfig(LoadedConfig);
}
}
}
ECommonInputType때문에 #include "CommonInput/Public/CommonInputBaseTypes.h"을 해줘야 하는데, 해당 플러그인은 Common UI로 검색해서 받으면된다.
3. GameUserSettings를 상속받은 DediTestSettingLocal을 만든다.
DediTestSettingLocal.h
<hide/>
// DediTestSettingLocal.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/GameUserSettings.h"
#include "Input/DediTestMappableConfigPair.h"
#include "DediTestSettingLocal.generated.h"
/**
*
*/
UCLASS()
class DEDISERVERTEST_API UDediTestSettingLocal : public UGameUserSettings
{
GENERATED_BODY()
public:
UDediTestSettingLocal();
static UDediTestSettingLocal* Get();
//~UGameUserSettings interface
//virtual void ApplyNonResolutionSettings() override;
//~End of UGameUserSettings interface
//void OnExperienceLoaded();
public:
DECLARE_EVENT_OneParam(UDediTestSettingLocal, FInputConfigDelegate, const FLoadedMappableConfigPair& /*Config*/);
/** Delegate called when a new input config has been registered */
FInputConfigDelegate OnInputConfigRegistered;
/** Delegate called when a registered input config has been activated */
FInputConfigDelegate OnInputConfigActivated;
/** Delegate called when a registered input config has been deactivate */
FInputConfigDelegate OnInputConfigDeactivated;
/** Register the given input config with the settings to make it available to the player. */
void RegisterInputConfig(ECommonInputType Type, const UPlayerMappableInputConfig* NewConfig, const bool bIsActive);
/** Unregister the given input config. Returns the number of configs removed. */
int32 UnregisterInputConfig(const UPlayerMappableInputConfig* ConfigToRemove);
/** Set a registered input config as active */
void ActivateInputConfig(const UPlayerMappableInputConfig* Config);
/** Deactivate a registered config */
void DeactivateInputConfig(const UPlayerMappableInputConfig* Config);
/** Get all currently registered input configs */
const TArray<FLoadedMappableConfigPair>& GetAllRegisteredInputConfigs() const { return RegisteredInputConfigs; }
private:
UPROPERTY(VisibleAnywhere)
TArray<FLoadedMappableConfigPair> RegisteredInputConfigs;
private:
//void ReapplyThingsDueToPossibleDeviceProfileChange();
};
DediTestSettingLocal.cpp
<hide/>
// DediTestSettingLocal.cpp
#include "Settings/DediTestSettingLocal.h"
UDediTestSettingLocal::UDediTestSettingLocal()
{
}
UDediTestSettingLocal* UDediTestSettingLocal::Get()
{
return GEngine ? CastChecked<UDediTestSettingLocal>(GEngine->GetGameUserSettings()) : nullptr;
}
//void UDediTestSettingLocal::ApplyNonResolutionSettings()
//{
// Super::ApplyNonResolutionSettings();
//
// //...
//}
//
//void UDediTestSettingLocal::OnExperienceLoaded()
//{
// ReapplyThingsDueToPossibleDeviceProfileChange();
//}
//
//void UDediTestSettingLocal::ReapplyThingsDueToPossibleDeviceProfileChange()
//{
// ApplyNonResolutionSettings();
//}
void UDediTestSettingLocal::RegisterInputConfig(ECommonInputType Type, const UPlayerMappableInputConfig* NewConfig, const bool bIsActive)
{
if (NewConfig)
{
const int32 ExistingConfigIdx = RegisteredInputConfigs.IndexOfByPredicate([&NewConfig](const FLoadedMappableConfigPair& Pair) { return Pair.Config == NewConfig; });
if (ExistingConfigIdx == INDEX_NONE)
{
const int32 NumAdded = RegisteredInputConfigs.Add(FLoadedMappableConfigPair(NewConfig, Type, bIsActive));
if (NumAdded != INDEX_NONE)
{
OnInputConfigRegistered.Broadcast(RegisteredInputConfigs[NumAdded]);
}
}
}
}
int32 UDediTestSettingLocal::UnregisterInputConfig(const UPlayerMappableInputConfig* ConfigToRemove)
{
if (ConfigToRemove)
{
const int32 Index = RegisteredInputConfigs.IndexOfByPredicate([&ConfigToRemove](const FLoadedMappableConfigPair& Pair) { return Pair.Config == ConfigToRemove; });
if (Index != INDEX_NONE)
{
RegisteredInputConfigs.RemoveAt(Index);
return 1;
}
}
return INDEX_NONE;
}
void UDediTestSettingLocal::ActivateInputConfig(const UPlayerMappableInputConfig* Config)
{
if (Config)
{
const int32 ExistingConfigIdx = RegisteredInputConfigs.IndexOfByPredicate([&Config](const FLoadedMappableConfigPair& Pair) { return Pair.Config == Config; });
if (ExistingConfigIdx != INDEX_NONE)
{
RegisteredInputConfigs[ExistingConfigIdx].bIsActive = true;
OnInputConfigActivated.Broadcast(RegisteredInputConfigs[ExistingConfigIdx]);
}
}
}
void UDediTestSettingLocal::DeactivateInputConfig(const UPlayerMappableInputConfig* Config)
{
if (Config)
{
const int32 ExistingConfigIdx = RegisteredInputConfigs.IndexOfByPredicate([&Config](const FLoadedMappableConfigPair& Pair) { return Pair.Config == Config; });
if (ExistingConfigIdx != INDEX_NONE)
{
RegisteredInputConfigs[ExistingConfigIdx].bIsActive = false;
OnInputConfigDeactivated.Broadcast(RegisteredInputConfigs[ExistingConfigIdx]);
}
}
}
OnInputConfigActivated부분은 하는 역할이 없는거같은데 아직 모르겠다...
DediTestSettingLocal는 프로젝트 세팅 > 엔진 > 일반 세팅 > Default Classes > 게임 유저 세팅 클래스에 넣어야한다.
4. EnhancedInputComponent를 상속받아 만든 InputComponent에 AddInputMappings()를 추가한다.
DediServerTestInputComponent
<hide/>
// DediServerTestInputComponent.h
UCLASS()
class DEDISERVERTEST_API UDediServerTestInputComponent : public UEnhancedInputComponent
{
GENERATED_BODY()
public:
// ...
void AddInputMappings(const UTestInputConfig* InputConfig, UEnhancedInputLocalPlayerSubsystem* InputSubsystem) const;
void RemoveInputMappings(const UTestInputConfig* InputConfig, UEnhancedInputLocalPlayerSubsystem* InputSubsystem) const;
// ...
};
// DediServerTestInputComponent.cpp
void UDediServerTestInputComponent::AddInputMappings(const UTestInputConfig* InputConfig, UEnhancedInputLocalPlayerSubsystem* InputSubsystem) const
{
check(InputConfig);
check(InputSubsystem);
/*ULyraLocalPlayer* LocalPlayer = InputSubsystem->GetLocalPlayer<ULyraLocalPlayer>();
check(LocalPlayer);*/
// Add any registered input mappings from the settings!
if (UDediTestSettingLocal* LocalSettings = UDediTestSettingLocal::Get())
{
// We don't want to ignore keys that were "Down" when we add the mapping context
// This allows you to die holding a movement key, keep holding while waiting for respawn,
// and have it be applied after you respawn immediately. Leaving bIgnoreAllPressedKeysUntilRelease
// to it's default "true" state would require the player to release the movement key,
// and press it again when they respawn
FModifyContextOptions Options = {};
Options.bIgnoreAllPressedKeysUntilRelease = false;
// Add all registered configs, which will add every input mapping context that is in it
const TArray<FLoadedMappableConfigPair>& Configs = LocalSettings->GetAllRegisteredInputConfigs();
for (const FLoadedMappableConfigPair& Pair : Configs)
{
if (Pair.bIsActive)
{
InputSubsystem->AddPlayerMappableConfig(Pair.Config, Options);
}
}
// Tell enhanced input about any custom keymappings that we have set
/*for (const TPair<FName, FKey>& Pair : LocalSettings->GetCustomPlayerInputConfig())
{
if (Pair.Key != NAME_None && Pair.Value.IsValid())
{
InputSubsystem->AddPlayerMappedKey(Pair.Key, Pair.Value);
}
}*/
}
}
void UDediServerTestInputComponent::RemoveInputMappings(const UTestInputConfig* InputConfig, UEnhancedInputLocalPlayerSubsystem* InputSubsystem) const
{
check(InputConfig);
check(InputSubsystem);
/*ULyraLocalPlayer* LocalPlayer = InputSubsystem->GetLocalPlayer<ULyraLocalPlayer>();
check(LocalPlayer);*/
if (UDediTestSettingLocal* LocalSettings = UDediTestSettingLocal::Get())
{
// Remove any registered input contexts
const TArray<FLoadedMappableConfigPair>& Configs = LocalSettings->GetAllRegisteredInputConfigs();
for (const FLoadedMappableConfigPair& Pair : Configs)
{
InputSubsystem->RemovePlayerMappableConfig(Pair.Config);
}
// Clear any player mapped keys from enhanced input
/*for (const TPair<FName, FKey>& Pair : LocalSettings->GetCustomPlayerInputConfig())
{
InputSubsystem->RemovePlayerMappedKey(Pair.Key);
}*/
}
}
GetCustomPlayerInputConfig()부분은 환경설정에서 커스텀 키바인딩하는 부분인거같은데 Lyra에선 GameSettings 플러그인을 만들어서 하는 방식인거 같다. 나중에 정리.
5. HeroComponent로 돌아와서 InitializePlayerInput()쪽에서 AddInputMappings()을 불러준다.
<hide/>
// DediServerTestHeroComponent.cpp
void UDediServerTestHeroComponent::InitializePlayerInput(UInputComponent* PlayerInputComponent)
{
// ...
UEnhancedInputLocalPlayerSubsystem* Subsystem = LP->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
check(Subsystem);
if(const UDediTestPawnExtensionComponent* PawnExtComp = UDediTestPawnExtensionComponent::FindPawnExtensionComponent(Pawn))
{
if(const UDediServerTestPawnData* PawnData = PawnExtComp->GetPawnData<UDediServerTestPawnData>())
{
if(const UTestInputConfig* InputConfig = PawnData->InputConfig)
{
UDediServerTestInputComponent* DediServerTestIC = CastChecked<UDediServerTestInputComponent>(PlayerInputComponent);
DediServerTestIC->AddInputMappings(InputConfig, Subsystem);
// ...
}
}
}
// ...
}
그럼 GameFeatureData는 만들었는데, 이걸 어떻게 적용시키냐는 Lyra의 Experience 시스템에 있다.
기존의 언리얼 개발 방식은 맵(레벨)마다 GameMode를 다르게 써서 GameMode가 여러개 생기는 방식인데, Lyra의 경우 공통적인 GameMode(프로젝트 세팅 > 프로젝트 > 맵 & 모드 > Default Modes > 기본 게임모드)가 있고, 월드 세팅에 Default Gameplay Experience를 따로 만들어서 이 Experience만 맵(레벨)마다 변경하는 식이다.
1. PrimaryDataAsset을 상속받은 ExperienceDefinition을 만든다.
DediTestExperienceDefinition.h
<hide/>
// DediTestExperienceDefinition.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "DediTestExperienceDefinition.generated.h"
class UGameFeatureAction;
/**
*
*/
UCLASS()
class DEDISERVERTEST_API UDediTestExperienceDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UDediTestExperienceDefinition();
public:
// List of Game Feature Plugins this experience wants to have active
UPROPERTY(EditDefaultsOnly, Category = Gameplay)
TArray<FString> GameFeaturesToEnable;
};
2. 월드세팅 만들기 : Default Gameplay Experience를 정의해야한다.
DediTestWorldSettings.h
<hide/>
// DediTestWorldSettings.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/WorldSettings.h"
#include "DediTestWorldSettings.generated.h"
class UDediTestExperienceDefinition;
/**
*
*/
UCLASS()
class DEDISERVERTEST_API ADediTestWorldSettings : public AWorldSettings
{
GENERATED_BODY()
public:
ADediTestWorldSettings(const FObjectInitializer& ObjectInitializer);
FPrimaryAssetId GetDefaultGameplayExperience() const;
protected:
// The default experience to use when a server opens this map if it is not overridden by the user-facing experience
UPROPERTY(EditDefaultsOnly, Category = GameMode)
TSoftClassPtr<UDediTestExperienceDefinition> DefaultGameplayExperience;
};
DediTestWorldSettings.cpp
<hide/>
// DediTestWorldSettings.cpp
#include "Online/DediTestWorldSettings.h"
#include "DediServerTestLogChannels.h"
#include "Engine/AssetManager.h"
#include "Online/DediTestExperienceDefinition.h"
ADediTestWorldSettings::ADediTestWorldSettings(const FObjectInitializer& ObjectInitializer)
{
}
FPrimaryAssetId ADediTestWorldSettings::GetDefaultGameplayExperience() const
{
FPrimaryAssetId Result;
if (!DefaultGameplayExperience.IsNull())
{
Result = UAssetManager::Get().GetPrimaryAssetIdForPath(DefaultGameplayExperience.ToSoftObjectPath());
if (!Result.IsValid())
{
UE_LOG(LogDediServerTest, Error, TEXT("%s.DefaultGameplayExperience is %s but that failed to resolve into an asset ID (you might need to add a path to the Asset Rules in your game feature plugin or project settings"),
*GetPathNameSafe(this), *DefaultGameplayExperience.ToString());
}
}
return Result;
}
만들고 나서 프로젝트 세팅 > 엔진 > 일반 세팅 > Default Classes > 월드 세팅 클래스로 가서 만든 클래스를 넣으면
이런식으로 월드 세팅 탭에 Default Gameplay Experience를 설정할 수 있는 탭이 나온다.
3. ExperienceManagerComponent 만들기 : GameFeature를 로드하고 활성화한다.
DediExperienceManagerComponent.h
<hide/>
// DediExperienceManagerComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/GameStateComponent.h"
#include "GameFeaturePluginOperationResult.h"
#include "DediExperienceManagerComponent.generated.h"
class UDediTestExperienceDefinition;
DECLARE_MULTICAST_DELEGATE_OneParam(FOnDediTestExperienceLoaded, const UDediTestExperienceDefinition* /*Experience*/);
enum class EDediTestExperienceLoadState
{
Unloaded,
Loading,
LoadingGameFeatures,
LoadingChaosTestingDelay,
ExecutingActions,
Loaded,
Deactivating
};
/**
*
*/
UCLASS()
class DEDISERVERTEST_API UDediExperienceManagerComponent : public UGameStateComponent
{
GENERATED_BODY()
public:
UDediExperienceManagerComponent(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
//~UActorComponent interface
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
//~End of UActorComponent interface
#if WITH_SERVER_CODE
void ServerSetCurrentExperience(FPrimaryAssetId ExperienceId);
#endif
// Ensures the delegate is called once the experience has been loaded
// If the experience has already loaded, calls the delegate immediately
void CallOrRegister_OnExperienceLoaded(FOnDediTestExperienceLoaded::FDelegate&& Delegate);
// Returns true if the experience is fully loaded
bool IsExperienceLoaded() const;
private:
UFUNCTION()
void OnRep_CurrentExperience();
void StartExperienceLoad();
void OnExperienceLoadComplete();
void OnGameFeaturePluginLoadComplete(const UE::GameFeatures::FResult& Result);
void OnExperienceFullLoadCompleted();
private:
UPROPERTY(ReplicatedUsing = OnRep_CurrentExperience)
const UDediTestExperienceDefinition* CurrentExperience;
EDediTestExperienceLoadState LoadState = EDediTestExperienceLoadState::Unloaded;
int32 NumGameFeaturePluginsLoading = 0;
TArray<FString> GameFeaturePluginURLs;
/**
* Delegate called when the experience has finished loading just before others
* (e.g., subsystems that set up for regular gameplay)
*/
//FOnDediTestExperienceLoaded OnExperienceLoaded_HighPriority;
/** Delegate called when the experience has finished loading */
FOnDediTestExperienceLoaded OnExperienceLoaded;
/** Delegate called when the experience has finished loading */
//FOnDediTestExperienceLoaded OnExperienceLoaded_LowPriority;
};
DediExperienceManagerComponent.cpp
<hide/>
// DediExperienceManagerComponent.cpp
#include "Online/DediExperienceManagerComponent.h"
#include "Net/UnrealNetwork.h"
#include "TimerManager.h"
#include "DediServerTestLogChannels.h"
#include "System/DediTestAssetManager.h"
#include "Online/DediTestExperienceDefinition.h"
#include "Settings/DediTestSettingLocal.h"
#include "GameFeatureAction.h"
#include "GameFeaturesSubsystemSettings.h"
#include "GameFeaturesSubsystem.h"
namespace DediTestConsoleVariables
{
static float ExperienceLoadRandomDelayMin = 0.0f;
static FAutoConsoleVariableRef CVarExperienceLoadRandomDelayMin(
TEXT("lyra.chaos.ExperienceDelayLoad.MinSecs"),
ExperienceLoadRandomDelayMin,
TEXT("This value (in seconds) will be added as a delay of load completion of the experience (along with the random value lyra.chaos.ExperienceDelayLoad.RandomSecs)"),
ECVF_Default);
static float ExperienceLoadRandomDelayRange = 0.0f;
static FAutoConsoleVariableRef CVarExperienceLoadRandomDelayRange(
TEXT("lyra.chaos.ExperienceDelayLoad.RandomSecs"),
ExperienceLoadRandomDelayRange,
TEXT("A random amount of time between 0 and this value (in seconds) will be added as a delay of load completion of the experience (along with the fixed value lyra.chaos.ExperienceDelayLoad.MinSecs)"),
ECVF_Default);
float GetExperienceLoadDelayDuration()
{
return FMath::Max(0.0f, ExperienceLoadRandomDelayMin + FMath::FRand() * ExperienceLoadRandomDelayRange);
}
}
void UDediExperienceManagerComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, CurrentExperience);
}
UDediExperienceManagerComponent::UDediExperienceManagerComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
SetIsReplicatedByDefault(true);
}
void UDediExperienceManagerComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
// deactivate any features this experience loaded
//@TODO: This should be handled FILO as well
for (const FString& PluginURL : GameFeaturePluginURLs)
{
UGameFeaturesSubsystem::Get().DeactivateGameFeaturePlugin(PluginURL);
}
}
#if WITH_SERVER_CODE
void UDediExperienceManagerComponent::ServerSetCurrentExperience(FPrimaryAssetId ExperienceId)
{
UDediTestAssetManager& AssetManager = UDediTestAssetManager::Get();
FSoftObjectPath AssetPath = AssetManager.GetPrimaryAssetPath(ExperienceId);
TSubclassOf<UDediTestExperienceDefinition> AssetClass = Cast<UClass>(AssetPath.TryLoad());
check(AssetClass);
const UDediTestExperienceDefinition* Experience = GetDefault<UDediTestExperienceDefinition>(AssetClass);
check(Experience != nullptr);
check(CurrentExperience == nullptr);
CurrentExperience = Experience;
StartExperienceLoad();
}
#endif
void UDediExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOnDediTestExperienceLoaded::FDelegate&& Delegate)
{
if (IsExperienceLoaded())
{
Delegate.Execute(CurrentExperience);
}
else
{
OnExperienceLoaded.Add(MoveTemp(Delegate));
}
}
bool UDediExperienceManagerComponent::IsExperienceLoaded() const
{
return (LoadState == EDediTestExperienceLoadState::Loaded) && (CurrentExperience != nullptr);
}
void UDediExperienceManagerComponent::OnRep_CurrentExperience()
{
StartExperienceLoad();
}
void UDediExperienceManagerComponent::StartExperienceLoad()
{
check(CurrentExperience != nullptr);
check(LoadState == EDediTestExperienceLoadState::Unloaded);
UE_LOG(LogDediServerTestExperience, Log, TEXT("EXPERIENCE: StartExperienceLoad(CurrentExperience = %s, %s)"),
*CurrentExperience->GetPrimaryAssetId().ToString(),
*GetClientServerContextString(this));
LoadState = EDediTestExperienceLoadState::Loading;
UDediTestAssetManager& AssetManager = UDediTestAssetManager::Get();
TSet<FPrimaryAssetId> BundleAssetList;
TSet<FSoftObjectPath> RawAssetList;
BundleAssetList.Add(CurrentExperience->GetPrimaryAssetId());
/*for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
{
if (ActionSet != nullptr)
{
BundleAssetList.Add(ActionSet->GetPrimaryAssetId());
}
}*/
TArray<FName> BundlesToLoad;
BundlesToLoad.Add(FDediTestBundles::Equipped);
//@TODO: Centralize this client/server stuff into the LyraAssetManager
const ENetMode OwnerNetMode = GetOwner()->GetNetMode();
const bool bLoadClient = GIsEditor || (OwnerNetMode != NM_DedicatedServer);
const bool bLoadServer = GIsEditor || (OwnerNetMode != NM_Client);
if (bLoadClient)
{
BundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateClient);
}
if (bLoadServer)
{
BundlesToLoad.Add(UGameFeaturesSubsystemSettings::LoadStateServer);
}
const TSharedPtr<FStreamableHandle> BundleLoadHandle = AssetManager.ChangeBundleStateForPrimaryAssets(BundleAssetList.Array(), BundlesToLoad, {}, false, FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority);
const TSharedPtr<FStreamableHandle> RawLoadHandle = AssetManager.LoadAssetList(RawAssetList.Array(), FStreamableDelegate(), FStreamableManager::AsyncLoadHighPriority, TEXT("StartExperienceLoad()"));
// If both async loads are running, combine them
TSharedPtr<FStreamableHandle> Handle = nullptr;
if (BundleLoadHandle.IsValid() && RawLoadHandle.IsValid())
{
Handle = AssetManager.GetStreamableManager().CreateCombinedHandle({ BundleLoadHandle, RawLoadHandle });
}
else
{
Handle = BundleLoadHandle.IsValid() ? BundleLoadHandle : RawLoadHandle;
}
FStreamableDelegate OnAssetsLoadedDelegate = FStreamableDelegate::CreateUObject(this, &ThisClass::OnExperienceLoadComplete);
if (!Handle.IsValid() || Handle->HasLoadCompleted())
{
// Assets were already loaded, call the delegate now
FStreamableHandle::ExecuteDelegate(OnAssetsLoadedDelegate);
}
else
{
Handle->BindCompleteDelegate(OnAssetsLoadedDelegate);
Handle->BindCancelDelegate(FStreamableDelegate::CreateLambda([OnAssetsLoadedDelegate]()
{
OnAssetsLoadedDelegate.ExecuteIfBound();
}));
}
// This set of assets gets preloaded, but we don't block the start of the experience based on it
TSet<FPrimaryAssetId> PreloadAssetList;
//@TODO: Determine assets to preload (but not blocking-ly)
if (PreloadAssetList.Num() > 0)
{
AssetManager.ChangeBundleStateForPrimaryAssets(PreloadAssetList.Array(), BundlesToLoad, {});
}
}
void UDediExperienceManagerComponent::OnExperienceLoadComplete()
{
check(LoadState == EDediTestExperienceLoadState::Loading);
check(CurrentExperience != nullptr);
UE_LOG(LogDediServerTestExperience, Log, TEXT("EXPERIENCE: OnExperienceLoadComplete(CurrentExperience = %s, %s)"),
*CurrentExperience->GetPrimaryAssetId().ToString(),
*GetClientServerContextString(this));
// find the URLs for our GameFeaturePlugins - filtering out dupes and ones that don't have a valid mapping
GameFeaturePluginURLs.Reset();
auto CollectGameFeaturePluginURLs = [This = this](const UPrimaryDataAsset* Context, const TArray<FString>& FeaturePluginList)
{
for (const FString& PluginName : FeaturePluginList)
{
FString PluginURL;
if (UGameFeaturesSubsystem::Get().GetPluginURLForBuiltInPluginByName(PluginName, /*out*/ PluginURL))
{
This->GameFeaturePluginURLs.AddUnique(PluginURL);
}
else
{
ensureMsgf(false, TEXT("OnExperienceLoadComplete failed to find plugin URL from PluginName %s for experience %s - fix data, ignoring for this run"), *PluginName, *Context->GetPrimaryAssetId().ToString());
}
}
// // Add in our extra plugin
// if (!CurrentPlaylistData->GameFeaturePluginToActivateUntilDownloadedContentIsPresent.IsEmpty())
// {
// FString PluginURL;
// if (UGameFeaturesSubsystem::Get().GetPluginURLForBuiltInPluginByName(CurrentPlaylistData->GameFeaturePluginToActivateUntilDownloadedContentIsPresent, PluginURL))
// {
// GameFeaturePluginURLs.AddUnique(PluginURL);
// }
// }
};
CollectGameFeaturePluginURLs(CurrentExperience, CurrentExperience->GameFeaturesToEnable);
/*for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
{
if (ActionSet != nullptr)
{
CollectGameFeaturePluginURLs(ActionSet, ActionSet->GameFeaturesToEnable);
}
}*/
// Load and activate the features
NumGameFeaturePluginsLoading = GameFeaturePluginURLs.Num();
if (NumGameFeaturePluginsLoading > 0)
{
LoadState = EDediTestExperienceLoadState::LoadingGameFeatures;
for (const FString& PluginURL : GameFeaturePluginURLs)
{
//ULyraExperienceManager::NotifyOfPluginActivation(PluginURL);
UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin(PluginURL, FGameFeaturePluginLoadComplete::CreateUObject(this, &ThisClass::OnGameFeaturePluginLoadComplete));
}
}
else
{
OnExperienceFullLoadCompleted();
}
}
void UDediExperienceManagerComponent::OnGameFeaturePluginLoadComplete(const UE::GameFeatures::FResult& Result)
{
// decrement the number of plugins that are loading
NumGameFeaturePluginsLoading--;
if (NumGameFeaturePluginsLoading == 0)
{
OnExperienceFullLoadCompleted();
}
}
void UDediExperienceManagerComponent::OnExperienceFullLoadCompleted()
{
check(LoadState != EDediTestExperienceLoadState::Loaded);
// Insert a random delay for testing (if configured)
if (LoadState != EDediTestExperienceLoadState::LoadingChaosTestingDelay)
{
const float DelaySecs = DediTestConsoleVariables::GetExperienceLoadDelayDuration();
if (DelaySecs > 0.0f)
{
FTimerHandle DummyHandle;
LoadState = EDediTestExperienceLoadState::LoadingChaosTestingDelay;
GetWorld()->GetTimerManager().SetTimer(DummyHandle, this, &ThisClass::OnExperienceFullLoadCompleted, DelaySecs, /*bLooping=*/ false);
return;
}
}
LoadState = EDediTestExperienceLoadState::ExecutingActions;
// Execute the actions
FGameFeatureActivatingContext Context;
// Only apply to our specific world context if set
const FWorldContext* ExistingWorldContext = GEngine->GetWorldContextFromWorld(GetWorld());
if (ExistingWorldContext)
{
Context.SetRequiredWorldContextHandle(ExistingWorldContext->ContextHandle);
}
auto ActivateListOfActions = [&Context](const TArray<UGameFeatureAction*>& ActionList)
{
for (UGameFeatureAction* Action : ActionList)
{
if (Action != nullptr)
{
//@TODO: The fact that these don't take a world are potentially problematic in client-server PIE
// The current behavior matches systems like gameplay tags where loading and registering apply to the entire process,
// but actually applying the results to actors is restricted to a specific world
Action->OnGameFeatureRegistering();
Action->OnGameFeatureLoading();
Action->OnGameFeatureActivating(Context);
}
}
};
/*ActivateListOfActions(CurrentExperience->Actions);
for (const TObjectPtr<ULyraExperienceActionSet>& ActionSet : CurrentExperience->ActionSets)
{
if (ActionSet != nullptr)
{
ActivateListOfActions(ActionSet->Actions);
}
}*/
LoadState = EDediTestExperienceLoadState::Loaded;
/*OnExperienceLoaded_HighPriority.Broadcast(CurrentExperience);
OnExperienceLoaded_HighPriority.Clear();*/
// ... PlayerState에서 AbilitySystem Init시에 사용 ...
OnExperienceLoaded.Broadcast(CurrentExperience);
OnExperienceLoaded.Clear();
/*OnExperienceLoaded_LowPriority.Broadcast(CurrentExperience);
OnExperienceLoaded_LowPriority.Clear();*/
// Apply any necessary scalability settings
#if !UE_SERVER
//UDediTestSettingLocal::Get()->OnExperienceLoaded();
#endif
}
뭐가 많은데, 중요한건 OnExperienceLoadComplete()에서 UGameFeaturesSubsystem::Get().LoadAndActivateGameFeaturePlugin()을 통해 GameFeauture를 활성화시키는 것이다.
4. Experience Definition을 상속받은 BP클래스를 만든다. (B_DediTestExperience)
안에는 만든 GameFeatureData이름을 넣는다. (Lyra에선 ShooterCore,TopDownArena에 해당)
월드 세팅 > Default Gameplay Experience에 만든 BP를 넣는다.
※ 주의
1. 프로젝트 세팅 > 엔진 > 일반 세팅 > Default Classes를 잘 설정했는지 확인
2. 프로젝트 세팅 > 게임 > Game Features > Game Feature Project Policy Class를 설정했는지 확인
(Lyra에서는 직접 상속받아 만든 클래스를 넣었지만 기본으로 넣어도 작동됨)
'언리얼 > Enhanced Input System' 카테고리의 다른 글
UE5 Enhanced Input으로 카메라 터치 조작하기(Pan Camera) (0) | 2023.03.21 |
---|---|
Lyra에서 Input Mapping Context 살펴보기 (3) (0) | 2022.10.27 |
Enhanced Input System을 위한 Game Feature 추가하기 (0) | 2022.10.14 |
Lyra에서 Input Mapping Context 살펴보기 (1) (0) | 2022.10.11 |
C++에서 Enhanced Input System 사용하기 (1) | 2022.09.13 |