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.
- Make sure it's new grid coordinate is different from it's previous
- Remove it from it's current
FGridCell::ActorsInCell
- Remove it from all cells it sees by reversing generating vision and remove the actor from each
TArray<TSet<AXYZActor*>> ActorsWithVisionByTeam = {{}, {}};
it used to see - If any of those cells now contains 0 actors in their vision set then we will add that to the NonVisible(Actor/Cell) Set and add it to the VisibleSet(Actor/Cell) Set
- Add the actor to it's new location in the
Grid
- Generate it's vision and add it to each
TArray<TSet<AXYZActor*>> ActorsWithVisionByTeam = {{}, {}};
- Make each of those cells visible and remove from the nonvisible set
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!