Battletech Developer Journal - 04

Published April 28, 2019
Advertisement

I'm Chris Eck, and I'm the tools developer at HBS for the Battletech project. I've recently been given permission to write up articles about some of the things I work on which I hope to post on a semi regular basis. Feel free to ask questions about these posts or give me suggestions for future topics. However, please note I am unable to answer any questions about new/unconfirmed features.

As a tools developer, my heavy workload is usually at the beginning of new versions. Laying all the groundwork for things to come. Now we're reaching the end of Urban Warfare (due out June 4th if you haven't heard). So I only have a few tasks left in my queue and none of them are critical for Urban Warfare. That leaves me in the lovely position of having time to tackle performance issues. I hate tackling performance issues. Especially in other people's code... And performance issues aren't sexy, so I'll also be talking about Encounter Logic and how that works too.

 

Performance

When tackling performance issues - the absolute first thing you do is MEASURE THE SLOWNESS! I can not stress this enough. I've seen it so many times where a developer (myself included) is "pretty sure the slowness is here" then they spend hours optimizing the wrong thing. Their efforts make a function taking half a millisecond run twice as fast and are surprised they don't notice anything when they retest. A performance profiler will tell you much more accurately where the problem is. 

I was assigned the task to start looking at Pathfinding and Line of Sight calculations during unit movement. In Urban environments especially, this can be a big drag. So the first thing I did was... hook up the profiler.

Profiling Snapshot

Running with the profiler attached makes things even worse. So don't look at these results and say OMG IT'S TOO SLOW! IT'LL NEVER WORK!! :)  Not only does the game have to run, but now it has to log information about every single function it calls and how much time each one takes. But since it's doing that everywhere, the profiler is still a good indicator for where the actual problem is. In the attached image, you can see the callstack of the running code and metrics about how long things took. Total is how much of that frame was used by this function and everything else it calls. Self is how much of that work was from this function.  

So in the highlighted node, we see that about 23.9% of the time is spent in PathNodeGrid.FindBlockerBetween() and 6.6% was because of what goes on inside that function leaving 17.3% to the children. The next line MapTerrainDataCell.get_height() takes 12.7% of the total time. This was a red flag to me. The height of a given cell should be lightning fast.

After some digging, it looks like the height of each cell is dynamically calculated by first seeing if there are any buildings in the hex. Getting the tallest one. Getting its height, and then comparing that to the height of the terrain. And then returning whichever one is bigger. It's doing this 12,351 times in this particular snapshot too. That is quite a bit of work for something that doesn't change very often. 

Instead of dynamically calculating the height, we can cache that value. Then update the height of cells when things change (like when a building gets destroyed). That basically eliminates the MapTerrainDataCell.get_height() row and all its children. Accessing a raw float is way faster than a property. There are a few tradeoffs though. It uses a bit more memory to cache that value. Obstructions now need to update their related cells when they get destroyed. And map load times will increase by a few milliseconds so it can build the cache. All in all, I think that's worth it. 


public float cachedHeight = 0f;

// This used to be a public property. I changed it to private so nobody could access it.
// That made refactoring easy/peasy cause the compiler told me wherever it was being used.
// Then I replaced accesses to height with cachedHeight.
private float height
{
	get
	{
		if ( MapEncounterLayerDataCell.HasBuilding)
			return Mathf.Max(MapEncounterLayerDataCell.GetBuildingHeightUnsafe(), terrainHeight);
		else
			return terrainHeight;
	}
}

// This function updates the cached values by using the old/slow properties. It
// gets called during the initial map load for every cell and then again if 
// buildings get destroyed. Other than that, a cell's height shouldn't change 
// so it will be safe to use the cached values.
public void UpdateCachedValues()
{
	cachedHeight = height;
	cachedSteepness = steepness;
}

There's definitely still more to do, but I just fixed about 12% of the slowness right then.

 

Encounter Logic

So the combat game is made up of all the rules of movement, shooting, melee, heat, stability, etc. It details out what you and the enemy can do. The Encounter Logic sits next to that and controls things like the states of objectives, whether or not a region is turned on, which units to spawn, what dialogue is displayed, etc. Here's how it works.

Encounter Logic (aka map logic) exists in one of two places: either in the Encounter Layer itself, or inside of C# coded "chunks". Let's look at the Encounter Layer first here's the first bit of logic for a Simple Battle contract.

Configured Encounter Logic

Each bit of logic (aka trigger) is made up of a message that it listens to. A conditional that must evaluate to true in order to fire the remainder of the logic. If that passes, then it transmits a signal to a list of 0 or more Encounter Objects (almost always Activate). And a list of 0 or more "Results" or commands that execute. This can be anything like tell a dropship to liftoff, play some dialogue, issue some orders to the AI. 

Name - Friendly name to tell the designer what this trigger is going to do. I think the name Ambush Warning is why sometimes Darius sounds strategically inept. Ambush Warning could mean warn of an impending ambush or it could mean warning we are being ambushed right now. Because of this sometimes Darius says something like: Heads up commander there may be additional hostiles in the area... You mean like the ones shooting me?!?! Thanks Darius!

Only Trigger Once - checked - So once this trigger fires, it doesn't fire again. OnVisibilityAcquiredBlip - This is the message it listens to. The designers are allowed to listen to any message in the game though some might not make sense (like OnAppShutdown). Any time there is an acquired blip, we'll test the conditional to see if it's true.

We have a GenericCompoundConditional which is a way for designers to link up multiple conditionals. This one is configured so that ALL the conditionals in the list must be true. The first is an ObjectInvolvedConditional configured to look at units tagged with ambush_units. The second is to make sure the DestroyAmbushers objective is not in progress yet. 

If both of those things are true, we fire the logic of this trigger. We activate the DestroyAmbushers objective, then we fire off the Results which starts some dialogue and queues an audio event to signal that the ambushers have arrived.

Being able to conditionally listen to different messages and fire off different results lets the designers do pretty much whatever they want. If there's something new they want to check against, I can code up a new Conditional. If there's something new they want to do, I can code up a new kind of Result

Another way encounter logic can exist is through custom written C# "chunks". Chunks are collections of related objects and logic bundled up into a prefab so designers can drop them into scenes, configure a couple of things and test it out. Let's take a look at the Dropship Extraction chunk. We usually make these after it becomes obvious that we're going to use it on more than one Encounter Layer.

Encounter Logic Chunk Prefab

In the Heirarchy on the left, you can see what's in this chunk. A dropship landing spot, a region, an objective to call the dropship, and an objective to load the dropship. In the middle you can see my horrendously ugly test scene along with the highlighted contents of the chunk. In the inspector on the right you can see some bits of configuration for the chunk itself. Spawn Lances When Landed will actually trigger the lances identified by RequiredTagsOnLances. Take Off Immediately will extract the units as soon as the last unit enters the region instead of waiting for the end of the round. Extract via Dropship says whether or not to actually use the dropship (or just disappear near a building or tunnel). This is a feature I just added otherwise I'd have called this chunk the Extraction Chunk instead of Dropship Extraction Chunk. :/ The individual objectives are then configured to the specific lance that's trying to get extracted. Maybe it's for the player units, or maybe it's for an allied lance.

Inside the chunk it listens to Objective Succeeded messages and checks things in much the same way as the encounter logic. Instead of a drag and drop graphical interface, it just uses C#.


private void OnObjectiveSucceeded(MessageCenterMessage message)
{
	ObjectiveSucceeded objectiveSucceeded = message as ObjectiveSucceeded;
	DropshipLandingSpotGameLogic dropshipLandingSpot = dropshipLandingSpotRef.GetEncounterObject(Combat.ItemRegistry);
	DropshipGameLogic dropship = dropshipLandingSpot.GetDropship();

	// If the CallDropship objective succeeded.
	if (objectiveSucceeded.ObjectiveGuid == callDropshipObjectiveRef.EncounterObjectGuid)
	{
		// and the dropship is off screen.
		if (dropship.currentAnimationState == DropshipAnimationState.OffScreen && extractViaDropship)
		{
			dropshipLandingSpot.LandDropship();
		}
	}

	// If the LoadDropship objective succeeded.
	if (objectiveSucceeded.ObjectiveGuid == loadDropshipObjectiveRef.EncounterObjectGuid)
	{
		DespawnUnits();

		if (dropship.currentAnimationState == DropshipAnimationState.Landed && extractViaDropship)
		{
			dropshipLandingSpot.TakeoffDropship();
		}
	}
}

Whew. This entry kind of got away from me and there's still tons more to cover with the Encounter Logic. Map/Encounterlayer relationship, Encounter Objects, Map Data, Applying Contract Data. That will have to wait for another day.

 

CWolf's Cool Mod

CWolf is a modder that ripped all my code apart, asked me all kinds of questions and built a really cool mod. It's called Mission Control and it allows other modders to build content that modifies existing contracts. Tthings like randomizing the spawn points, adding in new lances (sometimes friendly), and adding hooks for dialogue. Check it out!

https://www.nexusmods.com/battletech/mods/319

 

Tip from your Uncle Eck

When tackling any performance issue ALWAYS HOOK UP A PROFILER FIRST. Otherwise you'll spend time speeding up the wrong thing and it'll still be too slow. 
 

Links

4 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement