5 분 소요

Experience에 대해서는…

Experience 관련 블로깅

Image

이전에 배운 내용을 요약하는 블로깅 주소와
이미지 파일

이번에는 이러한 Experience를 불러오는(Loading)
방식에 대하여 알아보려 한다

핵심적인 요소는

  • GameMode(AGameModeBase)
  • ExperienceManagerComponent(UGameStateComponent)

GameMode와 Experience 사전 개념 요약

Image

GameMode와 Experience의 관계도

[GameMode] 
 ├─ GameStateClass → [GameState]
 │   └─ ExperienceManagerComponent → [ExperienceDefinition]
 │
 ├─ PlayerStateClass → [PlayerState]
 │   └─ PawnData (PawnClass, InputConfig, CameraMode)
 │
 ├─ PlayerControllerClass → [PlayerController]
 │   └─ PlayerCameraManager
 │
 └─ DefaultPawnClass → [Character]
     ├─ PawnExtensionComponent
     └─ CameraComponent
  • GameMode
    서버 전용의 권위(Authority) 보유 클래스
    사용할 GameState,PlayerState, PlayerController, Pawn 을 결정
    Experience 로딩 트리거를 ‘시작’
    (InitGameState를 통해 ExperienceManager에게
    로딩 트리거 전달)
  • GameState
    클라/서버 공통 존재(동기화 요구)
    ExperienceManager를 소유
    (일반적으로 GameMode와 1:1 대응)
    (Lyra는 하나의 게임 모드 안에 여러 게임이 존재)
    (이 경우, ExperienceManager가 Experience 전환을 대응)
  • ExperienceManager
    현재 적용 게임에 적용할 ExperienceDefinition을 보관
    Experience 로딩을 담당
    (PrimaryAsset 로드, GameFeature 활성화, ActionSet 적용 등)
    완료시 OnExperienceLoaded 델리게이트를 통해 broadcast
  • ExperienceDefinition
    데이터 에셋
    사용할 Pawn, CameraMode, AbilitySet, GameFeature 등을 정의
  • PlayerState
    플레이어 상태 정보 저장(네트워크 동기화 필요)
    PawnData를 참조하고 캐싱해놓는다
    (차후 gas 관련 용도)
  • PawnData
    플레이어 Pawn에 대한 데이터 설정
    (PawnClass, Input, CameraMode 등을 포함)
  • PlayerController
    입력 처리, 카메라 제어, UI 상호작용 담당
    PlayerCameraManager를 소유해 카메라 로직 제어
  • PlayerCameraManager
    카메라 모드를 관리
    (시야/시점 조정)
  • Character(DefaultPawn)
    실제 플레이어 캐릭터의 Pawn
    PawnExtensionComponent를 통해 확장
  • PawnExtensionComponent
    Pawn의 기능확장을 위한 컴포넌트
    (다만, Pawn과 컴포넌트가 종속적인 관계가 되지 않도록
    관리하는 중간 다리 역할을 한다)
  • CameraComponent
    CameraManager가 다루게 될 실제 카메라 속성/기능

(여담으로 Modular Gameplay와
Game Features, Gameplay Abilities 플러그인을 미리 켜두는 것을 추천)

이후
Bulid.cs에 추가해준다

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class Samples : ModuleRules
{
	public Samples(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] {
            "Core",
            "CoreUObject",
            "Engine",
            // GAS
            "GameplayTags",
            // Game Features
            "ModularGameplay",
			//Input
            "InputCore",
        });

		PrivateDependencyModuleNames.AddRange(new string[] {  });
	}
}

GameMode 세팅

프로젝트 세팅에서
만든 GameMode를 설정하고
해당 생성자에서 DefaultClass 들을 설정하면
기본적으로 해당 클래스들이 선택된다

ASampleGameMode::ASampleGameMode()
{
	// GameState를 설정해주었기에 World가 생성되는 시점에 생성자로 ExperienceManager를 생성
	GameStateClass = ASampleGameState::StaticClass();
	PlayerControllerClass = ASamplePlayerController::StaticClass();
	PlayerStateClass = ASamplePlayerState::StaticClass();
	DefaultPawnClass = ASampleCharacter::StaticClass();
	HUDClass = ASampleHUD::StaticClass();
}

InitGame에서
한 프레임 뒤에 HandleMatchAssignmentIfNotExpectingOne()를
호출하도록 설정
(InitGameState가 호출된 후, 호출됨)

  • 사실 InitGame()이 호출되는 시점은
    World가 생성되는 시점이기에
    GameState가 생성되지 않았음…

Image

  • 이런식으로 프레임이나, 일부 엔진 로딩 방식을 따르지 않고
    별도로 로딩 시스템을 만드는 이유?
    • 엔진 업데이트를 통한 내부 수정에 영향을 받지 않기 위함
void ASampleGameMode::InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage)
{
	Super::InitGame(MapName, Options, ErrorMessage);

	// 호출 시점엔 아직 GameInstance를 통해 초기화 작업이 진행중이므로
	// 해당 프레임에선 Lyra의 Comcept인 Experience 처리를 진행할 수 없음
	// - 이를 위해 한 프레임 뒤에 이벤트를 받아 처리를 이어서 진행
	// InitGameState가 호출된 후, HandleMatchAssignmentIfNotExpectingOne 가 호출된다
	// InitGame(이 떄 한 프레임 뒤에 HandleMatchAssignmentIfNotExpectingOne 호출) 
	// -> GameState 생성자 호출(ExperienceManager 생성) -> InitGameState 호출로 인하여
	// OnExperienceLoaded 등록(이 때, Pawn들을 Restart 시킴)
	// 
	// HandleMatchAssignmentIfNotExpectingOne 를 통해 한 프레임 뒤,
	// Manager에게 Experience를 Load하도록 시키며,
	// 완료 될시 OnExperienceLoaded 가 호출
	//
	GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::HandleMatchAssignmentIfNotExpectingOne);
}

HandleMatchAssignmentIfNotExpectingOne?

  • 실제로는 dedicate 서버 기반 프로젝트이기에 이런 함수명

ExperienceManagerComponent 생성

GameStateComponent를 상속받기에
“ModularGameplay” 플러그인을 설정하고 build.cs에 추가 필요

역할

  • GameState를 owner로 가지며, Experience의 상태 정보를 가지는 Component
  • 추가로 Manager의 역할로서 Experience 로딩 상태 업데이트 및 이벤트를 관리

UGameStateComponent?

  • 결국 UActorComponent를 상속받는 클래스
    -> 단순히 Actor에만 붙이는 ‘컴포넌트’를 GameState 등에 붙일 수 있도록 하는 클래스이다
    (GameState에 붙이는 추가적인 기능 같은 것)
    (-> Unity에서 다양한 GameObject에 컴포넌트를 붙일 수 있듯)
    GameFeature의 시스템 기능으로 unity처럼 부품을 붙였다 뗄수 있음
    (확장성이 좋아짐, 다만 디버깅이 힘들어지고, 부품이 많아질 순 있음)

GameState에 가서 먼저 컴포넌트 생성해주기

.h

UCLASS()
class SAMPLES_API ASampleGameState : public AGameStateBase
{
	GENERATED_BODY()
public:
	ASampleGameState();

public:
	UPROPERTY()
	TObjectPtr<USampleExperienceManagerComponent> ExperienceManagerComponent;

};

---
.cpp

#include "SampleGameState.h"
#include "SampleExperienceManagerComponent.h"

ASampleGameState::ASampleGameState()
{
	ExperienceManagerComponent = CreateDefaultSubobject<USampleExperienceManagerComponent>(TEXT("ExperienceManagerComponent"));
}

다시 돌아와서…

멤버 변수로 뭘 넣을 것인지?

public:
	// 가리키는 객체를 수정할 수 없도록 const 를 걸어준다
	// 그래도 ptr은 새로운 Experience를 가르킬 수 있음
	// c++ 의 type* const 방식
	//
	// 로딩 요청을 받아야 로딩을 한다
	UPROPERTY()
	TObjectPtr<const USampleExperienceDefinition> CurrentExperience;
  • CurrentExperience : 로딩을 할 대상

로딩 완료 체크 시점?

Image

InitGameState!

  • 이 시점에서 GameMode가 매니저에게 ‘완료 다 되면 알려달라고’
    구독하고 간다

그렇기에 2가지가 필요한데

  • ‘로딩 상태’에 대한 정의
    (Enum class)
enum class ESampleExperienceLoadState
{
	Unloaded,
	Loading,
	LoadingGameFeatures,
	ExecutingActions,
	Loaded,
	Deactivating,
};
  • ‘로딩 완료’에 따른 알림
    (DELEGATE가 필요한 시점이다)
// Multicast : 하나의 이벤트에 여러 함수 연결 가능
DECLARE_MULTICAST_DELEGATE_OneParam(FOnSampleExperienceLoaded, const USampleExperienceDefinition*);

이후 로딩을 완료한 ExperienceDefinition를 통해
해당 게임 모드에서 사용할 데이터를 넘겨준다

해당하는 두 요소와 넘겨줄 Experience를
매니저의 멤버 변수로 추가

// 가리키는 객체를 수정할 수 없도록 const 를 걸어준다
// 그래도 ptr은 새로운 Experience를 가르킬 수 있음
// c++ 의 type* const 방식
//
// 로딩 요청을 받아야 로딩을 한다
UPROPERTY()
TObjectPtr<const USampleExperienceDefinition> CurrentExperience;

// Experience의 로딩 상태를 모니터링
ESampleExperienceLoadState LoadState = ESampleExperienceLoadState::Unloaded;

// Experience 로딩이 완료된 이후, BroadCasting Delegate
FOnSampleExperienceLoaded OnExperienceLoaded;

IsExperienceLoaded()

// 로드 완료 + Experience 존재하는지 체크
bool IsExperienceLoaded() { return (LoadState == ESampleExperienceLoadState::Loaded) && (CurrentExperience != nullptr); }
  • 외부에서 이 함수를 통해 로딩 완료를 확인하는 용도

CallOrRegister_OnExperienceLoaded()?

Header

// FOnSampleExperienceLoaded::FDelegate - 해당 델리게이트에서 요구하는 함수 타입을 인자로 받는다는 뜻
// &&(RValue Reference) 
// - '이동'하여 객체를 복사하는 대신 해당 자원을 그대로 사용함
// - 리터럴, 람다, 함수반환값 등과 같이 임시 생성된 객체를 포함받을 수 있음
// &(LValue Reference)
// - 이 방식은 메모리 상에 존재하는 객체만 받을 수 있음
// 
// 아마 RValue인 람다 or std::function 등만 인자로 받을 수 있어 보임
//
// OnExperienceLoaded 에 바인딩 하거나, Experience 로딩이 완료되었다면 호출
void CallOrRegister_OnExperienceLoaded(FOnSampleExperienceLoaded::FDelegate&& Delegate);

---

cpp

void USampleExperienceManagerComponent::CallOrRegister_OnExperienceLoaded(FOnSampleExperienceLoaded::FDelegate&& Delegate)
{
	if (IsExperienceLoaded())
	{
		// 요청하는 함수에게 이미 로딩이 되었으므로
		// 현재 Experience를 인자로 건네주며 실행시킨다(callback)
		Delegate.Execute(CurrentExperience);
	}
	else
	{
		// movetemp 를 통해 rvalue Reference 위치를 이동 시킴
		// Delegate 리스트에 해당 함수를 등록
		//
		// Delegate 객체는 내부적으로 필요한 변수를 메모리 할당해놓음
		// (함수 인자 등, 여러 상태를 저장하는 변수
		// 
		// ex)
		// TArray<int> a = {1,2,3,4}
		// delegate_type delegate = [a](){return a.num;}
		// -> a는 delegate_type 내부에 new로 할당이 된다
		//  -> 복사 비용을 낮추기 위하여 Move를 통하여 전달
		OnExperienceLoaded.Add(MoveTemp(Delegate));
	}
}
  • 이미 로드된 Experience라면
    바로 Delegate를 호출시켜 준다
    (그대로 현재 Experience를 넘겨주어 실행)
    (사실상 바로 callback)

  • 아니라면 Delegate에 등록시켜서
    나중에 로딩 완료되었을때
    한번에 broadcast 하는 용도

  • rvalue로 넘긴 이유는
    ‘임시 Delegate’를 move를 통하여
    현재 delegate에 등록
    (Add 할때, move를 통해 소유권을 양도 받고 있음)

  • ::FDelegate?
    해당 델리게이트 타입에 ‘바인딩’할 ‘객체’
    (보통 함수 포인터를 전달하는 방식)
    더 쉽게 말하자면
    Delegate에 들어간 ‘원소 타입’이라고 봐도 좋음
    따라서 우리는 const USampleExperienceDefinition*를 인자로 받는
    void 함수를 전달 받은 것과 같음

댓글남기기