0. 개요
게임을 저장할 때 월드 내의 액터, 액터의 위치 등을 저장한다.
1. 월드 내의 액터 저장
(1) LoadScreenSaveGame 저장 오브젝트에서 액터 저장 구조체 선언
USTRUCT()
struct FSavedActor
{
GENERATED_BODY()
UPROPERTY()
FName ActorName = FName();
UPROPERTY()
FTransform Transform = FTransform();
// 액터에서 직렬화된 멤버 변수 - SaveGame으로 지정된 것만 넣기
UPROPERTY()
TArray<uint8> Bytes;
};
월드에 존재하는 액터의 정보를 저장하기 위한 액터 저장 구조체이다.
직렬화된 Bytes 배열은 비트 플래그를 이용하여 액터의 멤버 변수를 저장할 것이다.
(2) '==' 연산자 오버로딩
inline bool operator==(const FSavedActor& Left, const FSavedActor& Right)
{
return Left.ActorName == Right.ActorName;
}
해당 구조체를 이용하여 추후에 배열로 따로 저장할 때 중복 없이 배열에 추가하기 위한 AddUnique 함수를 사용하기 위해 동등 연산자를 오버로딩한다.
(3) 맵 저장 구조체 선언
USTRUCT()
struct FSavedMap
{
GENERATED_BODY()
UPROPERTY()
FString MapAssetName = FString();
UPROPERTY()
TArray<FSavedActor> SavedActors;
};
한 맵에서 저장한 이후, 다음 맵으로 넘어갈 때, 이전의 정보는 필요가 없게 된다.
따라서 맵에 따라 액터를 저장하기 위해 맵 이름과 저장된 액터 배열을 관리하는 구조체를 생성한다.
(4) FSavedMap 구조체를 저장 오브젝트 내에 멤버 변수로 선언
UPROPERTY()
TArray<FSavedMap> SavedMaps;
};
저장된 맵을 모아놓는 배열을 선언한다.
(4-1) 구조체의 존재를 확인하는 함수 선언
FSavedMap GetSavedMapWithMapName(const FString& InMapName);
bool HasMap(const FString& InMapName);
FSavedMap ULoadScreenSaveGame::GetSavedMapWithMapName(const FString& InMapName)
{
for (const FSavedMap& Map : SavedMaps)
{
if (Map.MapAssetName == InMapName)
return Map;
}
return FSavedMap();
}
bool ULoadScreenSaveGame::HasMap(const FString& InMapName)
{
for (const FSavedMap& Map : SavedMaps)
{
if (Map.MapAssetName == InMapName)
return true;
}
return false;
}
저장된 맵에 해당하는 맵이 있으면 구조체 또는 불리언을 반환하는 함수이다.
2. 월드 상태 저장
(1) AuraGameModeBase에서 월드 상태 저장 함수
// 월드 저장
void SaveWorldState(UWorld* World);
(1-1) 실제 맵 이름 가져오기
// 월드의 이름과 맵 이름이 일치하는가
FString WorldName = World->GetMapName();
WorldName.RemoveFromStart(World->StreamingLevelsPrefix);
GetMapName 함수는 맵의 전체 이름을 가져온다.
이 때, 실제 맵 이름만 가져오기 위해서는 전체 이름에서 접두사를 제외하고 가져와야 한다.
ex) UEDPIE_0_MyMap => MyMap
(1-2) 맵 저장하기
void AAuraGameModeBase::SaveWorldState(UWorld* World)
{
// 접두사를 제외한 실제 이름만 가져오기
FString WorldName = World->GetMapName();
WorldName.RemoveFromStart(World->StreamingLevelsPrefix);
UAuraGameInstance* AuraGI = Cast<UAuraGameInstance>(GetGameInstance());
check(AuraGI);
if (ULoadScreenSaveGame* SaveGame = GetSaveSlotData(AuraGI->LoadSlotName, AuraGI->LoadSlotIndex))
{
// 저장 데이터의 배열에 해당 맵이 없다면 추가
if (SaveGame->HasMap(WorldName) == false)
{
FSavedMap NewSavedMap;
NewSavedMap.MapAssetName = WorldName;
SaveGame->SavedMaps.Add(NewSavedMap);
}
(2) 액터 저장하기
맵 내의 특정 액터를 집어 저장하기 위해서는 해당 액터를 분류할 수 있어야 한다.
이를 편하게 할 수 있는 방법 중 하나는 인터페이스를 사용하는 것이다.
(2-1) Unreal Inteface C++ 클래스 생성
SaveInterface의 이름을 가지는 클래스를 생성한다.
(2-2) 체크 포인트 액터가 저장 인터페이스를 상속받게 하기
UCLASS()
class AURA_API ACheckPoint : public APlayerStart, public ISaveInterface
{
(2-3) 체크 포인트에 이미 도달 했는지 판단하는 불리언 추가
// 이미 도달했는가
UPROPERTY(BlueprintReadOnly, SaveGame)
bool bReached = false;
해당 불리언을 FSavedActor의 Bytes 배열에 넣어 비트 플래그로 저장하려 한다.
(3) 직렬화
void AAuraGameModeBase::SaveWorldState(UWorld* World)
{
// 접두사를 제외한 실제 이름만 가져오기
FString WorldName = World->GetMapName();
WorldName.RemoveFromStart(World->StreamingLevelsPrefix);
UAuraGameInstance* AuraGI = Cast<UAuraGameInstance>(GetGameInstance());
check(AuraGI);
if (ULoadScreenSaveGame* SaveGame = GetSaveSlotData(AuraGI->LoadSlotName, AuraGI->LoadSlotIndex))
{
// 저장 데이터의 배열에 해당 맵이 없다면 추가
if (SaveGame->HasMap(WorldName) == false)
{
FSavedMap NewSavedMap;
NewSavedMap.MapAssetName = WorldName;
SaveGame->SavedMaps.Add(NewSavedMap);
}
위의 과정에 이어서, 액터의 SaveGame 프로퍼티로 지정된 멤버 변수를 직렬화하여 저장한다.
//
FSavedMap SavedMap = SaveGame->GetSavedMapWithMapName(WorldName);
SavedMap.SavedActors.Empty();
// 월드 내의 모든 액터 순회
// 최적화를 위해 특정 조건을 만들수도 있음
for (FActorIterator It(World); It; ++It)
{
AActor* Actor = *It;
// 사망 상태가 아닐 때 또는 저장 인터페이스를 상속받지 않을 때 패스
if (IsValid(Actor) == false || Actor->Implements<USaveInterface>() == false)
continue;
FSavedActor SavedActor;
SavedActor.ActorName = Actor->GetFName();
SavedActor.Transform = Actor->GetTransform();
// 메모리 라이터 생성
FMemoryWriter MemoryWriter(SavedActor.Bytes);
// 아카이브 생성
FObjectAndNameAsStringProxyArchive Archive(MemoryWriter, true);
Archive.ArIsSaveGame = true;
// 직렬화
Actor->Serialize(Archive);
SavedMap.SavedActors.AddUnique(SavedActor);
}
// 이전 맵에 대한 정보 제거
for (FSavedMap& MapToReplace : SaveGame->SavedMaps)
{
if (MapToReplace.MapAssetName == WorldName)
{
MapToReplace = SavedMap;
}
}
// 데이터 저장
UGameplayStatics::SaveGameToSlot(SaveGame, AuraGI->LoadSlotName, AuraGI->LoadSlotIndex);
}
}
(3-1) 월드 내의 모든 액터를 순회하는 반복자, FActorIterator
FActorIterator는 매개 변수로 전달받은 월드 내의 모든 액터 각자를 가리킨다.
(3-2) 메모리 라이터, FMemoryWriter
특정 바이트 배열에 데이터를 저장하기 위한 메모리 라이터이다.
(버퍼의 일종)
(3-3) 아카이브, FObjectAndNameAsStringProxyArchive
일반적으로 아카이브는 포인터의 메모리 주소를 저장한다.
그러나, 메모리 주소는 매번 달라지기 때문에 특정 메모리 주소를 저장하는 것은 의미가 없다.
따라서 객체를 저장하여 추후에 쉽게 찾기 위해 식별하기 가장 좋은 오브젝트의 이름을 문자열로 가져와 저장한다.
(3-4) 아카이브 플래그 변수, ArIsSaveGame
아카이브 내에 저장되는 데이터가 게임 세이브/로드 용의 데이터인지 판단한다.
UPROPERTY가 SaveGame으로 지정된 멤버 변수를 저장한다.
(3-5) 저장 프로세스
최종적으로, SavedActor 안에 액터의 이름, 트랜스폼, 직렬화 된 액터의 이름(혹은 경로)가 저장되고, 래핑된 구조체를 SavedMap 구조체가 들고있게 되는 방식이다.
'UE 5 스터디 > Gameplay Ability System(GAS)' 카테고리의 다른 글
30-1. 맵 이동 - (1) 커스텀 뎁스 스텐실 - 액터 하이라이트 (0) | 2025.04.11 |
---|---|
29-9. 저장 - (10) 월드 불러오기와 역직렬화(Deserialize) (0) | 2025.04.10 |
29-7. 저장 - (8) 어빌리티 저장 및 불러오기 (0) | 2025.04.08 |
29-6. 저장 - (7) 플레이어 데이터 저장 및 불러오기, 디버그 (0) | 2025.04.07 |
29-5. 저장 - (6) 진행 상황 저장 (0) | 2025.04.07 |