[DevNote] 체크 포인트
세이브 포인트
- 캠프라는 이름의 체크 포인트를 정의했다
- 던전 안에는 여러 캠프가 존재하며, 던전 진입 시 마지막으로 활성화 및 상호작용한 체크포인트의 위치로 스폰되어야 했다
- 멀티 상황에서 최대 4개의 스폰 포인트가 있고, 스폰 시 특정 애니메이션을 출력해야 했다
진입 시 Spawn Point
AActor* ACustomGameMode::FindPlayerStart_Implementation(AController* Player, const FString& IncomingName)
{
// ...
APlayerController* PlayerController = Cast<APlayerController>(Player);
const int32 PlayerID = FL::GetPlayerID(PlayerController);
// 레벨 내에 Camp를 먼저 찾는다
if(AActor* StartSpot = GetCampStartByIndex(PlayerID))
{
return StartSpot;
}
// ...
}
APlayerStart* ACustomGameMode::GetCampStartByIndex(int32 Index)
{
ACampActor* DefaultCamp = nullptr;
// 시작 캠프가 없을 때 StartingCamp 탐색
if (IsValid(StartingCamp) == false)
{
// ActorManagerSubSystem 사용해 GetAllCampActors 호출하려 했으나 타이밍상 아직 액터를 컨테이너에 수집하기 전이라 정상적으로 작동하지 않음.
// 그래서 UGameplayStatics::GetAllActorsOfClass 함수를 사용
TArray<AActor*> AllCampActors;
UGameplayStatics::GetAllActorsOfClass(this, ACampActor::StaticClass(), AllCampActors);
for (AActor* Actor : AllCampActors)
{
if (ACampActor* CampActor = Cast<ACampActor>(Actor))
{
// CampActor->InitializeCampData();
// 레벨에서 정의된 디폴트 캠프 저장
if (CampActor->IsActivatedByDefault())
{
DefaultCamp = CampActor;
}
// 저장 데이터에서 마지막으로 활성화된 캠프인지 확인
if (CampActor->IsActivatedFromSaveData())
{
StartingCamp = CampActor;
break;
}
}
}
}
// 탐색 후에도 여전히 래벨에 (활성화된) 캠프가 없는 경우
if (IsValid(StartingCamp) == false)
{
// DefaultCamp에서 시작
if (IsValid(DefaultCamp))
{
StartingCamp = DefaultCamp;
}
// DefaultCamp도 없으면 그냥 nullptr 반환
if (IsValid(StartingCamp) == false)
{
return nullptr;
}
}
APlayerStart* PlayerStartInCamp = StartingCamp->GetPlayerStart(Index);
// 캠프의 PlayerStart가 Valid하지 않은 경우
if (IsValid(PlayerStartInCamp) == false)
{
return nullptr;
}
return PlayerStartInCamp;
}
- GameMode의 FindPlayerStart_Implementation 함수를 오버라이드 하고, 그 안에서 자체 로직을 구현한다
- Player는 접속한 순서대로 고유 아이디를 1 ~ N까지 할당 받으며, 이 Index를 기준으로 Camp Actor 내에 배치된 PlayerStart를 전달받아 이 위치로 스폰한다
Note
Is Spatially Loaded 토글은 해제해야 한다
디폴트로 true로 세팅되어 있는데, 이렇게 되면 스트리밍 소스의 범위 내에 있을 때만 로드하기 때문에
월드파티션이 적용된 레벨에서 체크포인트의 역할을 제대로 할 수 없다
너무 멀리 떨어져 있는 구역은 스트리밍이 되어있지 않을 것이고, 그럼 해당 체크포인트 액터도 로드되어 있지 않았을 테니까 말이다.
체크포인트 상태 처리
- 캠프는 마지막으로 활성화된 상태, 활성화된 상태, 비활성화된 상태 등으로 구분되어야 했다
- 마지막으로 활성화한 캠프는 레벨 진입 시 캐릭터가 최초 스폰되는 지역이다
- 활성화된 캠프로만 텔레포트할 수 있다
UCLASS()
class ACustomPlayerStateBase : public APlayerState
{
GENERATED_BODY()
public:
// ..
private:
// 현재 플레이어의 ID
UPROPERTY(Replicated)
int32 RepPlayerID = -1;
// 마지막으로 활성화된 캠프 ID
UPROPERTY()
int32 ActivatedCampID = -1;
// 발견한 캠프 ID 목록
UPROPERTY()
TArray<int32> DiscoveredCampIDs;
- 이들의 관리는 PlayerState에 버퍼를 두고 관리했다
- 데이터 업데이트 필요 시 로드하거나, 세이브 한다
- 활성화 시 연출은 동기화되어야 하며, 한 번 활성화한 캠프는 다른 플레이어에 의해 다시 활성화를 거치면 안되니, 캠프의 활성화 상태는 Replicated 됐어야 한다
- 비활성화, 활성화 이외에도 상태가 다양했다면 GameplayTag를 사용해도 좋았을 거 같다
멀티 코옵 시 4개의 플레이어 Spawn Point
- 최대 4인의 스폰 포인트가 필요했다
UCLASS()
class UCustomPlayerStartComponent : public UChildActorComponent
{
GENERATED_BODY()
public:
UCustomPlayerStartComponent(const FObjectInitializer& ObjectInitializer);
class APlayerStart* GetPlayerStart() const;
void PlayAbl(AActor* InPlayerCharacter, TSubclassOf<class UAblAbility> InAblToPlay);
FORCEINLINE TSubclassOf<class UAblAbility> GetSpawnAbl() const { return SpawnAbl; }
FORCEINLINE TSubclassOf<class UAblAbility> GetTeleportAbl() const { return TeleportAbl; }
private:
// 최초 진입 시 출력할 스타팅 포즈
UPROPERTY(EditAnywhere, Category = AAPlayerStart)
TSubclassOf<class UAblAbility> SpawnAbl;
// 캠프 간 이동 시 출력할 애니메이션
UPROPERTY(EditAnywhere, Category = AAPlayerStart)
TSubclassOf<class UAblAbility> TeleportAbl;
};
- CampActor 전용 Component를 만들어 붙이는 방식으로 풀었다
- 다만, ChildActorComponent를 사용하지 말라는 Epic 쪽의 권고가 있었기에, 추후 변경 혹은 다음에 비슷한 작업을 할 경우 다른 방법을 고안해야 겠다
- ChildActor를 APlayerStart로 세팅한다
활성화와 활성화 이후 상호작용
- 체크포인트는 첫번째 상호작용 시에는 활성화 하고, 두번째 상호작용부터는 레벨 이동 등 기능을 지원해야 했다
- 이 부분이 골치아팠던 것이, 기본적으로 NPC가 아닌, 레벨 액터의 상호작용 시스템은 1회성으로 디자인 되어 있었다
- 그래서 그냥 토글로 활성화 / 비활성화 상태를 나눈 뒤 조건을 체크해서 대응하는 상호작용 로직을 타도록 디자인했다
- 구조를 잡는 게 나았을 텐데, 2단계보다 더 많거나 그렇지도 않아서 그냥 이렇게 했다
텔레포트 시 카메라 이슈
bool ACampActor::TryTeleport(APlayerController* PlayerController)
{
if (IsValid(PlayerController) == false || IsValid(PlayerController->GetPawn()) == false)
{
return false;
}
const int32 PlayerID = AAFL::GetPlayerID(PlayerController);
if (ACustomALSCharacter* PlayerCharacter = Cast<ACustomALSCharacter>(PlayerController->GetPawn())
; PlayerStarts.Contains(PlayerID) && IsValid(PlayerStarts[PlayerID]))
{
PlayerCharacter->TeleportTo(PlayerStarts[PlayerID]->GetPlayerStart()->GetTargetLocation(), PlayerStarts[PlayerID]->GetPlayerStart()->GetActorRotation());
PlayerCharacter->GetMovementComponent()->StopMovementImmediately();
// 캠프 간 이동 시 이동 Abl 재생
PlayerStarts[PlayerID]->PlayAbl(PlayerCharacter, PlayerStarts[PlayerID]->GetTeleportAbl());
// 카메라도 이동
ACustomALSPlayerCameraManager* PlayerCameraManager = Cast<ACustomALSPlayerCameraManager>(PlayerController->PlayerCameraManager);
check(PlayerCameraManager);
PlayerCameraManager->TeleportCamera(false);
return true;
}
return false;
}
- 카메라 피벗 이동 시, 실제 카메라는 래깅을 주면서 피벗을 추적하는 로직으로 작동했다
- 그래서 플레이어가 텔레포트를 하는 등 순간적으로 먼 위치를 이동하면, 카메라 Position도 같이 변하도록 별도의 작업이 필요했다
void ACustomALSPlayerCameraManager::TeleportCamera(bool bRotate)
{
// 플레이어 캐릭터가 바라보는 방향으로 카메라 방향도 수정 필요
if (bRotate)
{
EnableCameraRotationUpdate(false);
const FTransform& PivotTarget = ControlledCharacter->GetThirdPersonPivotTarget();
// 임시 저장해둔 TargetCameraRotation도 업데이트
CameraBuffer.TargetCameraRotation = PivotTarget.GetRotation().Rotator();
}
// 현재 카메라 POV 받아오기
FMinimalViewInfo POV;
CustomCameraBehavior(10.f, POV.Location, POV.Rotation, POV.FOV);
// Location과 Rotation만 업데이트
POV.Location = TargetCameraLocation;
POV.Rotation = TargetCameraRotation;
SetCameraCachePOV(POV);
if (bRotate)
{
EnableCameraRotationUpdate(true);
}
}
Note
Checkpoint Component 같은 걸 만들어서 해당 위치로의 스폰, 이동, 저장 및 로드 등의 기능을 구현했어야 했다.
체크포인트 역할을 하는 액터가 하나였기에 망정이지, 같은 기능을 하는 액터가 다양하게 기획되었다면 끔찍했을 것…