A second thing I recently implemented that I found interesting was adding multiple levels. Going through the blueprints course and “Marble Run” there is a simple way to do this if you do not need to save progress - just create an array of all your level names and then find which one you are at by looking for the index of that name in the list. Then it is easy to “+1” it and move to next or know you completed the game. One important thing to note is that EVERY actor in your game, including the game mode and game state are completely destroyed and recreated when a new level is loaded. That means you cannot track the current overall “game state” in the game state itself. The only object that is a true singleton and is only created once per game is the “GameInstance”. So you are left with two ways to store state between level loads
- Create a saved game object and save and then reload the file between level loads
- Store this state on the GameInstance itself temporarily to shuffle it between levels
Since I was already saving the game after completing each level, I chose the first option. Note though you have to gain a deep understanding of the start sequence of a level through to begin play to get the loading right so it is a bit more challenging than option 2.
For each level I have a spawning system I learned from Stephen’s game dev course to spawn enemies and items in a random set of locations to make the game a bit more varied and procedural. I have a set of parameters I want to tweak between levels. I divide each unique map into a “level” and some of the levels have multiple “waves” I call “zones”. To coordinate transitioning the state, I have a “manager” class callled “ULevelLauncher”. This handles actually loading the level with the appropriate zone data, including the map name. So I have
ULevelLauncher -> FLevelConfig[] -> FLevelZoneConfig[]
The current level data is then in a FLevelState
struct to keep track of where you are, which is what gets persisted in the saved game.
I keep a TArray of the LevelZoneConfig on the game mode itself and then expose it to blueprints so that I can easily tweak it when I put my level designer hat on:
Here is a snippet of that blueprint variable:
“Player Start Tag” allows me to select from a number of spawn locations as in some of the larger maps I start the player off in a different location. Figuring out how to do that was a whole other thing I found the “Shooter Game” full game example from the marketplace helpful in getting me there. One way to do that is to override the ChoosePlayerStart_Implementation
function in the AGameModeBase (which is interestingly not even declared in the base class but you can override it like it is - something I don’t yet fully understand). The source code of the “Shooter Game” shows an implementation of this and I took that plus the source code of the ChoosePlayerStart
and then had to figure out some start order issues - again looking at the unreal source code to understand it better to be sure the level state was initialized by the time this function was called as the sequencing was not well documented.
Anyway, back to the level loader code. I’ve included my full source code for it and the unit tests I used to test some of it below in case it helps anyone else:
LevelLauncher.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "AIDifficulty.h"
#include "LevelLauncher.generated.h"
USTRUCT(BlueprintType)
struct FLevelZoneConfig
{
GENERATED_BODY()
UPROPERTY(Category = "Level | Enemies", EditDefaultsOnly, BlueprintReadOnly)
EAIDifficulty Difficulty {};
UPROPERTY(Category = "Level | Enemies", EditDefaultsOnly, BlueprintReadOnly)
int32 NumEnemies {};
UPROPERTY(Category = "Level | Items", EditDefaultsOnly, BlueprintReadOnly)
int32 NumHealthItems {};
UPROPERTY(Category = "Level | Items", EditDefaultsOnly, BlueprintReadOnly)
int32 NumAmmoItems {};
UPROPERTY(Category = "Level | Time", EditDefaultsOnly, BlueprintReadOnly)
int32 TimeTargetSeconds {};
UPROPERTY(Category = "Level | Player", EditDefaultsOnly, BlueprintReadOnly)
FName PlayerStartTag {};
UPROPERTY(Category = "Level | Enemies", EditDefaultsOnly, BlueprintReadOnly)
float MaxLineOfSightDetectionDistance {};
UPROPERTY(Category = "Level | Sound", EditDefaultsOnly, BlueprintReadOnly)
USoundBase* IntroMusic {};
};
USTRUCT(BlueprintType)
struct FLevelConfig
{
GENERATED_BODY()
UPROPERTY(Category = "Level", EditDefaultsOnly, BlueprintReadOnly)
FName LevelName {};
UPROPERTY(Category = "Level | Zones", EditDefaultsOnly, BlueprintReadOnly)
TArray<FLevelZoneConfig> Zones;
};
USTRUCT(BlueprintType)
struct FLevelState
{
GENERATED_BODY()
UPROPERTY(VisibleAnywhere, Category = Data, SaveGame)
FName LevelName {};
UPROPERTY(VisibleAnywhere, Category = Data, SaveGame)
int32 LevelIndex {};
UPROPERTY(VisibleAnywhere, Category = Data, SaveGame)
int32 ZoneIndex {};
UPROPERTY(VisibleAnywhere, Category = Data, SaveGame)
bool bMultipleZones {};
FString ToString() const;
FLevelState() = default;
explicit FLevelState(const FLevelConfig& LevelConfig, int32 LevelIndex = 0, int32 ZoneIndex = 0);
void SetLevelConfig(const FLevelConfig& LevelConfig);
};
/**
*
*/
UCLASS(BlueprintType)
class SIMPLESHOOTER_API ULevelLauncher : public UObject
{
GENERATED_BODY()
public:
void Init(UWorld* World, const TArray<FLevelConfig>& Levels, int32 StartLevelIndex = 0, int32 StartZoneIndex = 0);
bool HasNextZone(const FLevelState& CurrentLevelState) const;
bool HasNextLevel(const FLevelState& CurrentLevelState) const;
bool NextZone(const FLevelState& CurrentLevelState, FLevelState* OutNextZoneState=nullptr, FLevelZoneConfig* OutZoneConfig=nullptr) const;
bool LoadZone(const FLevelState& CurrentLevelState) const;
void FirstZone(FLevelState* outFirstZoneState, FLevelZoneConfig* OutZoneConfig = nullptr) const;
bool GetZoneConfig(const FLevelState& LevelState, FLevelZoneConfig& OutZoneConfig) const;
private:
void AssertState() const;
bool GetLevelConfig(const FLevelState& LevelState, const FLevelConfig** OutLevelConfig = nullptr) const;
bool GetZoneConfig(const FLevelState& LevelState, const FLevelConfig& LevelConfig, const FLevelZoneConfig** OutLevelZoneConfig = nullptr) const;
private:
UPROPERTY()
UWorld* World;
UPROPERTY(Category = "Level", EditDefaultsOnly)
int32 StartLevelIndex {};
UPROPERTY(Category = "Level", EditDefaultsOnly)
int32 StartZoneIndex {};
UPROPERTY(Category = "Level", EditDefaultsOnly)
TArray<FLevelConfig> Levels;
};
inline FString FLevelState::ToString() const
{
return bMultipleZones ? FString::Printf(TEXT("%d-%d"), LevelIndex + 1, ZoneIndex + 1) :
FString::Printf(TEXT("%d"), LevelIndex + 1);
}
inline bool ULevelLauncher::HasNextZone(const FLevelState& CurrentLevelState) const
{
AssertState();
return NextZone(CurrentLevelState);
}
LevelLauncher.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "LevelLauncher.h"
#include "Kismet/GameplayStatics.h"
FLevelState::FLevelState(const FLevelConfig& LevelConfig, int32 LevelIndex, int32 ZoneIndex)
: LevelName(LevelConfig.LevelName), LevelIndex(LevelIndex), ZoneIndex(ZoneIndex), bMultipleZones(LevelConfig.Zones.Num() > 1)
{
}
void ULevelLauncher::Init(UWorld* TheWorld, const TArray<FLevelConfig>& TheLevels, int32 TheStartLevelIndex, int32 TheStartZoneIndex)
{
UE_LOG(LogTemp, Display, TEXT("%s: initialized with %d level(%s); StartLevelIndex=%d; StartZoneIndex=%d"),
*GetName(), TheLevels.Num(), (TheLevels.Num() > 1 ? TEXT("s") : TEXT("")),
StartLevelIndex, StartZoneIndex);
this->World = TheWorld;
this->Levels = TheLevels;
this->StartLevelIndex = TheStartLevelIndex;
this->StartZoneIndex = TheStartZoneIndex;
AssertState();
for (const auto& level : Levels)
{
if (level.LevelName.IsNone() || !level.Zones.Num())
{
UE_LOG(LogTemp, Fatal, TEXT("%s: Init - Invalid level set with name %s and zone count %d"), *GetName(), *level.LevelName.ToString(), level.Zones.Num());
return;
}
}
}
void ULevelLauncher::AssertState() const
{
if (!World)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: AssertState - World is NULL!"), *GetName());
return;
}
if (!Levels.Num())
{
UE_LOG(LogTemp, Fatal, TEXT("%s: AssertState - no levels configured!"), *GetName());
return;
}
}
bool ULevelLauncher::NextZone(const FLevelState& CurrentLevelState, FLevelState* OutNextZoneState, FLevelZoneConfig* OutZoneConfig) const
{
AssertState();
const FLevelConfig* LevelConfig;
if (!GetLevelConfig(CurrentLevelState, &LevelConfig))
{
return false;
}
// check if there is a next zone in current level
const FLevelZoneConfig* ZoneConfig;
FLevelState NextZoneLevelState(CurrentLevelState);
++NextZoneLevelState.ZoneIndex;
if (GetZoneConfig(NextZoneLevelState, *LevelConfig, &ZoneConfig))
{
if (OutNextZoneState)
{
*OutNextZoneState = NextZoneLevelState;
}
if (OutZoneConfig)
{
*OutZoneConfig = *ZoneConfig;
}
return true;
}
// First zone in next level is the "NextZone"
++NextZoneLevelState.LevelIndex;
NextZoneLevelState.ZoneIndex = 0;
if (!GetLevelConfig(NextZoneLevelState, &LevelConfig))
{
return false;
}
NextZoneLevelState.SetLevelConfig(*LevelConfig);
if (!GetZoneConfig(NextZoneLevelState, *LevelConfig, &ZoneConfig))
{
return false;
}
if (OutNextZoneState)
{
*OutNextZoneState = NextZoneLevelState;
}
if (OutZoneConfig)
{
*OutZoneConfig = *ZoneConfig;
}
return true;
}
void FLevelState::SetLevelConfig(const FLevelConfig& LevelConfig)
{
bMultipleZones = LevelConfig.Zones.Num() > 1;
}
bool ULevelLauncher::LoadZone(const FLevelState& CurrentLevelState) const
{
AssertState();
const FLevelConfig* LevelConfig;
if (!GetLevelConfig(CurrentLevelState, &LevelConfig))
{
return false;
}
UE_LOG(LogTemp, Display, TEXT("Loading LevelName: %s"), *LevelConfig->LevelName.ToString());
UGameplayStatics::OpenLevel(World, LevelConfig->LevelName);
return true;
}
void ULevelLauncher::FirstZone(FLevelState* outNextZoneState, FLevelZoneConfig* OutZoneConfig) const
{
AssertState();
// Try and honor any starting level and zone overrides
const FLevelConfig* LevelConfig(&Levels[0]);
int32 FirstLevelIndex{ StartLevelIndex };
int32 FirstZoneIndex{ StartZoneIndex };
FLevelState FirstZoneState(*LevelConfig, FirstLevelIndex, FirstZoneIndex);
FLevelZoneConfig FirstZoneConfig;
if (!GetLevelConfig(FirstZoneState, &LevelConfig))
{
// invalid level config - reset to first level and zone
FirstZoneConfig = Levels[0].Zones[0];
FirstLevelIndex = FirstZoneIndex = 0;
}
else if (!GetZoneConfig(FirstZoneState, FirstZoneConfig))
{
// invalid zone config - keep level but reset zone to index 0 for that level
FirstZoneConfig = LevelConfig->Zones[0];
FirstZoneIndex = 0;
}
// ensure all state synced properly - i.e. level config
FirstZoneState = FLevelState(*LevelConfig, FirstLevelIndex, FirstZoneIndex);
if (outNextZoneState)
{
*outNextZoneState = FirstZoneState;
}
if (OutZoneConfig)
{
*OutZoneConfig = FirstZoneConfig;
}
}
bool ULevelLauncher::HasNextLevel(const FLevelState& CurrentLevelState) const
{
AssertState();
const FLevelConfig* LevelConfig;
if (!GetLevelConfig(CurrentLevelState, &LevelConfig))
{
return false;
}
// set to last zone of current valid level
FLevelState LastZoneStateCurrentLevel(CurrentLevelState);
LastZoneStateCurrentLevel.ZoneIndex = LevelConfig->Zones.Num() - 1;
return NextZone(LastZoneStateCurrentLevel);
}
bool ULevelLauncher::GetZoneConfig(const FLevelState& LevelState, FLevelZoneConfig& OutZoneConfig) const
{
const FLevelConfig* LevelConfig;
if (!GetLevelConfig(LevelState, &LevelConfig))
{
return false;
}
const FLevelZoneConfig* ZoneConfig;
if (!GetZoneConfig(LevelState, *LevelConfig, &ZoneConfig))
{
return false;
}
OutZoneConfig = *ZoneConfig;
return true;
}
bool ULevelLauncher::GetLevelConfig(const FLevelState& LevelState, const FLevelConfig** OutLevelConfig) const
{
if (LevelState.LevelIndex < 0 || LevelState.LevelIndex >= Levels.Num())
{
return false;
}
if (OutLevelConfig)
{
*OutLevelConfig = &Levels[LevelState.LevelIndex];
}
return true;
}
bool ULevelLauncher::GetZoneConfig(const FLevelState& LevelState, const FLevelConfig& LevelConfig, const FLevelZoneConfig** OutLevelZoneConfig) const
{
AssertState();
if (LevelState.ZoneIndex < 0 || LevelState.ZoneIndex >= LevelConfig.Zones.Num() ||
LevelState.LevelIndex < 0 || LevelState.LevelIndex >= Levels.Num())
{
return false;
}
if (OutLevelZoneConfig)
{
*OutLevelZoneConfig = &Levels[LevelState.LevelIndex].Zones[LevelState.ZoneIndex];
}
return true;
}
LevelLauncherTest.cpp
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "Engine/World.h"
#include "LevelLauncher.h"
#if WITH_DEV_AUTOMATION_TESTS
namespace
{
ULevelLauncher* NewLauncher();
TArray<FLevelConfig> CreateLevels();
const TArray<FLevelConfig> Levels = CreateLevels();
}
BEGIN_DEFINE_SPEC(LevelLauncherTest, "LevelLauncher", EAutomationTestFlags::ApplicationContextMask | EAutomationTestFlags::ProductFilter)
END_DEFINE_SPEC(LevelLauncherTest)
void LevelLauncherTest::Define() {
Describe("FLevelState::ToString", [this]() {
It("Should Return 1-1 for first level and zone", [this]() {
FLevelState LevelState(Levels[0]);
TestEqual(TEXT("Level 1-1"), *LevelState.ToString(), TEXT("1-1"));
});
It("Should Return 1-2 for first level and second zone", [this]() {
FLevelState LevelState(Levels[0], 0, 1);
TestEqual(TEXT("Level 1-2"), *LevelState.ToString(), TEXT("1-2"));
});
It("Should Return 2-1 for second level and first zone", [this]() {
FLevelState LevelState(Levels[0], 1, 0);
TestEqual(TEXT("Level 2-1"), *LevelState.ToString(), TEXT("2-1"));
});
It("Should Return only level name when there is only one zone in the config", [this]() {
const FLevelConfig LevelConfig
{
FName("Level"),
{
FLevelZoneConfig
{
EAIDifficulty::Hard,
10,
1,
1,
50
}
}
};
FLevelState LevelState(LevelConfig, 1, 0);
TestEqual(TEXT("Level with only one zone"), *LevelState.ToString(), TEXT("2"));
});
});
Describe("HasNextZone", [this]() {
It("Should Return True on first zone", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0]);
TestTrue(TEXT("HasNextZone returns true when there is a next zone on same level"), LevelLauncher->HasNextZone(LevelState));
});
It("Should Return True on second zone", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0], 0, 1);
TestTrue(TEXT("HasNextZone returns true when there is a next zone on same level"), LevelLauncher->HasNextZone(LevelState));
});
It("Should Return True on last zone of level when next level exists", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0], 0, 2);
TestTrue(TEXT("HasNextZone returns true when there is a next zone on next level"), LevelLauncher->HasNextZone(LevelState));
});
It("Should Return False on last zone of level when next level does not exist", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[1], 1, 2);
TestFalse(TEXT("HasNextZone returns false when there is not a next zone on next level"), LevelLauncher->HasNextZone(LevelState));
});
});
Describe("HasNextLevel", [this]() {
It("Should Return True on first level", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0],0,1);
TestTrue(TEXT("HasNextLevel returns true when there is a next level"), LevelLauncher->HasNextLevel(LevelState));
});
It("Should Return False on last level", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[1], 1, 0);
TestFalse(TEXT("HasNextLevel returns false when there is no next level"), LevelLauncher->HasNextLevel(LevelState));
});
});
Describe("NextZone", [this]() {
It("Should Return True on first zone", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0]);
FLevelState NextLevelStateOne, NextLevelStateTwo;
FLevelZoneConfig NextZoneConfig;
TestTrue(TEXT("NextZone returns true when there is a next zone on same level"), LevelLauncher->NextZone(LevelState));
TestTrue(TEXT("NextZone returns true when there is a next zone on same level"), LevelLauncher->NextZone(LevelState, &NextLevelStateOne));
TestEqual(TEXT("NextLevelState populated"), NextLevelStateOne.LevelIndex, 0);
TestEqual(TEXT("NextLevelState populated"), NextLevelStateOne.ZoneIndex, 1);
TestTrue(TEXT("NextZone returns true when there is a next zone on same level"), LevelLauncher->NextZone(LevelState, &NextLevelStateTwo, &NextZoneConfig));
TestEqual(TEXT("NextLevelState populated"), NextLevelStateTwo.LevelIndex, 0);
TestEqual(TEXT("NextLevelState populated"), NextLevelStateTwo.ZoneIndex, 1);
TestEqual(TEXT("NextZoneConfig populated"), NextZoneConfig.TimeTargetSeconds, 90);
});
It("Should Return True on second zone", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0], 0, 1);
FLevelState NextLevelStateOne, NextLevelStateTwo;
FLevelZoneConfig NextZoneConfig;
TestTrue(TEXT("NextZone returns true when there is a next zone on same level"), LevelLauncher->NextZone(LevelState));
TestTrue(TEXT("NextZone returns true when there is a next zone on same level"), LevelLauncher->NextZone(LevelState, &NextLevelStateOne));
TestEqual(TEXT("NextLevelState populated"), NextLevelStateOne.LevelIndex, 0);
TestEqual(TEXT("NextLevelState populated"), NextLevelStateOne.ZoneIndex, 2);
TestTrue(TEXT("NextZone returns true when there is a next zone on same level"), LevelLauncher->NextZone(LevelState, &NextLevelStateTwo, &NextZoneConfig));
TestEqual(TEXT("NextLevelState populated"), NextLevelStateTwo.LevelIndex, 0);
TestEqual(TEXT("NextLevelState populated"), NextLevelStateTwo.ZoneIndex, 2);
TestEqual(TEXT("NextZoneConfig populated"), NextZoneConfig.TimeTargetSeconds, 60);
});
It("Should Return True on last zone of level when next level exists", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0], 0, 2);
FLevelState NextLevelStateOne, NextLevelStateTwo;
FLevelZoneConfig NextZoneConfig;
TestTrue(TEXT("NextZone returns true when there is a next zone on next level"), LevelLauncher->NextZone(LevelState));
TestTrue(TEXT("NextZone returns true when there is a next zone on next level"), LevelLauncher->NextZone(LevelState, &NextLevelStateOne));
TestEqual(TEXT("NextLevelState populated"), NextLevelStateOne.LevelIndex, 1);
TestEqual(TEXT("NextLevelState populated"), NextLevelStateOne.ZoneIndex, 0);
TestTrue(TEXT("NextZone returns true when there is a next zone on next level"), LevelLauncher->NextZone(LevelState, &NextLevelStateTwo, &NextZoneConfig));
TestEqual(TEXT("NextLevelState populated"), NextLevelStateTwo.LevelIndex, 1);
TestEqual(TEXT("NextLevelState populated"), NextLevelStateTwo.ZoneIndex, 0);
TestEqual(TEXT("NextZoneConfig populated"), NextZoneConfig.TimeTargetSeconds, 50);
});
It("Should Return False on last zone of level when next level does not exist", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[1], 1, 2);
TestFalse(TEXT("NextZone returns false when there is not a next zone on next level"), LevelLauncher->NextZone(LevelState));
});
});
Describe("FirstZone", [this]() {
It("Should Return first zone of first level", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState FirstLevelStateOne, FirstLevelStateTwo;
FLevelZoneConfig FirstZoneConfig;
LevelLauncher->FirstZone(&FirstLevelStateOne);
TestEqual(TEXT("FirstLevelState populated"), FirstLevelStateOne.LevelIndex, 0);
TestEqual(TEXT("FirstLevelState populated"), FirstLevelStateOne.ZoneIndex, 0);
LevelLauncher->FirstZone(&FirstLevelStateTwo, &FirstZoneConfig);
TestEqual(TEXT("FirstLevelState populated"), FirstLevelStateTwo.LevelIndex, 0);
TestEqual(TEXT("FirstLevelState populated"), FirstLevelStateTwo.ZoneIndex, 0);
TestEqual(TEXT("FirstZoneConfig populated"), FirstZoneConfig.TimeTargetSeconds, 120);
});
});
Describe("GetZoneConfig", [this]() {
It("Should Return True on first zone", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0]);
FLevelZoneConfig ZoneConfig;
TestTrue(TEXT("GetZoneConfig returns true for first zone"), LevelLauncher->GetZoneConfig(LevelState, ZoneConfig));
TestEqual(TEXT("ZoneConfig populated"), ZoneConfig.TimeTargetSeconds, 120);
});
It("Should Return True on second zone", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0], 0, 1);
FLevelZoneConfig ZoneConfig;
TestTrue(TEXT("GetZoneConfig returns true for next zone on same level"), LevelLauncher->GetZoneConfig(LevelState, ZoneConfig));
TestEqual(TEXT("ZoneConfig populated"), ZoneConfig.TimeTargetSeconds, 90);
});
It("Should Return True for last zone first level", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0], 0, 2);
FLevelZoneConfig ZoneConfig;
TestTrue(TEXT("GetZoneConfig returns true for last zone on same level"), LevelLauncher->GetZoneConfig(LevelState, ZoneConfig));
TestEqual(TEXT("ZoneConfig populated"), ZoneConfig.TimeTargetSeconds, 60);
});
It("Should Return false for non-existent zone on a level", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[0], 0, 100);
FLevelZoneConfig ZoneConfig;
TestFalse(TEXT("GetZoneConfig returns false for non-existent zone on an existing level"), LevelLauncher->GetZoneConfig(LevelState, ZoneConfig));
});
It("Should Return True on first zone of last level", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[1], 1, 0);
FLevelZoneConfig ZoneConfig;
TestTrue(TEXT("GetZoneConfig returns true for first zone on last level"), LevelLauncher->GetZoneConfig(LevelState, ZoneConfig));
TestEqual(TEXT("ZoneConfig populated"), ZoneConfig.TimeTargetSeconds, 50);
});
It("Should Return True on last zone of last level", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[1], 1, 2);
FLevelZoneConfig ZoneConfig;
TestTrue(TEXT("GetZoneConfig returns true for last zone on last level"), LevelLauncher->GetZoneConfig(LevelState, ZoneConfig));
TestEqual(TEXT("ZoneConfig populated"), ZoneConfig.TimeTargetSeconds, 30);
});
It("Should Return False for a level index that does not exist", [this]() {
auto LevelLauncher = NewLauncher();
FLevelState LevelState(Levels[1], 100);
FLevelZoneConfig ZoneConfig;
TestFalse(TEXT("HasNextZone returns false when there is not a level with the given index"), LevelLauncher->GetZoneConfig(LevelState, ZoneConfig));
});
});
}
namespace
{
ULevelLauncher* NewLauncher()
{
ULevelLauncher* Launcher = NewObject<ULevelLauncher>();
Launcher->Init(NewObject<UWorld>(), Levels);
return Launcher;
}
TArray<FLevelConfig> CreateLevels()
{
TArray<FLevelConfig> levs
{
FLevelConfig
{
FName("LevelOne"),
{
FLevelZoneConfig
{
EAIDifficulty::Easy,
10,
1,
1,
120
},
FLevelZoneConfig
{
EAIDifficulty::Normal,
12,
1,
1,
90
},
FLevelZoneConfig
{
EAIDifficulty::Hard,
15,
1,
1,
60
}
}
},
FLevelConfig
{
FName("LevelTwo"),
{
FLevelZoneConfig
{
EAIDifficulty::Hard,
10,
1,
1,
50
},
FLevelZoneConfig
{
EAIDifficulty::Veteran,
12,
1,
1,
40
},
FLevelZoneConfig
{
EAIDifficulty::Epic,
15,
1,
1,
30
}
}
}
};
return levs;
}
}
#endif
And here are a couple of examples of usage
bool ASimpleShooterGameModeBase::NextZone()
{
ASimpleShooterGameStateBase* ShooterGameStateBase = GetGameState<ASimpleShooterGameStateBase>();
if (!ShooterGameStateBase)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: NextZone - game state is not an instance of ASimpleShooterGameStateBase"), *GetName());
return false;
}
FLevelState NextZoneState;
if (!LevelLauncher->NextZone(ShooterGameStateBase->GetCurrentLevelState(), &NextZoneState))
{
DeleteSavedGame();
return false;
}
ShooterGameStateBase->SetCurrentLevelState(NextZoneState);
SaveGame();
return LevelLauncher->LoadZone(NextZoneState);
}
void ASimpleShooterGameModeBase::RestartZone()
{
ASimpleShooterGameStateBase* ShooterGameStateBase = GetGameState<ASimpleShooterGameStateBase>();
if (!ShooterGameStateBase)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: NextZone - game state is not an instance of ASimpleShooterGameStateBase"), *GetName());
return;
}
LevelLauncher->LoadZone(ShooterGameStateBase->GetCurrentLevelState());
}
void ASimpleShooterGameModeBase::NewGame()
{
ASimpleShooterGameStateBase* ShooterGameStateBase = GetGameState<ASimpleShooterGameStateBase>();
if (!ShooterGameStateBase)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: NextZone - game state is not an instance of ASimpleShooterGameStateBase"), *GetName());
return;
}
DeleteSavedGame();
FLevelState FirstZoneState;
LevelLauncher->FirstZone(&FirstZoneState);
LevelLauncher->LoadZone(FirstZoneState);
}void ASimpleShooterGameModeBase::NewGame()
{
ASimpleShooterGameStateBase* ShooterGameStateBase = GetGameState<ASimpleShooterGameStateBase>();
if (!ShooterGameStateBase)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: NextZone - game state is not an instance of ASimpleShooterGameStateBase"), *GetName());
return;
}
DeleteSavedGame();
FLevelState FirstZoneState;
LevelLauncher->FirstZone(&FirstZoneState);
LevelLauncher->LoadZone(FirstZoneState);
}void ASimpleShooterGameModeBase::NewGame()
{
ASimpleShooterGameStateBase* ShooterGameStateBase = GetGameState<ASimpleShooterGameStateBase>();
if (!ShooterGameStateBase)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: NextZone - game state is not an instance of ASimpleShooterGameStateBase"), *GetName());
return;
}
DeleteSavedGame();
FLevelState FirstZoneState;
LevelLauncher->FirstZone(&FirstZoneState);
LevelLauncher->LoadZone(FirstZoneState);
}void ASimpleShooterGameModeBase::NewGame()
{
ASimpleShooterGameStateBase* ShooterGameStateBase = GetGameState<ASimpleShooterGameStateBase>();
if (!ShooterGameStateBase)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: NextZone - game state is not an instance of ASimpleShooterGameStateBase"), *GetName());
return;
}
DeleteSavedGame();
FLevelState FirstZoneState;
LevelLauncher->FirstZone(&FirstZoneState);
LevelLauncher->LoadZone(FirstZoneState);
}void ASimpleShooterGameModeBase::NewGame()
{
ASimpleShooterGameStateBase* ShooterGameStateBase = GetGameState<ASimpleShooterGameStateBase>();
if (!ShooterGameStateBase)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: NextZone - game state is not an instance of ASimpleShooterGameStateBase"), *GetName());
return;
}
DeleteSavedGame();
FLevelState FirstZoneState;
LevelLauncher->FirstZone(&FirstZoneState);
LevelLauncher->LoadZone(FirstZoneState);
}void ASimpleShooterGameModeBase::NewGame()
{
ASimpleShooterGameStateBase* ShooterGameStateBase = GetGameState<ASimpleShooterGameStateBase>();
if (!ShooterGameStateBase)
{
UE_LOG(LogTemp, Fatal, TEXT("%s: NextZone - game state is not an instance of ASimpleShooterGameStateBase"), *GetName());
return;
}
DeleteSavedGame();
FLevelState FirstZoneState;
LevelLauncher->FirstZone(&FirstZoneState);
LevelLauncher->LoadZone(FirstZoneState);
}
void ASinglePlayerEnemyHuntGameMode::SaveGame()
{
UE_LOG(LogTemp, Display, TEXT("Saving Game..."));
ASinglePlayerEnemyHuntGameState* MyGameState = Cast<ASinglePlayerEnemyHuntGameState>(GameState);
if (!MyGameState)
{
UE_LOG(LogTemp, Error, TEXT("ASinglePlayerEnemyHuntGameMode::SaveGame - GameState did not return a ASinglePlayerEnemyHuntGameState instance"));
return;
}
USaveSinglePlayerEnemyHuntGame* SavedGame = SaveGameUtilities::CreateSaveGameInstance<USaveSinglePlayerEnemyHuntGame>();
if (!SavedGame)
{
UE_LOG(LogTemp, Error, TEXT("Unable to create a saved game instance"));
return;
}
SavedGame->SetFromGameState(GetWorld(), *MyGameState);
SavedGame->Save();
OnGameSaved.Broadcast();
UE_LOG(LogTemp, Display, TEXT("Game Saved Successfully"));
}
void ASinglePlayerEnemyHuntGameMode::LoadGame()
{
if (!DoesSaveGameExist())
{
return;
}
UE_LOG(LogTemp, Display, TEXT("Loading Game..."));
ASinglePlayerEnemyHuntGameState* MyGameState = Cast<ASinglePlayerEnemyHuntGameState>(GameState);
if (!MyGameState)
{
UE_LOG(LogTemp, Error, TEXT("ASinglePlayerEnemyHuntGameMode::LoadGame - GameState did not return a ASinglePlayerEnemyHuntGameState instance"));
return;
}
USaveSinglePlayerEnemyHuntGame* LoadedGame = GetSavedGame();
if (LoadedGame)
{
UE_LOG(LogTemp, Display, TEXT("Loading game: Version=%d;SlotIndex=%d; SlotName=%s"), LoadedGame->GetVersion(), LoadedGame->SlotIndex, *LoadedGame->SlotName);
LevelLauncher->LoadZone(LoadedGame->CurrentLevelState);
}
}