UE5/Lyra Clone Coding

[UE5] Experience

검정색필통 2025. 1. 23. 21:53

Experience란?

  • Experience는 Lyra에서 게임플레이 경험을 정의하는 핵심 시스템이다.
  • Experience는 게임의 특정 모드(예: 팀 데스매치, 배틀로얄 등)를 정의하는 설정 집합
  • Gameplay Experience Definition (DataAsset) 형태로 저장되며, UExperienceDefinition 클래스를 기반으로 함
  • 게임이 시작될 때, 현재 적용할 Experience를 결정하고, 해당 Experience가 로드되면서 관련된 모든 설정(Abilities, Input Mapping, UI 등)이 적용

 

Experience의 장점

  1. 모듈화된 시스템
    • 게임 규칙(GameMode)뿐만 아니라 **UI, Input, 캐릭터 능력(Ability)**까지 통합하여 관리 가능.
  2. 런타임에서 변경 가능
    • 게임 도중 특정 조건에서 Experience를 변경할 수 있음.
      (예: 튜토리얼 모드 → 일반 게임 모드 전환)
  3. 자동 로딩 기능 제공
    • Experience에 설정된 Gameplay Feature 플러그인이 자동으로 활성화됨.
    • 특정 Experience에서만 특정 무기/아이템을 사용하도록 설정 가능.
  4. 멀티플레이 친화적
    • UGameFeatureData와 연동되어, 서버가 특정 Experience를 로드하면 모든 클라이언트도 동일한 설정을 받음.
  5. 비개발자도 설정 가능
    • GameMode는 C++/Blueprint로 개발해야 하지만,
      Experience는 DataAsset 형태라서 디자이너도 쉽게 설정 변경 가능.

 

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는 들어오기 전이다. 어떻게 해야할까?

 

HandleStartingNewPlayer_Implementation 오버라이딩

=> 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정보를 변경해준다.