ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [UE5] Experience
    UE5/Lyra Clone Coding 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정보를 변경해준다.

    'UE5 > Lyra Clone Coding' 카테고리의 다른 글

    [UE5] Camera  (0) 2025.02.05
    [UE5] PawnExtension(2) - 구성 요소 분석  (0) 2025.02.03
    [UE5] PawnExtension  (0) 2025.01.31
    [UE5] AssetManager Scan  (0) 2025.01.21
    [UE5] AssetManager  (0) 2025.01.21
Designed by Tistory.