The Inner Product, November 2002
Jonathan Blow (jon@number-none.com)
Last updated 17 January 2004
 
Toward Better Scripting, Part 2
Related articles:
My Friend, the Covariance Body 
(September 2002)
Toward Better Scripting, Part 1 (October 2002)
 
Last month I proposed a scripting language that automatically maintains history 
about the behavior of its variables, so we can more easily write scripts that 
ask time-oriented questions. I implemented an array of frame-rate-independent 
filters for each variable. The filters converged at different speeds, and I used 
them to estimate the mean and variance of each value over time.
Target Applications
To experiment with the history feature, I chose three time-oriented scripting 
tasks. I'll run through them one at a time, discussing how they can be 
implemented with the history feature, versus how they would likely be done 
without the feature.
In this month's sample code there are working implementations of all three 
scripts. However, the scripting language is still very simple, because I wanted 
to keep the sample code minimal and easy to understand. The scripts use a 
command/argument syntax with many restrictions; you might think of them as being 
written in a high-level assembly language. Just keep in mind that the length is 
due to syntactic constraints, but the semantics are much more powerful. In this 
article, I will use pseudocode to provide clearer illustrations.
Scenario 1: Dance Dance Revolution Commentator
Dance Dance Revolution (DDR) is a game where you have to press buttons at 
appropriate times. The game gives you a health bar, which we can conceptualize 
as a scalar between 0 and 1. When the health bar reaches 0, you die. A 
commentator says things about your performance, like "You're doing great!"
The problem with current implementations of DDR is that the commentator's words 
are chosen based on the instantaneous value of your health bar. So suppose you 
mess up and come close to losing, but then you start doing extremely well, and 
you're acing all the moves and you feel really good. But then the announcer says 
"You're not following the music!" because, even though you hit the last 20 
targets perfectly, your health bar is still low. So the commentator bludgeons 
your feeling of satisfaction, and he just seems broken (becuse you were 
following the music perfectly when he said that!)
Really, we want the commentator to detect patterns in the player's performance, 
rather than relying on the instantaneous value of the health variable. Not only 
does this increase the accuracy of comments, but it expands the scope of 
available comments. So he can say "Great comeback! For a while I thought you 
were going to lose!", which would have been impossible before.
In the sample code, script1 implements such a commentator. The game engine 
provides an instantaneous "player_goodness" value, ranging from 0 to 1, based on 
how well you hit the last target. The scripting system automatically and 
invisibly filters this value over time. The script queries an estimate of how 
well you've been doing for the past 40 or so seconds, which it uses to output a 
Parappa-style "You're doing bad / okay / good / cool" message.
The script then asks for the derivative of the goodness and queries the 
derivative for a period of about 20 seconds. Along with the earlier goodness 
value, the script picks from a range of appropriate comments, using rules like 
"Was the player doing badly for a while, but now his goodness is increasing?"
The derivative is not directly maintained by the core system. When you ask for 
the derivative, the system approximates it by taking finite differences on the 
history slots for the variable in question.
In one interesting case, I wanted not only the mean value of the player's 
goodness, but the variance as well. I wanted the announcer to say "Great! You're 
unstoppable!" if you consistently perform well. One way to formulate this is to 
take many queries from the history, and ensure that they are all above some 
threshold. But it was more natural to formulate the question in terms of "Is the 
long-term goodness high, and has the goodness been stable?" 
All of this was easy to perform due to the automatic history-keeping. Listing 1 
shows a pseudocode fragment of the script.
How would a programmer typically implement this in the absence of a history 
feature? I think he would end up implementing an ad hoc averaging scheme 
somewhat like our history feature, but probably not so well-defined (not 
frame-rate independent, or not providing estimates of variance). Sometimes 
programmers don't think of maintaining running averages at all; they resort to 
storing the player_goodness value for each frame into a circular array and query 
the array. This gets extremely messy and almost never works properly, since a 
lot of weird code is needed to handle aliasing problems.
 
| Listing 1: recent_average = mean(player_goodness, 30); // Mean over 30 seconds recent_change = mean(derivative(player_goodness), 20); // Mean of derivative over 20 seconds if (recent_average < POOR && recent_change > IMPROVEMENT) { output("Keep going, you're starting to get it!"); } | 
Scenario 2: Mortar Vehicle for RTS Game
A mortar vehicle fires ballistic projectiles over obstacles. Because the 
projectiles are slow, they are only effective when the target's position at 
projectile impact time can be predicted.
Game programmers usually implement a mortar firing heuristic one of two ways. 
One way is to just fire at anything, and fire a lot. The mortar will miss much 
of the time, but at least there are a lot of cool-looking explosions. Recently, 
though, gamers have been clamoring for better AI; since the mortar's 
indiscretion in choosing its target is an obvious example of dumbness, it would 
be nice to avoid. The second approach is to fire only at stationary or very 
slowly-moving targets, but this heuristic will fail in many cases. Consider a 
soldier in a trench alternating between two gunning positions. Overall he stays 
in the trench, but his instantaneous velocity is high while he is switching from 
one position to another. If the mortar makes its firing decision while he is 
moving, it will not consider him as a valid target. Furthermore, a player can 
exploit the heuristic, say, by placing many units in a tiny but fast patrol 
route. The units are staying within a small area, so the mortar could easily hit 
them; but their instantaneous velocities are large, so the mortar doesn't 
consider them.
Prior to this scripting language project, I (and, I think, most programmers) 
would try to solve this problem with some kind of bounding volume. I'd create a 
circle of a fixed size centered on the initial position of each unit in the 
game. Whenever the unit crosses the border of the circle, I reset the circle to 
the unit's current position. But if a unit doesn't cross the border for some 
amount of time, it's considered "not moving much" and flagged as a mortar 
target.
On a full-game scale, there are a few ways to organize this computation. The way 
most like the real world is for each mortar vehicle to compute its own bounding 
volumes around all targets in the world. Unfortunately, this is O(n2) 
in execution time and memory usage, so we would like something faster. The most 
natural solution from an optimization standpoint is for each unit in the game to 
manage its own bounding circle. But then the implementation of each basic game 
entity becomes more complicated, all for the sake of one specific entity type 
that most of the game has nothing to do with. If you have to add this kind of 
uncompartmentalized handling for even a third of the entity types in your game, 
things quickly become a mess. This also means that the game is, in a practical 
sense, hard to expand; if implementing a new unit typically requires changing 
the core entity movement code, the system is just not very modular.
A compromise between these two approaches is to implement a separate system, not 
associated with entities in the game world, that serves as a "mortar brain"; 
there's only one of these, or one per team. The mortar brain keeps track of all 
the bounding circles, and the individual mortar vehicles query the mortar brain 
to make their firing decisions. From a software engineering standpoint, this is 
cleaner than the uncompartmentalized version, but it's more work to implement 
than either of the previous approaches. If a substantial number of the unit 
types in your game require supplementary systems like this, your game's 
complexity (and implementation time and number of bugs) will rise substantially.
All of this is an example of a common software engineering ugliness. There's 
probably a name for it in some pattern book, but I will call it overextended 
responsibility. The requirement for efficient computation of bounding 
volumes creates complexity that pollutes the entire system.
In the end, none of these bounding-volume schemes are very good. If the 
projectile travel time differs substantially over the mortar's range, a 
fixed-size circle will not be good enough to segregate possible targets. In this 
case, to avoid the n2 solution, we must maintain several bounding 
circles of various sizes for each entity; the mess gets bigger, and the firing 
heuristic is only accurate to the quantum between circle sizes, and it's hard to 
gauge the effect of such inaccuracy on gameplay and game tuning.
By altering the firing heuristic to use position history, many problems 
magically go away. We fire at a target if the ellipse representing its recent 
position variance is very small. Already we have a solution comparable to the 
bounding-circle version, but since the scripting system remembers histories for 
us, we get everything for free. We quantify the variance by looking at the 
ellipse's circumference and area, as discussed last month. Because our 
measurements are continuous, we don't have any quantization problems like the 
bounding circles had. We just ask whether the variance is small enough, based on 
the amount of time the projectile will take to get there.
Given many targets, some of which are valid, we still need to choose one to fire 
at. So we still have to deal with an O(n2) problem, but it's much 
simpler since there's no bounding data to manage. A "mortar brain" that sorts 
targets by variance is so simple it's almost not worth thinking of as a separate 
system.
Interestingly, using position history solves some problems that the bounding 
volumes don't help with. Suppose we want to fire at targets that are moving at 
reasonable speeds, but in predictable fashions. In other words, we want to do 
dead reckoning. Most games simply extrapolate the instantaneous velocity of the 
target. Suppose you have a vehicle that is generally traveling west, but is 
constantly zig-zagging in an attempt to be evasive. If you sample the 
instantaneous velocity, the mortar shells will land to the northwest or 
southwest of the vehicle, whereas any dummy can see that they should be targeted 
to the vehicle's west. With position history, we easily query for the vehicle's 
average velocity over an extended period of time, which leads us to fire at the 
right spot.
The sample code script2 implements this. A target is controlled by the mouse 
pointer, and a mortar vehicle attempts to fire at it. The mortar shells are 
color-coded: white shells mean that the mortar sees the target traveling in a 
coherent direction and is dead-reckoning it; red shells indicate that the 
target's movement is too erratic to predict, but it is remaining within a 
confined area, so the shell is targeted at the center of that area. If the 
target is moving erratically over a large area, the mortar will not fire.
You can still confuse the mortar by moving in large but obvious patterns, like 
large circles or other curves. That's because the current dead-reckoning is only 
linear. You could implement nonlinear dead-reckoning by making several queries 
of the mortar position and curve-fitting them; and with the finite-differencing 
operator mentioned in the previous section, the implementation could become 
easier than that.
Scenario 3: A Tailing Mission
Sometimes you want to know if one entity has been in proximity of another for a 
period of time. Maybe one entity is a proximity mine, or an unidentified cargo 
ship that you are supposed to scan in an X-Wing style space game. The 
fiction of script3 is that you are a Thief and you're supposed to follow 
a messenger at night-time long enough to get a good look at him and identify 
him.
This is a case where we want a primitive that operates on the entire history of 
its inputs. We want to say (difference_vector = 
the_whole_history_of(messenger_position - thief_position)), then ask "What is 
the typical length of difference_vector over the past 30 seconds?"
This is easy; we can define a whole-history version of subtraction that returns 
a variable whose mean is the difference of the arguments' means. Some simple 
math tells us that this gives the same result as if we had subtracted the two 
values every frame and built a history that way. So to ask our question, we just 
query the average of this result for 30 seconds, then take its length.
In some cases, like in the tick code that evaluates mission objectives, it would 
be just as easy and clean to find the difference vector between the thief and 
messenger every frame, and put the result into a variable that builds its own 
history. But suppose we're not writing tick code; we're writing one-shot code 
that happens when an event is triggered. Say, the messenger has a conversation 
with a third party, and the end of the conversation fires an event that asks 
whether you have been near enough to witness most of the conversation. With a 
whole-history subtraction, we perform a single query and we're done. Without 
history, we need to add tick code to the Thief that maintains a distance measure 
for the event script to refer to. So we need to coordinate information between 
two scripts that run at different times, which is the kind of spaghetti code 
that strangles you if too much of it builds up. You ought to be able to 
implement a simple event without having to modify the entity tick, right?
Without a history mechanism, we would have to implement this with a per-frame 
distance measurement built into the Thief tick; so in the event-driven case we 
must suffer the two coordinating scripts. Essentially this solution is a 
bounding-volume implementation like Scenario 2, but it's not as nasty because 
the volume only concerns two specific entities and the measurement is 
1-dimensional. Even so, this solution suffers from discontinuities. We would 
have a timestamp that gets reset whenever you are standing a distance greater 
than k from the messenger; our query would end up being something like 
"Is the current time, minus that timestamp, greater than the number of seconds 
required for success?" Success-or-failure becomes determined by a discontinuity 
that is hard for the player to intuit; if he steps a tiny distance over some 
invisible line, the timestamp resets and he probably fails, regardless of how 
well he did during the rest of the mission. The history solution is continuous 
with respect to the instantaneous distance, and has some forgiveness built in 
(you can make up for some poor performance by exhibiting some better 
performance). This is probably better default behavior for most game design 
cases. Though there are times when you do want a hard discontinuity, they're 
rare.
Conclusion
We've only just touched the surface of the huge pile of time-related features a 
scripting language could support. I hope I've encouraged you to think about the 
space of features available to you when designing your next language.
Even if you never make a scripting language, the history techniques described in 
this column are still very useful. It is not difficult to code them up in C++ or 
whatever your favorite language is; they won't be as transparent, but the 
problem-solving power is still there.