Experience Load 2
Experience Load 2
이전 블로깅 작성 후 시간이 꽤 지났지만
다시 복습하면서 나아가보자
GameModeBase로 돌아와 InitGameState를 생성해주기
이제 USampleExperienceManagerComponent 에서 만든
CallOrRegister_OnExperienceLoaded() 을 사용하기 위하여
게임 모드 베이스 쪽 클래스에서 InitGameState을 생성해준다
UCLASS()
class SAMPLES_API ASampleGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
ASampleGameMode();
virtual void InitGame(const FString& MapName, const FString& Options, FString& ErrorMessage) override;
virtual void InitGameState() final;
void OnExperienceLoaded(const USampleExperienceDefinition* currentExperience);
void HandleMatchAssignmentIfNotExpectingOne();
}
---
...
void ASampleGameMode::InitGameState()
{
Super::InitGameState();
// GameState 내부에 해당 클래스 존재시 반환
// Experience 비동기 로딩을 위한 Delegate를 준비
USampleExperienceManagerComponent* ExperienceManagerComponent = GameState->FindComponentByClass<USampleExperienceManagerComponent>();
// InitGameState 가 호출된 시점에는 ExperienceManagerComponent 가 생성되어있음
// (GameState 에서 생성하면서 해당 클래스를 생성해놓았으므로)
check(ExperienceManagerComponent);
// OnExperienceLoaded를 등록
// 로딩이 되어있다면 바로 호출하고, 아니면 기다렸다 호출된다
// 현재 시점에선 로드가 안되어있으므로 기다렸다 호출하기 위함
ExperienceManagerComponent->CallOrRegister_OnExperienceLoaded(FOnSampleExperienceLoaded::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded));
}
void ASampleGameMode::OnExperienceLoaded(const USampleExperienceDefinition* currentExperience)
{
// 아직 대기!
}
...
-
InitGameState를 통해 GameState를 만든 후
Experience 비동기 로딩
을 위하여
로딩 완료 후,OnExperienceLoaded
를 호출하라고 등록함
(Experience들이 완전히 로딩되면 그재서야 GameMode가 Player를 생성시켜줌)
(Experience 로딩 여부는 ManagerComponent가 하겠지~) -
- OnExperienceLoaded
- 아직은 구현하지 않았지만 Player를 다시 생성시켜줄 예정
(모든 비동기 로딩이 끝났으니 Player를 안전하게 생성)
- OnExperienceLoaded
-
- FindComponentByClass?
- 해당 개체 내부에서 해당 클래스가 있다면 반환 없으면 null
(해당 템플릿 클래스를 ‘상속’받아도 Ok!)
(다만 Find’Component’이기에 컴포넌트 타입만 가능하다)
- check 는 ‘초기화’ 과정에서 사용할 법한 일종의 assert
해당 조건이 false라면 crash 시키고, 디버거를 통해 어디서
실패했는지를 알려준다
(디버깅용 장치로서, Init 시점 or 개발 과정에서 사용할 법 하다)
- FindComponentByClass?
-
- final을 붙인 이유??
- 게임 모드의 InitGameState를 상속한 후
더 이상 하위 게임 모드에선 수정하지 말 것을 당부
-> ‘게임 모드’에서 GameState 와 관련된 생성 로직을 건들지 말것!
- final을 붙인 이유??
그래서 우리가 어디쯤 온거지?
현재 1번 단계를 완료한 상황!
- 2번 단계부터는 엔진이 진행해준다
- 해당 과정을 요약하자면,
World 내에서 PlayerStart를 찾아 시작 위치를 정함
(혹시 없다면 생성 가능한 랜덤한 위치를 정함)
이후, 우리가 설정한 기본 Pawn을 세팅
그리고 PostLogin 시점에서 Player Spawn 과정을 시작하게 됨
- 해당 과정을 요약하자면,
그런데 여기서 한가지 문제가 발생한다
- 고작 1 프레임으로
ExperienceLoad
가 완료될리가 없다는 것!
우리가 원하는 것은
-
로딩 완료 후, 캐릭터를 소환하는 것
(그 Experience 정보들을 이용하여 내부 세팅을 하고 싶은 것이므로!) -
그렇기에 이러한 과정을 잠시 막아두어야 함
언제까지? 로딩이 완료될때까지!
(OnExperienceLoaded 에 Player Spawn 관련 코드가 들어가게 되는 이유!)
Restart 라는 함수를 통하여 생성해줄 예정
Restart 호출을 통하여 Experience Load 가 완료된 후
플레이어를 Spawn 시켜주는 것이 우리의 목표!
먼저 PlayerState로 가자
PawnData를 참조하고 캐싱해 놓는다고 이야기한 PlayerState 쪽으로 가
PawnData와 관련된 부분을 작성하기
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerState.h"
#include "SamplePlayerState.generated.h"
class USampleExperienceDefinition;
class USamplePawnData;
UCLASS()
class SAMPLES_API ASamplePlayerState : public APlayerState
{
GENERATED_BODY()
// AActor's interface
public:
virtual void PostInitializeComponents() final;
// member method
public:
// 폰 데이터 캐싱을 위하여 Experience 로딩 완료때 가져오기 위함
void OnExperienceLoaded(const USampleExperienceDefinition* CurrentExperience);
public:
// Cacheing 하는 이유? (ExperienceManagerComponent에도 있지 않나?)
// 나중에 PlayerState에 GAS 컴포넌트를 붙일 예정
// -> GAS가 폰데이터를 참조할 것이기에 미리 캐싱 코드를 만들어 놓음
UPROPERTY()
TObjectPtr<const USamplePawnData> PawnData;
};
---
cpp
void ASamplePlayerState::PostInitializeComponents()
{
Super::PostInitializeComponents();
const AGameStateBase* GameState = GetWorld()->GetGameState();
check(GameState);
USampleExperienceManagerComponent* ExperienceManagerComponent = GameState->FindComponentByClass<USampleExperienceManagerComponent>();
check(ExperienceManagerComponent);
ExperienceManagerComponent->CallOrRegister_OnExperienceLoaded(FOnSampleExperienceLoaded::FDelegate::CreateUObject(this, &ThisClass::OnExperienceLoaded));
}
void ASamplePlayerState::OnExperienceLoaded(const USampleExperienceDefinition* CurrentExperience)
{
// 아직 대기!
}
-
PawnData를 미리 캐싱해두는 이유?
GAS 쪽을 사용하기 위해 미리 캐싱을 해둠! -
PostInitializeComponents
?- Beginplay 이전에 호출되는 가장 마지막 지점
(모든 컴포넌트 들이 준비된 상태에서의 마지막 세팅)
해당 상황에서 GameState쪽에 OnExperienceLoaded 를 Delegate 시켜 놓는다
-
PlayerState가 생성되는 시점에서는 GameState가 존재하기에!
GameState를 호출 가능!
그렇기에 GameState 내부의 ExpManager에게
Delegate를 걸 수 있음 - OnExperienceLoaded 에서는 PawnData를 캐싱할 예정
다음은 PawnData에서 PawnClass를 세팅해준다
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "SamplePawnData.generated.h"
UCLASS()
class SAMPLES_API USamplePawnData : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
USamplePawnData(const FObjectInitializer& ObjectInitalizer = FObjectInitializer::Get());
// Pawn의 class
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Sample|Pawn")
TSubclassOf<APawn> PawnClass;
};
‘만들어질 Pawn’을 이쪽에서 TSubclassOf로 받을 예정
(Bp)
-
이전에 말하였듯
PrimaryDataAsset
이기에
BP 쪽에서 설정한 Pawn이 생성과정을 거쳐 생성되게 됨 -
이걸 BP에서 만들었다면 이걸
Experiecne Definition
에 넣어주자!
Experiecne Definition
|
- Level
- USamplePawnData
|- PawnClass
다만 여전히 시작시 Character 자체가 ‘생성’됨!
생성을 막아야 한다!
GameMode로 가자
GameMode.h
// HandleStartingNewPlayer
virtual void HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer) final;
// SpawnDefaultPawnAtTransform
virtual APawn* SpawnDefaultPawnAtTransform_Implementation(AController* NewPlayer, const FTransform& SpawnTransform) final;
bool isExperienceLoaded() const;
---
cpp
bool ASampleGameMode::isExperienceLoaded() const
{
check(GameState);
USampleExperienceManagerComponent* ExperienceManagerComponent = GameState->FindComponentByClass<USampleExperienceManagerComponent>();
check(ExperienceManagerComponent);
return ExperienceManagerComponent->IsExperienceLoaded();
}
void ASampleGameMode::HandleStartingNewPlayer_Implementation(APlayerController* NewPlayer)
{
if (isExperienceLoaded())
{
// experience가 load 된 이후에 pawn을 생성할 목적
// 그리고 load 완료후 다시 이 함수를 호출
Super::HandleStartingNewPlayer_Implementation(NewPlayer);
}
}
APawn* ASampleGameMode::SpawnDefaultPawnAtTransform_Implementation(AController* NewPlayer, const FTransform& SpawnTransform)
{
// 당장은 그냥 부모로 넘겨준다
return Super::SpawnDefaultPawnAtTransform_Implementation(NewPlayer,SpawnTransform);
}
-
HandleStartingNewPlayer_Implementation
?- 새로운 플레이어가 들어올때 호출
보통 PostLogin() 직후 호출되어
그 플레이어의 Pawn을 Spawn시키고 Possess 시키는 절차를 Trigger 함
(일반적으로 이걸 커스텀 Override 하는 경우는 RestartPlayer 같은 함수로
다시 호출시키는 것을 감안함 -> 첫 접속시 몇가지 로직을 끼워넣고 싶은 경우)
-
SpawnDefaultPawnAtTransform_Implementation
?- 실제 Pawn 생성 시에 호출
ChoosePlayerStart 같은 걸로 ‘위치’를 구한 후
Pawn을 스폰시키는 함수
(보통 Pawn Class, 스폰 방식 변경이 필요할때 커스텀 고려)
호출 흐름에 대한 간략한 타임라인
플레이어 접속 확정
↓
PostLogin(NewPlayer)
↓
HandleStartingNewPlayer_Implementation(NewPlayer) // 새로 들어온 플레이어를 ‘게임에 투입’시키는 입구
↓
RestartPlayer(NewPlayer) // 표준 스폰 루틴
↓
ChoosePlayerStart(NewPlayer) → StartSpot 선정
↓
SpawnDefaultPawnFor(...) 또는 SpawnDefaultPawnAtTransform_Implementation(NewPlayer, StartSpot->GetTransform())
↓
NewPlayer->Possess(SpawnedPawn)
↓
SetPlayerDefaults(Pawn) / Pawn::Restart() / OnPossess / PossessedBy 등 초기화 콜백
위에서 본 이미지를 다시 한번 보면
저 1번과 3번에 각각 포함되는 함수들이다!
따라서
-
HandleStartingNewPlayer_Implementation에서
Experience Load가 완료되지 않았다면
Spawn 시키지 않도록 설정해버린다!
(그렇기에 우리가 Experience가 완료되면 별도로 캐릭터를 Spawn 시켜주어야 함!) -
해당 함수 구현시 더이상 DefaultPawn 이 생성되지 않아
Experience Load가 완료될때까지 생성을 막을 수 있다!
실제 Experience Load는 다음 TIL에…
댓글남기기