Blast from the past
I originally drafted this blog post a year or so after Splinter Cell: Blacklist launched, and I was still at Ubisoft (around 2014)!
I decided to hold off publishing it since there weren’t any active Splinter Cell projects at the time, and I always figured I’d come back to it and hit publish at a later date.
And well… here we are, very exciting stuff to come from Ubisoft Toronto and their partner studios π
I’ve left the blog post largely as I wrote it back then, and in hindsight it’s pretty funny to think that I was working in Unreal 2, on a game mode that was inspired by the Gears Of War Horde game modes, years before I made the move to The Coalition to work on Gears!
Extraction (Charlie’s Missions)
When working on Splinter Cell: Blacklist, we had guidelines for the numbers of AI spawned.
So a heavy AI might be worth 1.1 bananas, and a dog 0.7 bananas, with a total banana budget of 10. The numbers roughly mapped to CPU budgets in milliseconds, but the important thing really was the ratio of costs for the various archetypes.
It’s a tricky thing to manage AI budgets across all the game modes and maps, and probably something that the Design department and AI programmers lost lots of sleep over.
Where it got particular tricky was in the CO-OP Extraction game mode.
The game mode has “waves” (a round of AI that is finished only when all of the enemies have been dealt with).
Within waves there are sub-waves, where AI have various probabilities of spawning, and these sub-waves can start either based off numbers of take downs in a previous sub-wave, or based off time.
So the player, for example, could just happen to let the most computationally expensive enemies live in a sub-wave, take out all the computationally cheap enemies (with tactical knock out cuddles, of course), and the next sub-wave could spawn in and we’d blow our AI budgets!
The team building the co-op maps in our Shanghai studio were great at sticking to the budgets, but this variation in the spawning for AIs was obviously going to be very hard to manage.
Having our QC team just test over and over again to see if the budgets were getting blown was obviously not going to be very helpful.
XML and C#/WPF to the rescue
Luckily, one of the engineers who was focused on Extraction, Thuan Ta, put all of the Extraction data in XML. This is not the default setup for data in the Unreal engine, almost all of the source data is in various other binary file formats, but his smart choice saved us a lot of pain.
It made it incredibly easy for me to spend a week(ish) bashing together this glorious beast:
A feat of engineering and icon design, I hear you say!!
Certainly can never be enough Comic Sans in modern UI design, in my opinion…
What is this I don’t even
Each row is an AI wave that contains boxes that represent varying numbers of sub-waves.
The sub-wave boxes contain an icon for each of the different AI types it might spawn, assuming the worst case (most expensive random AI) for that sub-wave (heavy, dog, tech, sniper, regular with a helmet, etc):
The number at the top right of each sub-wave box is the worst case AI cost that can occur in that sub-wave, and it can be affected by enemy units that carry over from the previous sub-wave:
So, for example, if sub-wave 1 has a chance of spawning 0-2 heavies, and 1-3 regulars, but only to a max number of 4 enemies, the tool will assume 2 heavies get spawned (because they are more expensive), and 2 regulars get spawned to estimate the worst cost AI for the sub-wave.
If sub-wave 2 then has a trigger condition of “start sub-wave 2 when 1 enemy in sub-wave 1 is taken out” (killed, or convinced to calmly step away and consider their path in life), then the tool would assume that the player chose to remove a regular in sub-wave 1, not a heavy, because regulars are cheaper than heavies.
Following this logic, the cost of each sub-wave is always calculated on the worst cases all the way to the end of the wave.
Long lived
Sometimes you’d want to know, at a glance, which enemies in a sub-wave can live on to the next sub-wave.
If you mouse over the header part of a sub-wave (where the orange circle is below), all the units that are created in that sub-wave are highlighted red, and stay highlighted in the following waves indicating the longest they can survive based off the trigger conditions for the following sub-waves:
So in the above case, the heavies that spawn in Wave 15, Sub-wave 1 can survive all the way through to sub-wave 3.
This is important, because if sub-wave 3 was over budget, one possible solution would be to change the condition on sub-wave 2 to require the player to take out one additional unit.
Also worth pointing out, the colour on the sub-wave bar headers are an indication of how close to breaking the budget we are, with red being bad. Green, or that yucky browny green are fine.
The colour on the bar on the far left (on the wave itself) is representative of the highest cost of any sub-wave belonging to this wave.
So you can see at a glance if any wave is over budget, and then scroll the list box over to find which sub-wave(s) are the culprits.
Listboxes of listboxes of listboxes
There’s about 300 lines of XAML UI for this thing, and most of it is a set of DataTemplates that set up the three nested listboxes: One containing all the waves, a listbox in each wave for the subwaves, a listbox in each sub-wave for the AI icons.
Each of the icon blocks has its own DataTemplate, which just made it easier for me to overlay helmets and shields onto the images for the different AI variants:
<datatemplate x:key="EAIShieldedHeavyController_Template" datatype="{x:Type local:Enemy}"> <grid> <rectangle fill="Black" width="30" height="30" tooltip="Heavy + Shield"> <rectangle.opacitymask> <imagebrush imagesource="pack://application:,,,/Icons/Heavy.png"> </imagebrush></rectangle.opacitymask> </rectangle> <rectangle horizontalalignment="Right" verticalalignment="Bottom" fill="Green" width="15" height="15"> <rectangle.opacitymask> <imagebrush imagesource="pack://application:,,,/Icons/Shield.png"> </imagebrush></rectangle.opacitymask> </rectangle> </grid> </datatemplate>
System.Xml.Linq.Awesome
Probably goes without saying, but even in a horrible hard-codey, potentially exception ridden hacky way like the way I was using it in this application, using the XDocument functionality in Linq makes life really easy π
I definitely prefer it to XPath, etc.
Forgive me for one line Linq query without error handling, but sometimes you’ve got to live on the wild side, you know?:
_NPCTemplates = SourceDirInfo.GetFiles("*.ntmp").Select(CurrentFile => XDocument.Load(CurrentFile.FullName)).ToList();
And with those files, pulling out data (again, with error/exception handling stripped out):
foreach (XDocument Current in _NPCTemplates) { // Get a list of valid NPC names foreach (XElement CurrentNPC in Current.Descendants("npc")) { List NameAttr = CurrentNPC.Attributes("name").ToList(); if (NameAttr != null) { // Do things!! } } }
Conclusion
Although it’s nothing particularly fancy, I really do like it when programmers choose XML for source data π
It makes life really really easy for Tech Art folk, along with frameworks like WPF that really minimize the plumbing work you have to do between your data models and view, as well as making very custom (ugly) interfaces possible using composition in XAML.
Beats trying to create custom combo boxes in DataGrids in Borland C++ at any rate π
Also, Comic Sans. It’s the future.