CodeSOD: Do You Think This is a Game?
We've passed Christmas and made our way through a Steam sale with our wallets mostly intact, and now most of us have a pile of games that we'll probably never actually play.
Game programming is hard. Setting aside the "cultural" problems in the industry- endless crunches, compensation tied to review scores, conflicts between publishers and studios, and a veneer of glamour over unglamorous work- the actual work of developing a game is a hard job.
Building a game engine is even harder. Not only do you have to build highly performant code, you have to build a system flexible enough so that game developers can build a game on top of it. You need to provide a set of high-level abstractions that make it easy for them to build a game, and this is where the problems come in.
For example, I went through a brief period of playing Frozen Cortex, an interesting approach at a turn-based sports game. I was stunned at how badly it performs, though. Weirdly, it's not during gameplay that performance stinks, but when staring at the menus. I was puzzling over this for some time, when Anonymous sent us a message.
You see, Frozen Cortex is build on the Torque engine, and our anonymous submitter is working on a different game that also uses the Torque engine. And they've encountered a few" special warts.
First, take a look at this code:
a = getWords("The quick brown fox jumped over the lazy programmer.", 3); b = getWords("The quick brown fox jumped over the lazy programmer.", 3, 5); c = getWords("The quick brown fox jumped over the lazy programmer.", 3, 2);
getWords is a substring function, taking a string, the starting index and the ending index. Now, what would you expect to happen if the ending index comes before the starting index? Would you expect it to throw an exception? Well, bad news- TorqueScript has no concept of exceptions. It just crashes the entire game. This is a great tool to teach you how to be a better defensive programmer.
Now, you could argue that prohibiting exceptions is a pretty clever optimization- exceptions and stack unwinding are expensive operations. We have to wonder though, because here's a performance comparison between calling a function and in-lining the operation:
==>%r=getRealTime();for(%i=0;%i<999999;%i++)%d=getMin(getRandom(),getRandom());echo(getRealTime()-%r); 7040 ==>%r=getRealTime();for(%i=0;%i<999999;%i++)%d=(%a=getRandom()<%b=getRandom()?%a:%b);echo(getRealTime()-%r); 448
If you do a little arithmetic, it's 15.7 times more expensive to call a function rather than in-line it.
The final stinger Anonymous wanted to share with us was this:
TorqueScript's "variables", behind the scenes, involve a lookup table to match a variable name to its stored value. But this isn't done quite right - even when a function has exited, its local variable names are never cleaned up. So the lookup table simply grows and grows over time" until, eventually, TorqueScript's already poor performance reaches unteneble levels of slowness. Essentially, it's a memory leak in the scripting language itself. You can restart the game to clear the lookup table and bring it back to its original level of "speed", or you could just let Torque eventually crash.
Now, this is a bit unfair. Torque was released by a small company, and after a few years of trying to find some market traction, became an open source product. I'm sure for the specific cases the developers were shooting for, some of these trade offs make sense. We certainly wouldn't expect to see anything so strange in a big-budget game engine, right?
Well, Rich D is trying his hand at making a few X-COM 2 mods, and thus is exploring the quirks of Unreal Script for Unreal Engine 3 (or, more precisely, the highly modified version of the Unreal Engine used by X-COM 2).
He's found a few quirks of his own:
1) Basic data structures that we take for granted, like maps and sets, don't exist.
2) If you're in a static function, you can't call another static function that takes a delegate as a parameter. Even if the delegate is also a static function.
3) If you're looping over an array of structs, for and foreach have different behavior
4) If a variable is None (Unrealscript's equivalent of null) and you try to access something in it, that only results in a warning and your function continues executing anyway. No need to declare "On Error Resume Next"; for convenience
Again, many of these tradeoffs probably are there to optimize for performance, and can be avoided by defensive programming. But take a look at these two code blocks:
static function bool IsUnitValidForCrossTrainSlot(XComGameState_StaffSlot SlotState, StaffUnitInfo UnitInfo) { local XComGameState_Unit Unit; Unit = XComGameState_Unit(`XCOMHISTORY.GetGameStateForObjectID(UnitInfo.UnitRef.ObjectID)); if (Unit.IsASoldier() && !Unit.IsInjured() && !Unit.IsTraining() && !Unit.IsPsiTraining() && class'CrossTrainUtilities'.static.GetCrossClassAbilities(Unit).Length > 0) { return true; } return false; }
versus:
static function bool IsUnitValidForCrossTrainSlot(XComGameState_StaffSlot SlotState, StaffUnitInfo UnitInfo) { local XComGameState_Unit Unit; local array<SoldierClassAbilityType> Abilities; Unit = XComGameState_Unit(`XCOMHISTORY.GetGameStateForObjectID(UnitInfo.UnitRef.ObjectID)); if (Unit.IsASoldier() && !Unit.IsInjured() && !Unit.IsTraining() && !Unit.IsPsiTraining()) { Abilities = class'CrossTrainUtilities'.static.GetCrossClassAbilities(Unit); if (Abilities.Length > 0) { return true; } } return false; }
Tracing through the logic, you'll see that both of these are logically equivalent. The only difference is that first example accesses the array in the if condition, while the second stuffs the array into a local variable. Which" as it turns out, is required. If the array doesn't get put into a local variable, the entire contents of that array are wiped.
Game programming is hard, but sometimes, it seems like game engines make it actively harder.