diff --git a/Content/G2I_Game/Core/Controllers/BP_PlayerController.uasset b/Content/G2I_Game/Core/Controllers/BP_PlayerController.uasset index c47fd986..15aec29a 100644 Binary files a/Content/G2I_Game/Core/Controllers/BP_PlayerController.uasset and b/Content/G2I_Game/Core/Controllers/BP_PlayerController.uasset differ diff --git a/Content/G2I_Game/Core/Input/Actions/IA_Load.uasset b/Content/G2I_Game/Core/Input/Actions/IA_Load.uasset new file mode 100644 index 00000000..36b850e2 Binary files /dev/null and b/Content/G2I_Game/Core/Input/Actions/IA_Load.uasset differ diff --git a/Content/G2I_Game/Core/Input/Actions/IA_Save.uasset b/Content/G2I_Game/Core/Input/Actions/IA_Save.uasset new file mode 100644 index 00000000..e80276d8 Binary files /dev/null and b/Content/G2I_Game/Core/Input/Actions/IA_Save.uasset differ diff --git a/Content/G2I_Game/Core/Input/IMC_Characters.uasset b/Content/G2I_Game/Core/Input/IMC_Characters.uasset index 77164e6e..4812d9ce 100644 Binary files a/Content/G2I_Game/Core/Input/IMC_Characters.uasset and b/Content/G2I_Game/Core/Input/IMC_Characters.uasset differ diff --git a/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/4/76/6L66UZVID6WZS0H9AKAK77.uasset b/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/4/76/6L66UZVID6WZS0H9AKAK77.uasset new file mode 100644 index 00000000..ba57eb4b Binary files /dev/null and b/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/4/76/6L66UZVID6WZS0H9AKAK77.uasset differ diff --git a/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/8/WQ/XBYSC668Y63NHQOBB7E3VE.uasset b/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/8/WQ/XBYSC668Y63NHQOBB7E3VE.uasset new file mode 100644 index 00000000..02b65860 Binary files /dev/null and b/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/8/WQ/XBYSC668Y63NHQOBB7E3VE.uasset differ diff --git a/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/9/L7/JL34ZRUO6G1TEMXPIJ4LT3.uasset b/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/9/L7/JL34ZRUO6G1TEMXPIJ4LT3.uasset index d0f10a0a..2b7df57b 100644 Binary files a/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/9/L7/JL34ZRUO6G1TEMXPIJ4LT3.uasset and b/Content/__ExternalActors__/G2I_Game/Maps/TestLevel/9/L7/JL34ZRUO6G1TEMXPIJ4LT3.uasset differ diff --git a/Source/G2I/G2I.Build.cs b/Source/G2I/G2I.Build.cs index 0d1cfcf6..ecf45610 100644 --- a/Source/G2I/G2I.Build.cs +++ b/Source/G2I/G2I.Build.cs @@ -20,7 +20,7 @@ public G2I(ReadOnlyTargetRules Target) : base(Target) "UMG", "Slate", "AIModule", - "GameplayTags", + "GameplayTags", "CinematicCamera", "NavigationSystem", "SlateCore" @@ -44,6 +44,7 @@ public G2I(ReadOnlyTargetRules Target) : base(Target) "G2I/Public/Interfaces", "G2I/Public/Interfaces/Camera", "G2I/Public/Interfaces/SteamGlove", + "G2I/Public/SaveSystem", "G2I/Public/UI", "G2I/Public/UI/Widgets", "G2I/Public/UI/WidgetComponents", @@ -64,6 +65,7 @@ public G2I(ReadOnlyTargetRules Target) : base(Target) "G2I/Private/Interfaces", "G2I/Private/Interfaces/Camera", "G2I/Private/Interfaces/SteamGlove", + "G2I/Private/SaveSystem", "G2I/Private/UI", "G2I/Private/UI/Widgets", "G2I/Private/UI/WidgetComponents", diff --git a/Source/G2I/Private/Characters/G2ICharacterDaughter.cpp b/Source/G2I/Private/Characters/G2ICharacterDaughter.cpp index ab27a76d..6d2ac7e8 100644 --- a/Source/G2I/Private/Characters/G2ICharacterDaughter.cpp +++ b/Source/G2I/Private/Characters/G2ICharacterDaughter.cpp @@ -2,6 +2,7 @@ #include "G2I.h" #include "G2IFlightComponent.h" #include "Engine/LocalPlayer.h" +#include "Game/G2IPlayerState.h" #include "Components/Camera/G2IThirdPersonCameraComponent.h" #include "Components/G2ICharacterMovementComponent.h" #include "Components/G2IInteractionComponent.h" @@ -9,6 +10,7 @@ #include "Components/Camera/G2ICameraControllerComponent.h" #include "Components/Camera/G2IFixedCamerasComponent.h" #include "Components/G2IInventoryComponent.h" +#include AG2ICharacterDaughter::AG2ICharacterDaughter(const FObjectInitializer& ObjectInitializer) : ACharacter(ObjectInitializer.SetDefaultSubobjectClass( @@ -69,3 +71,42 @@ FUnPossessedDelegate& AG2ICharacterDaughter::GetUnPossessedDelegate() { return OnUnPossessedDelegate; } + +void AG2ICharacterDaughter::SaveData_Implementation(UG2IGameplaySaveGame* SaveGameRef) +{ + if (!ensure(SaveGameRef)) + { + UE_LOG(LogG2I, Warning, TEXT("Got null SaveGameRef while trying to save %s's data."), *GetName()); + return; + } + + if (IsPlayerControlled()) + { + SaveGameRef->PlayersSaveData.CurrentCharacter = GetClass(); + } + + SaveGameRef->PlayersSaveData.CharactersTransform.Add(GetClass(), GetTransform()); +} + +void AG2ICharacterDaughter::LoadData_Implementation(const UG2IGameplaySaveGame* SaveGameRef) +{ + if (!ensure(SaveGameRef)) + { + UE_LOG(LogG2I, Warning, TEXT("Got null SaveGameRef while trying to save %s's data."), *GetName()); + return; + } + + const TSubclassOf CurrentCharacterClass = SaveGameRef->PlayersSaveData.CurrentCharacter; + if (IsPlayerControlled() && !IsA(CurrentCharacterClass)) + { + if (auto* G2IPlayerState = Cast(GetPlayerState())) + { + G2IPlayerState->SetCharacterByClass(CurrentCharacterClass); + } + } + + if (const FTransform* KeyTransform = SaveGameRef->PlayersSaveData.CharactersTransform.Find(GetClass())) + { + SetActorTransform(*KeyTransform); + } +} diff --git a/Source/G2I/Private/Characters/G2ICharacterEngineer.cpp b/Source/G2I/Private/Characters/G2ICharacterEngineer.cpp index 426073b8..edcbce36 100644 --- a/Source/G2I/Private/Characters/G2ICharacterEngineer.cpp +++ b/Source/G2I/Private/Characters/G2ICharacterEngineer.cpp @@ -7,6 +7,7 @@ #include "Components/SteamGlove/G2ISteamGloveComponent.h" #include "Components/Camera/G2ICameraControllerComponent.h" #include "Components/Camera/G2IFixedCamerasComponent.h" +#include "Game/G2IPlayerState.h" #include "GameFramework/Controller.h" #include "Engine/World.h" #include "G2I.h" @@ -59,4 +60,43 @@ FPossessedDelegate& AG2ICharacterEngineer::GetPossessedDelegate() FUnPossessedDelegate& AG2ICharacterEngineer::GetUnPossessedDelegate() { return OnUnPossessedDelegate; -} \ No newline at end of file +} + +void AG2ICharacterEngineer::SaveData_Implementation(UG2IGameplaySaveGame* SaveGameRef) +{ + if (!ensure(SaveGameRef)) + { + UE_LOG(LogG2I, Warning, TEXT("Got null SaveGameRef while trying to save %s's data."), *GetName()); + return; + } + + if (IsPlayerControlled()) + { + SaveGameRef->PlayersSaveData.CurrentCharacter = GetClass(); + } + + SaveGameRef->PlayersSaveData.CharactersTransform.Add(GetClass(), GetTransform()); +} + +void AG2ICharacterEngineer::LoadData_Implementation(const UG2IGameplaySaveGame* SaveGameRef) +{ + if (!ensure(SaveGameRef)) + { + UE_LOG(LogG2I, Warning, TEXT("Got null SaveGameRef while trying to save %s's data."), *GetName()); + return; + } + + const TSubclassOf CurrentCharacterClass = SaveGameRef->PlayersSaveData.CurrentCharacter; + if (IsPlayerControlled() && !IsA(CurrentCharacterClass)) + { + if (auto* G2IPlayerState = Cast(GetPlayerState())) + { + G2IPlayerState->SetCharacterByClass(CurrentCharacterClass); + } + } + + if (const FTransform* KeyTransform = SaveGameRef->PlayersSaveData.CharactersTransform.Find(GetClass())) + { + SetActorTransform(*KeyTransform); + } +} diff --git a/Source/G2I/Private/Controls/G2IPlayerController.cpp b/Source/G2I/Private/Controls/G2IPlayerController.cpp index c76b3824..84bb9058 100644 --- a/Source/G2I/Private/Controls/G2IPlayerController.cpp +++ b/Source/G2I/Private/Controls/G2IPlayerController.cpp @@ -19,6 +19,7 @@ #include "G2ISteamShotInputInterface.h" #include "G2IUIManager.h" #include "G2IWidgetNames.h" +#include "G2ISavingGameplayManager.h" #include "Kismet/KismetSystemLibrary.h" void AG2IPlayerController::SetupInputComponent() @@ -92,6 +93,8 @@ void AG2IPlayerController::SetupInputComponent() &ThisClass::SwitchCameraBehavior); // TODO: Add Pause Action after adding all UI systems //EnhancedInputComponent->BindAction(DebugPauseAction, ETriggerEvent::Started,this, &ThisClass::CallPause); + EnhancedInputComponent->BindAction(SaveAction, ETriggerEvent::Triggered, this, &ThisClass::SaveGameplay); + EnhancedInputComponent->BindAction(LoadAction, ETriggerEvent::Triggered, this, &ThisClass::LoadGameplay); #endif } else @@ -435,27 +438,18 @@ void AG2IPlayerController::Fly(int Direction) { IG2IFlightInterface::Execute_Fly(FlightComponent, MovementComponent, Direction); } - else - { - UE_LOG(LogG2I, Log, TEXT("Pawn doesn't have component with fly interface in %s"), *GetName()); - UE_LOG(LogG2I, Log, TEXT("Pawn doesn't have component with movement interface in %s"), *GetName()); - } } void AG2IPlayerController::Jump(const FInputActionValue& Value) { - if (!FlightComponent) - { - UE_LOG(LogG2I, Log, TEXT("Pawn doesn't have component with fly interface in %s"), *GetName()); - } - else + if (FlightComponent) { return; } if (!ensure(MovementComponent)) { - UE_LOG(LogG2I, Warning, TEXT("Pawn doesn't have component with movement interface in %s"), *GetName()); + UE_LOG(LogG2I, Warning, TEXT("Pawn doesn't have movement component in %s"), *GetName()); return; } @@ -636,4 +630,40 @@ void AG2IPlayerController::GlovePunchActivation(const FInputActionInstance& Inst { IG2IGlovePunchInterface::Execute_GlovePunchActivation(GlovePunchComponent); } -} \ No newline at end of file +} + +#if WITH_EDITOR +void AG2IPlayerController::SaveGameplay(const FInputActionValue& Value) +{ + if (auto* GameInstance = GetGameInstance()) + { + + if (UG2ISavingGameplayManager* SavingGameplayManager = GameInstance->GetSubsystem()) + { + SavingGameplayManager->SaveAllDataAndGameplay(true); + } + else + { + UE_LOG(LogG2I, Warning, TEXT("Couldn't get SavingGameplayManager subsystem from GameInstance in %s."), *GetName()); + return; + } + } +} + +void AG2IPlayerController::LoadGameplay(const FInputActionValue& Value) +{ + if (auto* GameInstance = GetGameInstance()) + { + if (UG2ISavingGameplayManager* SavingGameplayManager = GameInstance->GetSubsystem()) + { + SavingGameplayManager->LoadGameplay(false); + SavingGameplayManager->LoadAllData(); + } + else + { + UE_LOG(LogG2I, Warning, TEXT("Couldn't get SavingGameplayManager subsystem from GameInstance in %s."), *GetName()); + return; + } + } +} +#endif diff --git a/Source/G2I/Private/Game/G2IGameInstance.cpp b/Source/G2I/Private/Game/G2IGameInstance.cpp index e1b29542..1d9865f3 100644 --- a/Source/G2I/Private/Game/G2IGameInstance.cpp +++ b/Source/G2I/Private/Game/G2IGameInstance.cpp @@ -1,4 +1,5 @@ #include "G2IGameInstance.h" +#include "G2I.h" UG2IWidgetsCatalog* UG2IGameInstance::GetWidgetsCatalog() { diff --git a/Source/G2I/Private/Game/G2IPlayerState.cpp b/Source/G2I/Private/Game/G2IPlayerState.cpp index d9552590..622cb733 100644 --- a/Source/G2I/Private/Game/G2IPlayerState.cpp +++ b/Source/G2I/Private/Game/G2IPlayerState.cpp @@ -6,6 +6,7 @@ #include "Engine/World.h" #include "GameFramework/Pawn.h" #include "Kismet/GameplayStatics.h" +#include "GameFramework/Character.h" void AG2IPlayerState::BeginPlay() { @@ -225,7 +226,7 @@ void AG2IPlayerState::SelectNextCharacter() PlayableCharactersRowNames.Num(); if (NewCharacterIndex == NumberCurrentCharacter) { - UE_LOG(LogG2I, Log, TEXT("Character doesn't switched")); + UE_LOG(LogG2I, Log, TEXT("Couldn't switch to the next character.")); return; } @@ -257,6 +258,70 @@ void AG2IPlayerState::SelectNextCharacter() } } +void AG2IPlayerState::SetCharacterByClass(const TSubclassOf TargetClass) +{ + if (!ensure(TargetClass)) + { + UE_LOG(LogG2I, Warning, TEXT("An attempt to set character with null class.")); + return; + } + + if (!ensure(!PlayableCharactersRowNames.IsEmpty())) + { + UE_LOG(LogG2I, Warning, TEXT("An attempt to set character when array of playable characters is empty.")); + return; + } + + for (int32 OffsetRowName = 1; OffsetRowName <= PlayableCharactersRowNames.Num(); ++OffsetRowName) + { + const int32 NewCharacterIndex = (NumberCurrentCharacter + OffsetRowName) % PlayableCharactersRowNames.Num(); + + if (NewCharacterIndex == NumberCurrentCharacter) + { + UE_LOG(LogG2I, Log, TEXT("Couldn't switch to %s character."), *TargetClass->GetName()); + return; + } + + const FG2IItemCharacter* Row = PlayableCharactersDataTable->FindRow( + PlayableCharactersRowNames[NewCharacterIndex], TEXT("Playable Character Context")); + if (!Row) + { + UE_LOG(LogG2I, Error, TEXT("Array of row names isn't synced with %s in %s."), + *PlayableCharactersDataTable.GetName(), *GetName()); + continue; + } + + if (Row->CharacterClass == TargetClass) + { + const int32 OldCharacterNumber = NumberCurrentCharacter; + NumberCurrentCharacter = NewCharacterIndex; + + if (!SetupControllerForPawn(OldCharacterNumber)) + { + UE_LOG(LogG2I, Warning, TEXT("Character %i couldn't switch to AI controller."), + OldCharacterNumber); + NumberCurrentCharacter = OldCharacterNumber; + continue; + } + + if (SetupControllerForPawn(NumberCurrentCharacter)) + { + OnNewControllerPossessDelegate.Broadcast(GetPawn(OldCharacterNumber)); + OnNewControllerPossessDelegate.Broadcast(GetPawn(NumberCurrentCharacter)); + break; + } + else + { + UE_LOG(LogG2I, Warning, TEXT("Character %i couldn't switch to player controller."), + NumberCurrentCharacter); + NumberCurrentCharacter = OldCharacterNumber; + SetupControllerForPawn(NumberCurrentCharacter); + continue; + } + } + } +} + APawn *AG2IPlayerState::GetPawn(const uint32 PawnNumber) { if (const FG2IItemCharacter *Row = PlayableCharactersDataTable->FindRow( diff --git a/Source/G2I/Private/SaveSystem/G2ICheckpoint.cpp b/Source/G2I/Private/SaveSystem/G2ICheckpoint.cpp new file mode 100644 index 00000000..54799711 --- /dev/null +++ b/Source/G2I/Private/SaveSystem/G2ICheckpoint.cpp @@ -0,0 +1,106 @@ +#include "G2ICheckpoint.h" +#include "G2I.h" +#include "Kismet/GameplayStatics.h" + +AG2ICheckpoint::AG2ICheckpoint() +{ +#if WITH_EDITOR + SetActorHiddenInGame(false); +#endif +} + +void AG2ICheckpoint::BeginPlay() +{ + Super::BeginPlay(); + + UGameInstance* GameInstance = GetGameInstance(); + if (!GameInstance) + { + UE_LOG(LogG2I, Error, TEXT("GameInstance is NULL in %s."), *GameInstance->GetName(), *GetName()); + return; + } + + if (!ensure(SavingGameplayManager = GameInstance->GetSubsystem())) + { + UE_LOG(LogG2I, Error, TEXT("Couldn't get SavingGameplayManager subsystem from GameInstance in %s."), *GameInstance->GetName(), *GetName()); + return; + } + + SavingGameplayManager->LoadRequestedData(this); + SavingGameplayManager->OnGameplaySavedDelegate.AddUniqueDynamic(this, &ThisClass::OnGameplaySaved); +} + +void AG2ICheckpoint::NotifyActorBeginOverlap(AActor* OtherActor) +{ + Super::NotifyActorBeginOverlap(OtherActor); + + const ACharacter* OtherCharacter = Cast(OtherActor); + if (!OtherCharacter) + { + return; + } + if (!OtherCharacter->IsPlayerControlled()) + { + UE_LOG(LogG2I, Log, TEXT("AI controlled character overlapped %s. Saving isn't triggered."), *GetName()); + return; + } + + bActivated = true; + + if (!SavingGameplayManager) + { + UE_LOG(LogG2I, Error, TEXT("SavingGameplayManager is null in %s."), *GetName()); + return; + } + + SavingGameplayManager->SaveAllDataAndGameplay(true); +} + +void AG2ICheckpoint::OnGameplaySaved(bool bSuccess) +{ + if (bActivated) + { + Destroy(); + } +} + +void AG2ICheckpoint::SaveData_Implementation(UG2IGameplaySaveGame* SaveGameRef) +{ + if (!ensure(SaveGameRef)) + { + UE_LOG(LogG2I, Warning, TEXT("Got null SaveGameRef while trying to save %s's data."), *GetName()); + return; + } + + const UWorld* World = GetWorld(); + if (!World) + { + UE_LOG(LogG2I, Error, TEXT("World is null in %s."), *GetName()); + return; + } + + const FCheckpointSaveData CheckpointSavedData = FCheckpointSaveData(UGameplayStatics::GetCurrentLevelName(World), GetActorLocation()); + SaveGameRef->CheckpointsSaveData.Add(CheckpointSavedData, bActivated); +} + +void AG2ICheckpoint::LoadData_Implementation(const UG2IGameplaySaveGame* SaveGameRef) +{ + if (!ensure(SaveGameRef)) + { + UE_LOG(LogG2I, Warning, TEXT("Got null SaveGameRef while trying to load %s's data."), *GetName()); + return; + } + + const UWorld* World = GetWorld(); + if (!World) + { + UE_LOG(LogG2I, Error, TEXT("World is null in %s."), *GetName()); + return; + } + + const FCheckpointSaveData CheckpointData = FCheckpointSaveData(UGameplayStatics::GetCurrentLevelName(World), GetActorLocation()); + if (const bool* Key = SaveGameRef->CheckpointsSaveData.Find(CheckpointData)) + { + if (*Key) { Destroy(); } + } +} diff --git a/Source/G2I/Private/SaveSystem/G2ISavingGameplayManager.cpp b/Source/G2I/Private/SaveSystem/G2ISavingGameplayManager.cpp new file mode 100644 index 00000000..3a841214 --- /dev/null +++ b/Source/G2I/Private/SaveSystem/G2ISavingGameplayManager.cpp @@ -0,0 +1,237 @@ +#include "G2ISavingGameplayManager.h" +#include "G2ISavableInterface.h" +#include "G2I.h" + + +void UG2ISavingGameplayManager::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + OnGameplayAsyncSavedDelegate.BindUObject(this, &UG2ISavingGameplayManager::OnGameplayAsyncSaved); + OnGameplayAsyncLoadedDelegate.BindUObject(this, &UG2ISavingGameplayManager::OnGameplayAsyncLoaded); +} + +void UG2ISavingGameplayManager::CreateNewGameplaySaveGameObject() +{ + if (UGameplayStatics::DoesSaveGameExist(GameplaySaveSlotName, 0)) + UGameplayStatics::DeleteGameInSlot(GameplaySaveSlotName, 0); + + CreateGameplaySaveGame(); +} + +void UG2ISavingGameplayManager::CreateGameplaySaveGame() +{ + GameplaySaveGame = Cast(UGameplayStatics::CreateSaveGameObject(UG2IGameplaySaveGame::StaticClass())); + if (!ensure(GameplaySaveGame)) + UE_LOG(LogG2I, Error, TEXT("Couldn't create GameplaySaveGame object in %s."), *GetName()); +} + +void UG2ISavingGameplayManager::OnGameplayAsyncSaved(const FString& SlotName, const int32 UserIndex, bool bSuccess) +{ + OnGameplaySavedDelegate.Broadcast(bSuccess); + if (bSuccess) + { + UE_LOG(LogG2I, Log, TEXT("Gameplay saved successfully in the slot %s."), *GameplaySaveSlotName); + } + else + UE_LOG(LogG2I, Error, TEXT("Gameplay saving in the slot %s failed."), *GameplaySaveSlotName); +} + +void UG2ISavingGameplayManager::OnGameplayAsyncLoaded(const FString& SlotName, const int32 UserIndex, USaveGame* LoadedGameData) +{ + if (!LoadedGameData) + { + // Load failed + UE_LOG(LogG2I, Error, TEXT("Gameplay load from the slot %s failed."), *GameplaySaveSlotName); + OnGameplayLoadedDelegate.Broadcast(false); + return; + } + + if (GameplaySaveGame = Cast(LoadedGameData)) + { + UE_LOG(LogG2I, Log, TEXT("Gameplay loaded successfully from the slot %s."), *GameplaySaveSlotName); + OnGameplayLoadedDelegate.Broadcast(true); + } + else + { + // SaveGame file exists, but not valid + UE_LOG(LogG2I, Error, TEXT("Gameplay loaded from the slot %s but is invalid. Operation failed."), *GameplaySaveSlotName); + OnGameplayLoadedDelegate.Broadcast(false); + } +} + +void UG2ISavingGameplayManager::SaveGameplay(bool bAsync) +{ + if (!GameplaySaveGame) + CreateGameplaySaveGame(); + + OnGameplaySaveStartedDelegate.Broadcast(); + + if (bAsync) + { + // Asynchronous + UGameplayStatics::AsyncSaveGameToSlot(GameplaySaveGame, GameplaySaveSlotName, 0, OnGameplayAsyncSavedDelegate); + } + else + { + // Synchronous + if (UGameplayStatics::SaveGameToSlot(GameplaySaveGame, GameplaySaveSlotName, 0)) + { + // Successfully saved + OnGameplaySavedDelegate.Broadcast(true); + UE_LOG(LogG2I, Log, TEXT("Gameplay saved successfully in the slot %s."), *GameplaySaveSlotName); + } + else + { + // Failed to save + OnGameplaySavedDelegate.Broadcast(false); + UE_LOG(LogG2I, Error, TEXT("Gameplay saving in the slot %s failed."), *GameplaySaveSlotName); + } + } +} + +void UG2ISavingGameplayManager::SetGameplaySaveSlotName(const FString& NewSlotName) +{ + GameplaySaveSlotName = NewSlotName; +} + +const FString UG2ISavingGameplayManager::GetGameplaySaveSlotName() +{ + return GameplaySaveSlotName; +} + +void UG2ISavingGameplayManager::LoadGameplay(bool bAsync) +{ + OnGameplayLoadStartedDelegate.Broadcast(); + if (bAsync) + { + // Asynchronous + UGameplayStatics::AsyncLoadGameFromSlot(GameplaySaveSlotName, 0, OnGameplayAsyncLoadedDelegate); + } + else + { + // Synchronous + if (USaveGame* LoadedGame = UGameplayStatics::LoadGameFromSlot(GameplaySaveSlotName, 0)) + { + if (GameplaySaveGame = Cast(LoadedGame)) + { + // Successfully loaded + OnGameplayLoadedDelegate.Broadcast(true); + UE_LOG(LogG2I, Log, TEXT("Gameplay loaded successfully from the slot %s."), *GameplaySaveSlotName); + } + else + { + // Loaded SaveGame object, but it's invalid + OnGameplayLoadedDelegate.Broadcast(false); + UE_LOG(LogG2I, Error, TEXT("Gameplay loaded from the slot %s but is invalid. Operation failed."), *GameplaySaveSlotName); + } + } + else + { + // Failed to load + OnGameplayLoadedDelegate.Broadcast(false); + UE_LOG(LogG2I, Error, TEXT("Gameplay load from the slot %s failed."), *GameplaySaveSlotName); + } + } +} + +void UG2ISavingGameplayManager::SaveAllData() +{ + if (!ensure(GameplaySaveGame)) + { + UE_LOG(LogG2I, Error, TEXT("GameplaySaveGame in null in %s."), *GetName()); + return; + } + + // Getting all actors with interface 'Savable' + TArray FoundSavableActors; + GetAllActorsWithSavableIntetrface(FoundSavableActors); + + // Iterating on them & saving their data + for (auto* Actor : FoundSavableActors) + { + IG2ISavableInterface::Execute_SaveData(Actor, GameplaySaveGame); + } + + UE_LOG(LogG2I, Log, TEXT("Gameplay data saved & stored in GameplaySaveGame object.")); +} + +void UG2ISavingGameplayManager::LoadAllData() +{ + if (!ensure(GameplaySaveGame)) + { + UE_LOG(LogG2I, Error, TEXT("GameplaySaveGame in null in %s."), *GetName()); + return; + } + + // Getting all actors with interface 'Savable' + TArray FoundSavableActors; + GetAllActorsWithSavableIntetrface(FoundSavableActors); + + // Iterating on them & loading their data + for (auto* Actor : FoundSavableActors) + { + IG2ISavableInterface::Execute_LoadData(Actor, GameplaySaveGame); + } + + UE_LOG(LogG2I, Log, TEXT("Gameplay data loaded from the GameplaySaveGame object.")); +} + +void UG2ISavingGameplayManager::GetAllActorsWithSavableIntetrface(TArray& FoundActors) +{ + UWorld* World = GetWorld(); + if (!World) + { + UE_LOG(LogG2I, Error, TEXT("World doesn't exist in %s."), *GetName()); + return; + } + + UGameplayStatics::UGameplayStatics::GetAllActorsWithInterface( + this, + UG2ISavableInterface::StaticClass(), + FoundActors); +} + +void UG2ISavingGameplayManager::SaveAllDataAndGameplay(bool bAsync) +{ + SaveAllData(); + SaveGameplay(bAsync); +} + +void UG2ISavingGameplayManager::SaveRequestedData(UObject* Requester) +{ + if (!ensure(GameplaySaveGame)) + { + UE_LOG(LogG2I, Error, TEXT("GameplaySaveGame in null. You'll have to load it first or create new.")); + return; + } + + if (Requester) + { + if (Requester->Implements()) + { + IG2ISavableInterface::Execute_SaveData(Requester, GameplaySaveGame); + } + else + UE_LOG(LogG2I, Warning, TEXT("%s doesn't implement Savable interface. It's data will be lost."), *Requester->GetName()); + } +} + +void UG2ISavingGameplayManager::LoadRequestedData(UObject* Requester) +{ + if (!ensure(GameplaySaveGame)) + { + UE_LOG(LogG2I, Error, TEXT("GameplaySaveGame in null. You'll have to load it first or create new.")); + return; + } + + if (Requester) + { + if (Requester->Implements()) + { + IG2ISavableInterface::Execute_LoadData(Requester, GameplaySaveGame); + } + else + UE_LOG(LogG2I, Warning, TEXT("%s doesn't implement Savable interface. It's data won't be loaded."), *Requester->GetName()); + } +} diff --git a/Source/G2I/Public/Characters/G2ICharacterDaughter.h b/Source/G2I/Public/Characters/G2ICharacterDaughter.h index f157d459..f872fd9e 100644 --- a/Source/G2I/Public/Characters/G2ICharacterDaughter.h +++ b/Source/G2I/Public/Characters/G2ICharacterDaughter.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "G2ICharacterInterface.h" #include "GameFramework/Character.h" +#include "G2ISavableInterface.h" #include "G2ICharacterDaughter.generated.h" class UG2IFlightComponent; @@ -21,7 +22,7 @@ class UG2IInventoryComponent; * Implements a controllable orbiting camera */ UCLASS(Blueprintable) -class AG2ICharacterDaughter : public ACharacter, public IG2ICharacterInterface +class AG2ICharacterDaughter : public ACharacter, public IG2ICharacterInterface, public IG2ISavableInterface { GENERATED_BODY() @@ -61,6 +62,10 @@ class AG2ICharacterDaughter : public ACharacter, public IG2ICharacterInterface explicit AG2ICharacterDaughter(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + virtual void SaveData_Implementation(UG2IGameplaySaveGame* SaveGameRef) override; + + virtual void LoadData_Implementation(const UG2IGameplaySaveGame* SaveGameRef) override; + virtual void PossessedBy(AController* NewController) override; virtual void UnPossessed() override; diff --git a/Source/G2I/Public/Characters/G2ICharacterEngineer.h b/Source/G2I/Public/Characters/G2ICharacterEngineer.h index 70c10701..0a5c2eb2 100644 --- a/Source/G2I/Public/Characters/G2ICharacterEngineer.h +++ b/Source/G2I/Public/Characters/G2ICharacterEngineer.h @@ -3,6 +3,7 @@ #include "CoreMinimal.h" #include "G2ICharacterInterface.h" #include "GameFramework/Character.h" +#include "G2ISavableInterface.h" #include "Components/G2IValveInteractionComponent.h" #include "Components/G2IHoleInteractionComponent.h" #include "G2ICharacterEngineer.generated.h" @@ -20,7 +21,7 @@ class UG2IInventoryComponent; * Implements a controllable orbiting camera */ UCLASS(Blueprintable) -class G2I_API AG2ICharacterEngineer : public ACharacter, public IG2ICharacterInterface +class G2I_API AG2ICharacterEngineer : public ACharacter, public IG2ICharacterInterface, public IG2ISavableInterface { GENERATED_BODY() @@ -67,6 +68,10 @@ class G2I_API AG2ICharacterEngineer : public ACharacter, public IG2ICharacterInt explicit AG2ICharacterEngineer(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get()); + virtual void SaveData_Implementation(UG2IGameplaySaveGame* SaveGameRef) override; + + virtual void LoadData_Implementation(const UG2IGameplaySaveGame* SaveGameRef) override; + virtual void PossessedBy(AController* NewController) override; virtual void UnPossessed() override; diff --git a/Source/G2I/Public/Controls/G2IPlayerController.h b/Source/G2I/Public/Controls/G2IPlayerController.h index 42f35c83..e574b895 100644 --- a/Source/G2I/Public/Controls/G2IPlayerController.h +++ b/Source/G2I/Public/Controls/G2IPlayerController.h @@ -211,4 +211,20 @@ class G2I_API AG2IPlayerController : public APlayerController TObjectPtr GlovePunchComponent; void GlovePunchActivation(const FInputActionInstance& Instance); + + /** Debug keys for testing saving system */ + +#if WITH_EDITORONLY_DATA + UPROPERTY(EditAnywhere, Category = "Input|Debug") + TObjectPtr SaveAction; + + UPROPERTY(EditAnywhere, Category = "Input|Debug") + TObjectPtr LoadAction; +#endif + +#if WITH_EDITOR + void SaveGameplay(const FInputActionValue& Value); + + void LoadGameplay(const FInputActionValue& Value); +#endif }; diff --git a/Source/G2I/Public/Game/G2IGameInstance.h b/Source/G2I/Public/Game/G2IGameInstance.h index db3c1aaa..2661c391 100644 --- a/Source/G2I/Public/Game/G2IGameInstance.h +++ b/Source/G2I/Public/Game/G2IGameInstance.h @@ -10,6 +10,7 @@ class UG2IWidgetsCatalog; /** * Base game instance for G2I game + * saves and loads gameplay & settings data */ UCLASS() class G2I_API UG2IGameInstance : public UGameInstance @@ -28,11 +29,10 @@ class G2I_API UG2IGameInstance : public UGameInstance TObjectPtr WidgetComponentsParameters; public: - - UG2IWidgetsCatalog *GetWidgetsCatalog(); - UG2IStringTablesCatalog *GetStringTablesCatalog(); + UG2IWidgetsCatalog* GetWidgetsCatalog(); - UG2IWidgetComponentParameters *GetWidgetComponentParameters(); - + UG2IStringTablesCatalog* GetStringTablesCatalog(); + + UG2IWidgetComponentParameters* GetWidgetComponentParameters(); }; diff --git a/Source/G2I/Public/Game/G2IPlayerState.h b/Source/G2I/Public/Game/G2IPlayerState.h index 53b3f3fa..bf91cfbf 100644 --- a/Source/G2I/Public/Game/G2IPlayerState.h +++ b/Source/G2I/Public/Game/G2IPlayerState.h @@ -37,6 +37,8 @@ class G2I_API AG2IPlayerState : public APlayerState void SelectNextCharacter(); + void SetCharacterByClass(const TSubclassOf TargetClass); + protected: APawn *GetPawn(const uint32 PawnNumber); @@ -59,4 +61,5 @@ class G2I_API AG2IPlayerState : public APlayerState AActor *SpawnActor(const TSubclassOf ActorClass) const; + void SwitchToCharacter(int32 NewCharacterIndex); }; diff --git a/Source/G2I/Public/SaveSystem/G2ICheckpoint.h b/Source/G2I/Public/SaveSystem/G2ICheckpoint.h new file mode 100644 index 00000000..886cbf86 --- /dev/null +++ b/Source/G2I/Public/SaveSystem/G2ICheckpoint.h @@ -0,0 +1,39 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/TriggerBox.h" +#include "G2ISavableInterface.h" +#include "G2ISavingGameplayManager.h" +#include "G2ICheckpoint.generated.h" + +/** + * Trigger box that, when triggered by the player, saves gameplay & deletes itself + */ +UCLASS() +class G2I_API AG2ICheckpoint : public ATriggerBox, public IG2ISavableInterface +{ + GENERATED_BODY() + +protected: + UPROPERTY(BlueprintReadOnly) + TObjectPtr SavingGameplayManager; + + UPROPERTY() + bool bActivated = false; + +public: + AG2ICheckpoint(); + + UFUNCTION() + virtual void NotifyActorBeginOverlap(AActor* OtherActor) override; + + UFUNCTION() + void OnGameplaySaved(bool bSuccess); + + virtual void SaveData_Implementation(UG2IGameplaySaveGame* SaveGameRef) override; + + virtual void LoadData_Implementation(const UG2IGameplaySaveGame* SaveGameRef) override; + +protected: + virtual void BeginPlay() override; +}; diff --git a/Source/G2I/Public/SaveSystem/G2IGameplaySaveGame.h b/Source/G2I/Public/SaveSystem/G2IGameplaySaveGame.h new file mode 100644 index 00000000..880a0236 --- /dev/null +++ b/Source/G2I/Public/SaveSystem/G2IGameplaySaveGame.h @@ -0,0 +1,82 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/SaveGame.h" +#include "GameFramework/Character.h" +#include "Misc/Crc.h" +#include "G2IGameplaySaveGame.generated.h" + +USTRUCT(BlueprintType) +struct FPlayerSaveData +{ + GENERATED_BODY() + + UPROPERTY(SaveGame, BlueprintReadWrite) + TMap, FTransform> CharactersTransform; + + UPROPERTY(SaveGame, BlueprintReadWrite) + TSubclassOf CurrentCharacter; +}; + +USTRUCT(BlueprintType) +struct FCheckpointSaveData +{ + GENERATED_BODY() + + UPROPERTY(SaveGame, BlueprintReadWrite) + FString LevelName; + + UPROPERTY(SaveGame, BlueprintReadWrite) + FVector Location; + + // Extending the struct for using it in TMap as a key + + FCheckpointSaveData(FString NewLevelName, FVector NewLocation) + : LevelName(NewLevelName), Location(NewLocation) { + } + + FCheckpointSaveData() + : LevelName(TEXT("")), Location(FVector::ZeroVector) { + } + + FCheckpointSaveData(const FCheckpointSaveData& Other) + : LevelName(Other.LevelName), Location(Other.Location) { + } + + bool operator==(const FCheckpointSaveData& Other) const + { + return Equals(Other); + } + + bool operator!=(const FCheckpointSaveData& Other) const + { + return !Equals(Other); + } + + bool Equals(const FCheckpointSaveData& Other) const + { + return LevelName.Equals(Other.LevelName) && Location.Equals(Other.Location); + } + + friend uint32 GetTypeHash(const FCheckpointSaveData& Thing) + { + uint32 Hash = FCrc::MemCrc32(&Thing, sizeof(FCheckpointSaveData)); + return Hash; + } +}; + +/** + * SaveGame for gameplay data. + */ +UCLASS() +class G2I_API UG2IGameplaySaveGame : public USaveGame +{ + GENERATED_BODY() + +public: + UPROPERTY(SaveGame, BlueprintReadWrite, Category = "Save Gameplay Data|Player") + FPlayerSaveData PlayersSaveData; + + UPROPERTY(SaveGame, BlueprintReadWrite, Category = "Save Gameplay Data|Checkpoints") + TMap CheckpointsSaveData; +}; diff --git a/Source/G2I/Public/SaveSystem/G2ISavableInterface.h b/Source/G2I/Public/SaveSystem/G2ISavableInterface.h new file mode 100644 index 00000000..dd5bbd5d --- /dev/null +++ b/Source/G2I/Public/SaveSystem/G2ISavableInterface.h @@ -0,0 +1,30 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Interface.h" +#include "G2IGameplaySaveGame.h" +#include "G2ISavableInterface.generated.h" + +// This class does not need to be modified. +UINTERFACE(MinimalAPI) +class UG2ISavableInterface : public UInterface +{ + GENERATED_BODY() +}; + +/** + * Interface that every object that needs to be saved have to implement + */ +class G2I_API IG2ISavableInterface +{ + GENERATED_BODY() + +public: + // Adds/updates needed data to the save game object + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Savable") + void SaveData(UG2IGameplaySaveGame* SaveGameRef); + + // Loads needed data from the save game object + UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Savable") + void LoadData(const UG2IGameplaySaveGame* SaveGameRef); +}; diff --git a/Source/G2I/Public/SaveSystem/G2ISavingGameplayManager.h b/Source/G2I/Public/SaveSystem/G2ISavingGameplayManager.h new file mode 100644 index 00000000..9318570b --- /dev/null +++ b/Source/G2I/Public/SaveSystem/G2ISavingGameplayManager.h @@ -0,0 +1,108 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/GameInstanceSubsystem.h" +#include "Delegates/Delegate.h" +#include "Kismet/GameplayStatics.h" +#include "G2IGameplaySaveGame.h" +#include "G2ISavingGameplayManager.generated.h" + +// Delegates + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameplaySavedDelegate, bool, bSuccess); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnGameplaySaveStartedDelegate); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameplayLoadedDelegate, bool, bSuccess); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnGameplayLoadStartedDelegate); + +/** + * Responsible for saving, loading & storing gameplay data + */ +UCLASS() +class G2I_API UG2ISavingGameplayManager : public UGameInstanceSubsystem +{ + GENERATED_BODY() + +public: + + UPROPERTY(BlueprintAssignable) + FOnGameplaySavedDelegate OnGameplaySavedDelegate; + + UPROPERTY(BlueprintAssignable) + FOnGameplaySaveStartedDelegate OnGameplaySaveStartedDelegate; + + UPROPERTY(BlueprintAssignable) + FOnGameplayLoadedDelegate OnGameplayLoadedDelegate; + + UPROPERTY(BlueprintAssignable) + FOnGameplayLoadStartedDelegate OnGameplayLoadStartedDelegate; + +protected: + + UPROPERTY() + FString GameplaySaveSlotName = TEXT("GameplaySaveSlot0"); + + // C++-only delegate called from AsyncSaveGameToSlot + FAsyncSaveGameToSlotDelegate OnGameplayAsyncSavedDelegate; + + // C++-only delegate called from AsyncLoadGameToSlot + FAsyncLoadGameFromSlotDelegate OnGameplayAsyncLoadedDelegate; + +private: + // SaveGame with gameplay info + UPROPERTY() + TObjectPtr GameplaySaveGame; + +public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + + UFUNCTION(BlueprintCallable) + void CreateNewGameplaySaveGameObject(); + + UFUNCTION(BlueprintCallable) + void SetGameplaySaveSlotName(const FString& NewSlotName); + + UFUNCTION(BlueprintCallable) + const FString GetGameplaySaveSlotName(); + + // Saves SaveGame to slot + UFUNCTION(BlueprintCallable) + void SaveGameplay(bool bAsync); + + // Loads SaveGame from slot + UFUNCTION(BlueprintCallable) + void LoadGameplay(bool bAsync); + + // Saves data of all ACTORS on the level with Savable interface in the SaveGame (not in the slot!) + UFUNCTION(BlueprintCallable) + void SaveAllData(); + + // Loads data of all ACTORS on the level with Savable interface from the SaveGame + UFUNCTION(BlueprintCallable) + void LoadAllData(); + + // Saves data of all ACTORS on the level with Savable interface in the SaveGame & saves SaveGame to slot + UFUNCTION(BlueprintCallable) + void SaveAllDataAndGameplay(bool bAsync); + + // Saves Requester's data in the SaveGame object (not in the slot!) + // This will be called when Requester needs it (in it's OnDestroyed, etc.) + UFUNCTION(BlueprintCallable) + void SaveRequestedData(UObject* Requester); + + // Loads Requester's data from the SaveGame object (not from the slot!) + // This will be called when Requester needs it (in it's BeginPlay, etc.) + UFUNCTION(BlueprintCallable) + void LoadRequestedData(UObject* Requester); + +protected: + + UFUNCTION(BlueprintCallable) + void CreateGameplaySaveGame(); + + UFUNCTION(BlueprintCallable) + void GetAllActorsWithSavableIntetrface(TArray& FoundActors); + + void OnGameplayAsyncSaved(const FString& SlotName, const int32 UserIndex, bool bSuccess); + + void OnGameplayAsyncLoaded(const FString& SlotName, const int32 UserIndex, USaveGame* LoadedGameData); +}; diff --git a/Source/G2I/Public/SaveSystem/G2ISettingsSaveGame.h b/Source/G2I/Public/SaveSystem/G2ISettingsSaveGame.h new file mode 100644 index 00000000..5cc5a8b1 --- /dev/null +++ b/Source/G2I/Public/SaveSystem/G2ISettingsSaveGame.h @@ -0,0 +1,15 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/SaveGame.h" +#include "G2ISettingsSaveGame.generated.h" + +/** + * SaveGame for settings data. + */ +UCLASS() +class G2I_API UG2ISettingsSaveGame : public USaveGame +{ + GENERATED_BODY() + +};