Need help finishing up my Advanced Wave Spawning System.

So, as part of my C++ tutelage, I have been brainstorming and slowly implementing a Wave Spawning System similar to that used in Nazi Zombies. I want to release this as a free example/tutorial (along with the rest of my tps shooter stuff) but I am having a bit of trouble putting it all together. I am going to break a few of the code pieces apart and ask questions or explain why I am using them(I will post the full code ,h & .cpp at the bottom. Feel free to comment on everything as I don’t pretend to know the right way to do stuff (I come from C# so I still sort of think in C# ways unfortunately).

First off I will create a struct that can use data from an excel file.

ZombieSpawnTutorial.h




//creation of enemy information
USTRUCT()
struct FEnemyInfo :  FTableRowBase
{
	GENERATED_USTRUCT_BODY()
	
	UPROPERTY()
	float percentage;

	UPROPERTY()
	APawn* BotPawnClass; // this will tell the spawner what Pawn to spawn
};



And we reach my first question: basically I am trying to figure out how to format the excel file. The data i need would be stored in type of enemy and waves. So for example:

Wave 1
Zombie, 1.0 chance
Bloater, 0.0 chance
Boss, 0.0 chance

Wave 2
Zombie, .7 chance
Bloater, .3 chance
Boss, 0.0 chance

Etc.

Now, should i have separate excel files for each wave, or is there a way to condense this data so finding a row (or rows) gets the percentages needed to decide what Enemy to spawn.

Anyways, here is the full .cpp for those interested. If some of this looks familiar it is because I am borrow some basic stuff from the free SurvivalGame Template by Tom Looman (of which I am upgrading into a full TPS system in order to learn c++):

ZombieSpawnTutorial.cpp (comments should tell you what is going on and why).



#include "SurvivalGame.h"
#include "SZombieAIController.h"
#include "ZombieSpawnTutorial.h"



AZombieSpawnTutorial::AZombieSpawnTutorial(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	
	WaveNumber = 1;				//current wave number, initialize at 1 for ease of use.
	MaxSpawnsPerWave = 0;		//The number of enemies spawned during the entire wave, used to end a wave when all are dead
	CurrentSpawns = 0;			//current number of zombies in the world
	MaxSpawnsAtTime = 0;		//max number of zombies allowed in the level at one time
	CanSpawn  = false;			//can zombies be spawned?, used to stop spawning during "rest time"
	BotSpawnInterval = 2.0f;	//intervalbetween spawns at a spawner.
	TimeBetweenWaves = 30.0f;	//how much time there is between the end of one wave and the start of another
	

}





void AZombieSpawnTutorial::StartMatch()
{


	/* Spawn a new bot every n seconds (bothandler will opt-out based on if CanSpawn is true) */
	GetWorldTimerManager().SetTimer(TimerHandle_BotSpawns, this, &AZombieSpawnTutorial::SpawnBotHandler, BotSpawnInterval, true);
}

void AZombieSpawnTutorial::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	/* Immediately start the match while playing in editor */
	if (GetWorld()->IsPlayInEditor())
	{
		StartMatch();
	}

	//Has the Current number of zombies spawned from the begining of the wave reached the Max Total for that wave
	if (CurrentSpawns >= MaxSpawnsPerWave)
	{
		if (CanSpawn) CanSpawn = false;       //stop spawning zombies
		
		//Are all zombies dead? If so End the wave.
		if (GetWorld()->GetNumPawns() <= 0)	  /*BUG: Need  to find all of the current players and subtract them otherwise this will never return true*/
		{
			EndWave();
		}
	}
}



//Randomly select a Bot from the WaveSpawn Data and Spawn it
void AZombieSpawnTutorial::SpawnNewBot(FEnemyInfo WaveSpawnData)  /*Missing:Needs to bring in the WaveSpawnDataArray*/
{
	
	/*MISSING: Need code to randomly select Bot to spawn and Spawn it*/
	
	//After bot is Spawned increment current spawns by one
	CurrentSpawns = CurrentSpawns + 1; 
	
}

//Decide when to spawn bots
void AZombieSpawnTutorial::SpawnBotHandler()
{
	if (!CanSpawn) return; //if we cannot spawn leave the function

		//Check number of pawns in the world and how many have been spawn during the wave 
		if (GetWorld()->GetNumPawns() < MaxSpawnsAtTime && CurrentSpawns < MaxSpawnsPerWave) /*BUG:Need to subtract the players from the GetNumPawns()*/
		{
			//Start Spawning
			SpawnNewBot(WaveSpawnData);
			
		}
		
		
}

//Start a wave
void AZombieSpawnTutorial::StartWave()
{
	//clear the Rest Timer so it can be used again later and doesn't continueally update
	GetWorldTimerManager().ClearTimer(TimerHandle_RestTimer);
	
	//set current spawns to zero
	CurrentSpawns = 0;
	
	//tell spawnhandler it can start spawning again
	CanSpawn = true;
	
	/*MISSING: Code to update UI with current wavenumber*/
	
	//this switch initializes the data neeeded for the SpawnHander and SpawnNewBotFunctions, set increments here
	/*MISSING: Need a way to store WaveSpawnData*/
	/*It Should look like this:
		const EnemyInfo enemyData] = {
		{ Enemy::ZombieEasy,             100, PawnToSpawn },       
		{ Enemy::ZombieMedium,           0,   PawnToSpawn },      
		{ Enemy::ZombieHard,             0,	  PawnToSpawn },       
		{ Enemy::Exploder,			     0,   PawnToSpawn },
		{ Enemy::Ranged,				 0,   PawnToSpawn },       
		{ Enemy::Fast,					 0,   PawnToSpawn },      
		{ Enemy::Boss,					 0,   PawnToSpawn }
		};
	the first item is the percentage, the second is a reference to the Pawn to be spawned

	Should it be hard coded? Or should it be held in a text file somewhere? Needs exploration
	*/
	switch (WaveNumber)
	{
	case 0:
		///do nothing
		break;
	case 1:
		MaxSpawnsPerWave = 10;
		MaxSpawnsAtTime = 10;
		//WaveSpawnData == 
		break;
	case 2:
		MaxSpawnsPerWave = 20;
		MaxSpawnsAtTime = 15;
		//WaveSpawnData == 
		break;
	case 3:
		MaxSpawnsPerWave = 30;
		MaxSpawnsAtTime = 20;
		//WaveSpawnData == 
		break;
	case 4:
		MaxSpawnsPerWave = 40;
		MaxSpawnsAtTime = 25;
		//WaveSpawnData == 
		break;
	case 5:
		MaxSpawnsPerWave = 50;
		MaxSpawnsAtTime = 30;
		//WaveSpawnData == 
		break;
	case 6:
		MaxSpawnsPerWave = 60;
		MaxSpawnsAtTime = 35;
		//WaveSpawnData == 
		break;
	case 7:
		MaxSpawnsPerWave =70;
		MaxSpawnsAtTime = 40;
		//WaveSpawnData == 
		break;
	case 8:
		MaxSpawnsPerWave = 80;
		MaxSpawnsAtTime = 45;
		//WaveSpawnData == 
		break;
	case 9:
		MaxSpawnsPerWave =90;
		MaxSpawnsAtTime = 50;
		//WaveSpawnData == 
		break;
	case 10:
		MaxSpawnsPerWave = 100;
		MaxSpawnsAtTime = 55;
		//WaveSpawnData == 
		break;
	}
}



	
//End the wave
void AZombieSpawnTutorial::EndWave()
{
	//increment wavenumber
	WaveNumber = WaveNumber + 1;
	
	//Start the rest timer
	StartRestTimer();
}

void AZombieSpawnTutorial::StartRestTimer()
{
	//Rest timer will start the next wave after interval
	GetWorldTimerManager().SetTimer(TimerHandle_RestTimer, this, &AZombieSpawnTutorial::StartWave, TimeBetweenWaves, true);
}



Any comments are appreciated (particularly if I am doing something wrong or not using the best method). Again, once I get all this working I want to release a full tutorial explaining how to do it.

I always try to code in a way that is designer friendly, even if I’m working on my own. It seems you would be better served by using a DataTable. You can create and edit these in editor or export/import data to .csv and .json.
DataTables are built from a struct, make sure your struct is properly exposed to the editor.

Everything you need to know about referencing a DataTable in C++ and accessing its rows can be found in these links:

You dont actually need Excel to work with data tables anymore, those links are a little old. In the content browser RightClick->Miscellaneous->DataTable to create a new table. A popup asks you for a struct to base the table on, just give it your FWaveData struct and you can start adding rows for each wave.

Here’s an example of how I would approach it. I havn’t compiled any of this, just copy/pasted from a few sources and tweaked, but it should give you an idea of how to set it up.

In .h



USTRUCT(BlueprintType)
struct FWaveData
{
	GENERATED_USTRUCT_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		int32 MaxSpawnsPerWave;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		int32 MaxSpawnsAtATime;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		TArray<int32> SpawnPriorities;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
		TArray<TSubclassOf<AEnemy_Base>> EnemyTypes;
};

UPROPERTY(BlueprintReadOnly)
UDataTable* WaveDataTable;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
FWaveData CurrentWaveData;



In .cpp



AZombieSpawnTutorial::AZombieSpawnTutorial()  //Find the editor asset in the constructor
{
        ConstructorHelpers::FObjectFinder<UDataTable> WaveDataTable_BP(TEXT("DataTable'/Game/DataTables/ExcelExample.ExcelExample'"));

        WaveDataTable= WaveDataTable_BP.Object;
}

AZombieSpawnTutorial::IterateWave()
{
	CurrentWave++;

	FString WaveName = FString::FromInt(CurrentWave);

	CurrentWaveData = WaveDataTable->FindRow<FWaveData>(FText::FromString(WaveName));

	OnBeginSpawnWave();
}


The resulting DataTable in editor. Super easy to edit!

Thanks Hum, that’s a great tutorial on DataTables.

Yeah I got a little too into it :stuck_out_tongue:
The TL;DR would be: Use data tables for defining waves. They’re awesome!

Wow thanks so much! I got it working with excel but this is way more elegant. Now to finish up this tutorial

I was wondering if there was an update on this Tutorial Project release date?