미리보기

구현내용
플레이어가 맵 이탈, 낙사, 혹은 강제 사망 상태에 진입하는 상황을
트리거 기반으로 처리하였으며 특정 구간 진입 시
현재 위치와 맵 정보를 자동으로 저장하는 오토 세이브 시스템을 구현했습니다
흔히 부르는 개념 세이브의 일종입니다
최종으로 기획된 트리거의 역할은 이와 같습니다
- 낙사 처리 트리거
- 강제 죽음 트리거
- 오토 세이브 트리거
- 듀토리얼 매니저 트리거
이번 글에서는 트리거 기반으로 처리한 낙사 시스템과 오토 세이브 기능의 구조 및 설계 의도를 다룹니다.
사용 클래스
| 클래스 이름 | 사용 목적 |
| ManagerTrigger(액터) | 맵 상에 배치되는 범용 트리거 액터 |
| SaveFile(USaveGame) | 게임 상태 데이터 컨테이너 게임 진행 상태를 순수 데이터 형태로 저장하는 역할 |
| SaveManager(UObject) | SaveFile을 실제로 디스크에 저장하고 불러오는 저장 로직 전담 클래스 |
| ProjectGameInstance(Gameinstance) | 게임 전반에서 공통으로 사용되는 전역 관리자 역할 |
| LoadGameMode(AGameModeBase) | 로딩맵에서 사용되는 로딩맵 전용 GameMode 게임 시작과 복귀의 관문 역할 |
구현
전반적인 세이브의 로직은 이전글에서 다루었습니다
이번에는 세이브 게임의 확장격인 오토세이브를 다룹니다
2025.12.02 - [Unreal 프로젝트 다이어리/두번째 프로젝트] - Unreal - 세이브 게임
Unreal - 세이브 게임
구현내용게임중 진행되는 세이브되어야하는 데이터들의 값 저장 구현목적언리얼에서는 게임을 실행할 때마다 이전 인스턴스가 모두 초기화되기 때문에, 별도의 조치를 취하지 않으면 게임 데
lucodev.tistory.com
플레이어가 특정 공간에 진입했을때 발생하는 게임 진행 이벤트를 중앙에서 제어하기 위해 설계된 액터가
ManagerTrigger입니다
간단하게 이벤트 단위의 관리자 개념입니다
UENUM(BlueprintType)
enum class EManagerTriggerType : uint8
{
None,
Fall,
Tutorial,
AutoSave
};
void AManagerTrigger::OnOverlapBegin(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (OtherActor == mainChar && OtherComp == capsuleComp)
{
if (triggerMap.Contains(triggerType))
{
triggerMap[triggerType](OtherActor);
if (bOnce)
triggerMap.Remove(triggerType);
}
}
}
void AManagerTrigger::InitTrigger()
{
triggerMap.Add(EManagerTriggerType::Fall, [this](AActor* OtherActor) {
mainChar->FallingDie();
ChangeDeadCam();
});
triggerMap.Add(EManagerTriggerType::Tutorial, [this](AActor* OtherActor) {
//mainGameInst-> 구현예정..
});
triggerMap.Add(EManagerTriggerType::AutoSave, [this](AActor* OtherActor) {
mainGameInst->SavePlayerTransform(mainChar);
});
}
원하는 트리거를 선택하여 사용합니다

오토세이브
오토 세이브 트리거를 통해 이벤트가 발생하면 현재 사용하고있는 게임인스턴스를 통해
현재 플레이어의 위치정보 및 소속 맵 정보를 저장하고 해당 위치로 시작했을때 플레이어가 페이드인 을 하는동안
위치를 변경하도록 설계하였습니다
그리고 언리얼 엔진은 PIE 환경에서 맵 이름앞에 UEDPIE_0 , UEDPIE_1 같은 접두사를 자동으로 붙혀서
접두사를 제거한 순수한 맵 이름만을 저장하도록 처리하였습니다
void UProjectGameInstance::SavePlayerTransform(AMainCharacter* mainCharacter)
{
USaveFile* saveF = saveManager->currentSave;
saveF->playerLoc = mainCharacter->GetActorLocation();
saveF->playerRot = mainCharacter->GetActorRotation();
FString MapName = mainCharacter->GetWorld()->GetMapName();
MapName.RemoveFromStart(TEXT("UEDPIE_0_"));
MapName.RemoveFromStart(TEXT("UEDPIE_1_"));
saveF->savedMapName = FName(*MapName);
saveManager->SaveGame();
}
void UProjectGameInstance::RestorePlayerTransform()
{
APlayerController* pc = GetWorld()->GetFirstPlayerController();
if (!pc)
return;
APawn* mayPawn = pc->GetPawn();
if (!mayPawn)
return;
mayPawn->SetActorLocation(saveManager->currentSave->playerLoc);
mayPawn->SetActorRotation(saveManager->currentSave->playerRot);
}
플레이어는 게임을 시작하면 Beginplay에서 페이드인이 재생되는 동안
RestorePlayerTransform으로 위치를 재설정하게됩니다
비동기 맵 로딩 시스템
언리얼 엔진에서 OpenLevel을 직접 호출할 경우, 맵 규모가 커질수록
로딩 과정에서 순간적인 프레임 드랍이나 화면 정지와 같은 UX 저하가 발생할 수 있습니다.
이를 개선하기위해 LoadPackageAsync를 활용하여 맵 패키지를 비동기 방식으로 사전 로딩 한 뒤
로딩이 완료된 시점에 실제 맵 전환을 수행하는 구조로 설계하였습니다
구조는 이와 같습니다

로딩맵에서 게임모드를 별도로 만들어 비동기 사전 로딩의 구현 방식입니다
void ALoadGameMode::BeginPlay()
{
Super::BeginPlay();
gameIst = Cast<UProjectGameInstance>(GetGameInstance());
if (gameIst && gameIst->loadingWidget)
gameIst->loadingWidget->AddToViewport();
FName targetMap = NAME_None;
if (!gameIst->saveManager->currentSave->savedMapName.IsNone())
targetMap = gameIst->saveManager->currentSave->savedMapName;
if (!targetMap.IsNone())
{
FString levelPath = FString::Printf(TEXT("/Game/Maps/%s.%s"),*targetMap.ToString(),*targetMap.ToString());
LoadPackageAsync(levelPath,FLoadPackageAsyncDelegate::CreateUObject(this,&ALoadGameMode::OnGameLevelLoaded));
}
}
void ALoadGameMode::OnGameLevelLoaded(const FName& packageName, UPackage* loadedPackage, EAsyncLoadingResult::Type result)
{
if (gameIst && gameIst->loadingWidget)
gameIst->loadingWidget->SetVisibility(ESlateVisibility::Visible);
if (result == EAsyncLoadingResult::Succeeded)
{
FTimerHandle TH_OpenLevelPack;
FTimerDelegate TD_OpenLevelDel;
TD_OpenLevelDel.BindLambda([this, packageName]()
{
FString mapName = packageName.ToString();
UGameplayStatics::OpenLevel(this, FName(*mapName));
});
GetWorldTimerManager().SetTimer(TH_OpenLevelPack, TD_OpenLevelDel, 3.0f, false);
}
}
낙사
이제 플레이어가 높은곳에서 떨어져 낙사 이벤트가 발생되는 지점에 트리거 타입이 Fall인 트리거 매니저를 깔아주고
트리거에 지정된 카메라에 카메라 시점을 변경하면서 플레이어의 행동을 처리해주었습니다
시점 변경은 SetViewTarget을 사용하였습니다
void AManagerTrigger::ChangeDeadCam()
{
FViewTargetTransitionParams camParams;
camParams.BlendTime = 0.6f;
camParams.BlendFunction = VTBlend_EaseInOut;
camParams.BlendExp = 2.0f;
camParams.bLockOutgoing = true;
mainPc->SetViewTarget(this, camParams);
}
플레이어의 행동은 모션매칭을 사용하고있기때문에 스키마를 추가한뒤
선택기 테이블에 FallingDie라는 State를 추가하여 낙사처리를 해주었습니다

모션매칭과 모션매칭 스키마또한 이전에 다룬적이 있으니 궁금하신분은 밑의 글에서 한번 구경해보시길바랍니다
2025.08.15 - [Unreal 프로젝트 다이어리/두번째 프로젝트] - Unreal - 모션매칭(Motion Matching) - 스키마 데이터 관리하기 (점프,달리기)
Unreal - 모션매칭(Motion Matching) - 스키마 데이터 관리하기 (점프,달리기)
1편과 이어집니다2025.08.09 - [Unreal5 프로젝트 다이어리2] - Unreal - 모션매칭(Motion Matching) - 기본이동 구현하기 Unreal - 모션매칭(Motion Matching) - 기본이동 구현하기Motion Trajectory플러그인을 설치해줍니
lucodev.tistory.com
결과물
낙사

오토세이브는 GIF파일로 설명이 불가능한거같네요 영상을 남깁니다
낙사를 한뒤 땅바닥에서 스폰하는게 아닌 마지막으로 오토세이브 된 위치에서 다시 스폰하게됩니다
'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글
| Unreal - 달리 줌 (Dolly - Zoom) (0) | 2026.01.25 |
|---|---|
| Unreal - 상호작용 추가 (2) | 2026.01.24 |
| Unreal - 죽음 / 리스폰 (0) | 2026.01.20 |
| Unreal - 적 공격 간파하기 (2) | 2026.01.16 |
| Unreal - 가드 (Guard) / 체간 (Posture) 시스템 (4) | 2026.01.12 |