Game Programming/Unreal Engine

Lyra와 모듈형 게임플레이

타자치는 문돌이

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/game-features-and-modular-gameplay-in-unreal-engine

Game Feature와 Experience

Lyra는 여러가지 모드가 존재하고, 이를 Experience라는 단위로 나누어 구분한다.

이때, 필요없는 종속성과 인터랙션을 방지하기 위해 Game Feature로 여러 기능을 Plugin화 한 뒤, Experience를 로드할 때 필요한 Game Feature만 동적으로 추가하는 방식으로 작동한다.

Experience는 ULyraExperienceDefinition라는 데이터 에셋으로 저장되어 이 Experience를 로드할 때 수행해야할 Action, 사용할 DefaultPawn 등으로 구성되어 있다.

여기서 수행하는 액션으로는 Ability 부착, InputBinding 추가, Widget 추가, 컴포넌트 추가 등이 있다.

즉, 게임 플레이 맵으로 이동할 때, 기존의 Gameplay Actor에 여러 컴포넌트를 붙여 무기 기능, 인벤토리 기능 등을 활성화 하는 것이다. 이를 위해 로딩 개념을 추가했다.

void ALyraGameMode::OnMatchAssignmentGiven(FPrimaryAssetId ExperienceId, const FString& ExperienceIdSource)  
{  
    if (ExperienceId.IsValid())  
    {  
       UE_LOG(LogLyraExperience, Log, TEXT("Identified experience %s (Source: %s)"), *ExperienceId.ToString(), *ExperienceIdSource);  

       ULyraExperienceManagerComponent* ExperienceComponent = GameState->FindComponentByClass<ULyraExperienceManagerComponent>();  
       check(ExperienceComponent);  
       ExperienceComponent->SetCurrentExperience(ExperienceId);  
    }  
    else  
    {  
       UE_LOG(LogLyraExperience, Error, TEXT("Failed to identify experience, loading screen will stay up forever"));  
    }  
}

GameMode에서 매칭을 시작하기 전에 ExperienceId를 받아 GameState에 부착된 ULyraExperienceManagerComponent를 통해 Experience를 Set한다. 이 과정에서 StartExperienceLoad가 호출되어 필요한 에셋을 비동기로 로드한다. 그 후, OnExperienceLoadComplete에서 Game Feature를 로드한 뒤, OnExperienceFullLoadCompleted에서 GameFeature Action을 실행한다.

Experience의 로드가 완료되면 OnExperienceLoaded를 구독한 요소들에게 알림을 보낸다. 주요 구독자로는 ALyraGameModeALyraPlayerState가 있다.

Modular Gameplay Actor와 Component

많은 로직이 동적으로 Component로 붙기 때문에 Component 초기화 로직이 중요해진다. 이를 ModularGameplayActor를 통해 해결한다.

Lyra의 Gameplay Actor(PlayerController, PlayerState...)는 ModularGameplayActor와 CommonGame을 상속 받는다. ModularGameplayActor는 GameFeature를, CommonGame은 온라인 서브시스템을 위한 플러그인의 일부이다.

void AModularPlayerState::PreInitializeComponents()  
{  
    Super::PreInitializeComponents();  

    UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);  
}  

void AModularPlayerState::BeginPlay()  
{  
    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);  

    Super::BeginPlay();  
}  

void AModularPlayerState::EndPlay(const EEndPlayReason::Type EndPlayReason)  
{  
    UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);  

    Super::EndPlay(EndPlayReason);  
}  

void AModularPlayerState::Reset()  
{  
    Super::Reset();  

    TArray<UPlayerStateComponent*> ModularComponents;  
    GetComponents(ModularComponents);  
    for (UPlayerStateComponent* Component : ModularComponents)  
    {  
       Component->Reset();  
    }  
}  

void AModularPlayerState::CopyProperties(APlayerState* PlayerState)  
{  
    Super::CopyProperties(PlayerState);  

    TInlineComponentArray<UPlayerStateComponent*> PlayerStateComponents;  
    GetComponents(PlayerStateComponents);  
    for (UPlayerStateComponent* SourcePSComp : PlayerStateComponents)  
    {  
       if (UPlayerStateComponent* TargetComp = Cast<UPlayerStateComponent>(static_cast<UObject*>(FindObjectWithOuter(PlayerState, SourcePSComp->GetClass(), SourcePSComp->GetFName()))))  
       {  
          SourcePSComp->CopyProperties(TargetComp);  
       }  
    }  
}
void AModularCharacter::PreInitializeComponents()  
{  
    Super::PreInitializeComponents();  

    UGameFrameworkComponentManager::AddGameFrameworkComponentReceiver(this);  
}  

void AModularCharacter::BeginPlay()  
{  
    UGameFrameworkComponentManager::SendGameFrameworkComponentExtensionEvent(this, UGameFrameworkComponentManager::NAME_GameActorReady);  

    Super::BeginPlay();  
}  

void AModularCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)  
{  
    UGameFrameworkComponentManager::RemoveGameFrameworkComponentReceiver(this);  

    Super::EndPlay(EndPlayReason);  
}

Modular GameplayActor들의 역할은 단순히 UGameFrameworkComponentManager에 자신을 등록하고, 적절한 타이밍에 GameActorReady를 보내는 것이다. GameActorReady를 보내면 UGameFrameworkComponentManager는 알림을 구독한 Component에 State가 변경되었다는 알림을 보낸다. 구독자는 IGameFrameworkInitStateInterface라는 Interface를 가지고 있고, 알림이 오면 자신의 CanChangeInitState를 확인해 자신의 상태를 변환한다. 그 후, 변환된 상태에 맞게 HandleChangeInitState를 실행해 특정 행동을 한다.

반응형