XYZ

Fog of War 2

If you read my first devlog on Fog of War you'll note that I left some comments about how I should probably improve it in the future. Well after trying to stress test 100+ units moving at once I quickly noticed an enourmous amount of lag. I turned on unit stat and we'll every frame was taking around 60ms which well is not good. While I am running 2 clients + the server this is still way too high for my liking.

So I started up a trace log, and quickly noticed that a big culprit was my XYZGameMode::Process(float DeltaTime) function using up around 24ms per tick. This is the main driver of the game's engine. While the game in IN_PROGRESS state the following managers are run.

void AXYZGameMode::Process(float DeltaSeconds)
{
    ActorManager->Process(DeltaSeconds);
    InputManager->Process(DeltaSeconds);
    BlobManager->Process(DeltaSeconds);
    UpgradeManager->Process(DeltaSeconds);
    DeathManager->Process(DeltaSeconds);
    MapManager->Process(DeltaSeconds);
    MatchManager->Process(DeltaSeconds);
    ProjectileManager->Process(DeltaSeconds);

and the big culprit was our MapManager->Process(DeltaSeconds) it was hitching the games tick by a gigantic amount.

MapManager

Our map manager contains our grid which is a map of <FIntVector2,FGridCell> this allows us quickly access actors based on their location in world which is handy for keeping track of team vision.

TMap<FIntVector2, TSharedPtr<FGridCell>> Grid;
int32 GRID_SIZE = 128;
float MAP_SIZE = 10000;

struct FGridCell
	TSet<AXYZActor*> ActorsInCell;
	TArray<bool> TeamVisions= {false, false};
	int32 Height = 0;

Currently in our XYZMapManager::Process we clear all the vision in the map turning all the TeamVisions to {false,false} we remove all the actors in the grid. Then we go through all our actors and generate their vision by looping over the tiles they can see in their vision range. We store our past Delta(Non/Visible)(Actors/Cells) and we send the client the difference since we don't need to rehide things that are hidden or show actors we already see.

Ok so how do we fix this? I think the classic trading time for space is the answer. First let's modify how we check if a player has vision. Instead of a raw bool let's store which actors see each cell. And if a team has > 0 actors seeing the cell that means that grid is visible.

struct FGridCell
	TSet<AXYZActor*> ActorsInCell;
	TArray<TSet<AXYZActor*>> ActorsWithVisionByTeam = {{}, {}};
	int32 Height = 0;

Next I don't want to clear the grid or regenerate the vision for every actor each time we call UXYZMapManager::Process instead let's create some new variables to keep track of what actors are visible/hidden and the same for cells.

UXYZMapManger
TArray<TSet<FIntVector2>> VisibleCells = {{},{}};
TArray<TSet<FIntVector2>> NonVisibleCells = {{},{}};
TArray<TSet<AXYZActor*>> VisibleActors = {{},{}};
TArray<TSet<AXYZActor*>> NonVisibleActors = {{},{}};

We will also add a TSet<AXYZActor*> ActorsToUpdate and only regenerate their vision. Only actors that have moved atleast one cell can be added to the set.

Previously we were regenerating the vision for every actor after clearing our grid. This is also suboptimal, so let's think of a better way.

When we update an actor in the grid we will do the following.

Quite a lot of rules let's see the new code

void UXYZMapManager::Process(float DeltaSeconds) {
	for(AXYZActor* Actor : ActorsToUpdate)
	{
		if(!Actor || Actor->IsA(AXYZResourceActor::StaticClass()))
		{
			continue;
		}
		if(Actor->GridCoord == GetGridCoordinate(Actor->GetActorLocation())) continue;
		RemoveActorFromGrid(Actor);
		AddActorToGrid(Actor);
	}

Add is just reversed

void UXYZMapManager::RemoveActorFromGrid(AXYZActor* Actor) {
	if(Actor && IsGridCoordValid(Actor->GridCoord) && Grid[Actor->GridCoord]->ActorsInCell.Contains(Actor))
	{
		RemoveVisionForActor(Actor);
		Grid[Actor->GridCoord]->ActorsInCell.Remove(Actor);
		for(int i = 0;i < 2;i++)
		{
			if(!TeamHasVision(i, Actor->GridCoord))
			{
				NonVisibleActors[i].Add(Actor);
				VisibleActors[i].Remove(Actor);
			}
		}
	}
}
void UXYZMapManager::RemoveActorVision(AXYZActor* Actor, FIntVector2 GridCoord)
{
	if(Actor && Grid[GridCoord]->ActorsWithVisionByTeam.IsValidIndex(Actor->TeamId))
	{
		Grid[GridCoord]->ActorsWithVisionByTeam[Actor->TeamId].Remove(Actor);
	}
}

And after a lot of debugging it's working like a charm. Of course the trade off is we are storing our visible and nonvisible cells/actors, but I think I can live with that. Our MapManager went from taking from 20-40ms to a whopping 1-2ms! Sorry for no gifs, but I wrote this all after doing the fix.

Thanks for you reading feel free to email me at jandro@xyzrts.com if you have any questions or want to talk about gamedev!

View original