[UE5] Experience
Experience란?
- Experience는 Lyra에서 게임플레이 경험을 정의하는 핵심 시스템이다.
- Experience는 게임의 특정 모드(예: 팀 데스매치, 배틀로얄 등)를 정의하는 설정 집합
- Gameplay Experience Definition (DataAsset) 형태로 저장되며, UExperienceDefinition 클래스를 기반으로 함
- 게임이 시작될 때, 현재 적용할 Experience를 결정하고, 해당 Experience가 로드되면서 관련된 모든 설정(Abilities, Input Mapping, UI 등)이 적용
Experience의 장점
- 모듈화된 시스템
- 게임 규칙(GameMode)뿐만 아니라 **UI, Input, 캐릭터 능력(Ability)**까지 통합하여 관리 가능.
- 런타임에서 변경 가능
- 게임 도중 특정 조건에서 Experience를 변경할 수 있음.
(예: 튜토리얼 모드 → 일반 게임 모드 전환)
- 게임 도중 특정 조건에서 Experience를 변경할 수 있음.
- 자동 로딩 기능 제공
- Experience에 설정된 Gameplay Feature 플러그인이 자동으로 활성화됨.
- 특정 Experience에서만 특정 무기/아이템을 사용하도록 설정 가능.
- 멀티플레이 친화적
- UGameFeatureData와 연동되어, 서버가 특정 Experience를 로드하면 모든 클라이언트도 동일한 설정을 받음.
- 비개발자도 설정 가능
- GameMode는 C++/Blueprint로 개발해야 하지만,
Experience는 DataAsset 형태라서 디자이너도 쉽게 설정 변경 가능.
- GameMode는 C++/Blueprint로 개발해야 하지만,
InitGame
void AZSGameModeBase::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
Super::InitGame(MapName, Options, ErrorMessage);
GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::HandleMatchAssignmentIfNotExpectingOne);
}
InitGame에서 실행하는 HandleMatchAssignmentIfNotExpectingOne이라는 함수는 설정한 Experience를 불러오는 로직인데, 도식에서 보듯이 InitGame이 실행되었을 때는 Experience에 대한 어떤 정보도 담기기 전이다. 따라서 NextTick 즉, 다음 프레임에 함수를 실행하여 초기 세팅을 할 시간을 벌어준다.
void AZSGameModeBase::HandleMatchAssignmentIfNotExpectingOne()
{
FPrimaryAssetId ExperienceId;
UWorld* World = GetWorld();
if (!ExperienceId.IsValid())
{
ExperienceId = FPrimaryAssetId(FPrimaryAssetType("ZSExperienceDefinition"), FName("B_ZSDefaultExperience"));
}
OnMatchAssignmentGiven(ExperienceId);
}
ExperienceMangerComponent
ExperienceManagerComponent는 특정 Gameplay Experience를 로드하고, 적용하며, 실행이 끝날 때 정리하는 역할을 하는 Experience를 다루는 데 있어 핵심 역할을 수행한다.
InitGame을 실행했던 GameMode에서는 ExperienceManagerComponent에 Experience를 실행해 달라고 요청하는 역할을 하고 실질적인 동작은 매니저에서 진행한다.
AZSGameState::AZSGameState()
{
ExperienceManagerComponent = CreateDefaultSubobject<UZSExperienceManagerComponent>(TEXT("ExperienceManagerComponent"));
}
ExperienceManagerComponent는 UGameStateComponent를 상속받는데, 타고 올라가면 ActorComponent를 상속받는다는 것을 알 수 있다. 즉, GameState에 다양한 Component를 붙여서 활용 가능한, 확장성이 뛰어난 Lyra의 구조를 엿볼 수 있다.
ExperienceManagerComponent에서 핵심 로직은 바로 CallOrRegister_OnExperienceLoaded 함수이다.
void UZSExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOnZSExperienceLoaded::FDelegate&& Delegate)
{
if (IsExperienceLoaded())
{
Delegate.Execute(CurrentExperience);
}else
{
OnExperienceLoaded.Add(MoveTemp(Delegate));
}
}
Experience가 로드되어있으면, 함수에 인자로 들어온 Delegate를 바로 실행하고 CurrentExperience를 넘겨준다.
로드되어 있지 않다면 Multicast Delegate에 추가를 해주어 이후에 Broadcast되었을 때 함수를 실행한다.
CallOrRegister_OnExperienceLoaded함수는 GameState가 생성이 된 직후 호출되는 InitGameState에서 호출된다.
void AZSGameModeBase::InitGameState()
{
Super::InitGameState();
UZSExperienceManagerComponent* ExperienceManagerComponent = GameState->FindComponentByClass<UZSExperienceManagerComponent>();
check(ExperienceManagerComponent);
ExperienceManagerComponent->CallOrRegister_OnExperienceLoaded(FOnZSExperienceLoaded::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded));
}
이제 Experience의 초기화 과정을 진행하고, 플레이어의 캐릭터를 소환하고자 한다.
그런데 플레이어가 갖고있는 캐릭터에 대한 정보나 조작 방법, 규칙 등을 Experience가 들고 있는데 아직 Experience는 들어오기 전이다. 어떻게 해야할까?
=> Experience가 로딩되기 전까지 플레이어 관련 로직을 오버라이드해서 막아 놓을 것이다!! Experience 로딩이 완료된 후에 재호출해서 Experience를 이용해 플레이어 설정을 진행하게 된다.
void AZSGameModeBase::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
if (IsExperienceLoaded())
{
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
}
}
HandleStartingNewPlayer의 실행을 막아버려서 이후의 과정이 진행되지 못하게 한다.
이제 다음 프레임에서 Experience를 가져오는 과정을 살펴보자.
위에서 살펴봤던 HandleMatchAssignmentIfNotExpectingOne 함수를 살펴보면
void AZSGameModeBase::HandleMatchAssignmentIfNotExpectingOne()
{
..
if (!ExperienceId.IsValid())
{
ExperienceId = FPrimaryAssetId(FPrimaryAssetType("ZSExperienceDefinition"), FName("B_ZSDefaultExperience"));
}
...
}
AssetManager에서 Type과 Name으로 Scan을 해서 PrimaryAssetId를 갖고 오는 것을 확인할 수 있다.
void AZSGameModeBase::OnMatchAssignmentGiven(FPrimaryAssetId ExperienceId)
{
check(ExperienceId.IsValid());
UZSExperienceManagerComponent* ExperienceManagerComponent = GameState->FindComponentByClass<UZSExperienceManagerComponent>();
check(ExperienceManagerComponent);
ExperienceManagerComponent->ServerSetCurrentExperience(ExperienceId);
}
ExperienceManager에게 ExperienceId를 넘겨주어 Experience를 호출할 수 있도록 한다.
void UZSExperienceManagerComponent::ServerSetCurrentExperience(FPrimaryAssetId ExperienceId)
{
UZSAssetManager& AssetManager = UZSAssetManager::Get();
TSubclassOf<UZSExperienceDefinition> AssetClass;
FSoftObjectPath AssetPath = AssetManager.GetPrimaryAssetPath(ExperienceId);
AssetClass = Cast<UClass>(AssetPath.TryLoad());
const UZSExperienceDefinition* Experience = GetDefault<UZSExperienceDefinition>(AssetClass);
check(Experience != nullptr);
check(CurrentExperience == nullptr);
CurrentExperience = Experience;
StartExperienceLoad();
}
StartExperienceLoad에서 비동기로 Experience가 갖고 있는 추가적인 Asset정보를 불러오고, Experience를 호출하기 위한 델리게이트를 실행한다.
그 후 Experience가 로드되면 RestartPlayer를 통해 위에서 언급했던 것처럼 Experinece의 정보를 담은 Player Spawn을 시작한다.
void AZSGameModeBase::OnExperienceLoaded(const UZSExperienceDefinition* CurrentExperience)
{
for (FConstPlayerControllerIterator Iterator = GetWorld()->GetPlayerControllerIterator(); Iterator; ++Iterator)
{
APlayerController* PC = Cast<APlayerController>(*Iterator);
if (PC && PC->GetPawn() == nullptr)
{
if (PlayerCanRestart(PC))
{
RestartPlayer(PC);
}
}
}
}
추가적으로 중요한 과정이, CDO에서 DefaultPawnClass를 가져올 때, 우리가 설정한 PawnClass로 변경하기 위해서
UClass* AZSGameModeBase::GetDefaultPawnClassForController_Implementation(AController* InController)
{
if (const UZSPawnData* PawnData = GetPawnDataForController(InController))
{
if (PawnData->PawnClass)
{
return PawnData->PawnClass;
}
}
return Super::GetDefaultPawnClassForController_Implementation(InController);
}
와 같이 Class정보를 변경해준다.