UnrealScript and Domain-Specific Languages

UnrealScript is really cool. I’ve been working on my Explosive Ammo Mutator for UT 2004, so I’ve gotten a pretty good crash course in Epic’s scripting language. (On a humorous side note, check out http://epicgames.com.)

First, let me preface by saying that I had experience with an older version of UnrealScript. Back in college, we hacked up the bots in the original Unreal Tournament for an Artifical Intelligence class. We implemented a learning algorithm, and it worked relatively well. Except the bot sucked. We couldn’t get it to do what we wanted because its own code was too complex. But it really did learn. Honest! Anyway, the point is that I had sorta forgotten how cool UnrealScript is.

You see, Epic didn’t just make a game. They built an entire robust, portable gaming engine, simulator, and toolset. The Unreal engine actually has a built-in virtual machine that runs compiled bytecode, a technique identical to that used by Java and .NET. For those of you out there who aren’t geeks, this lets the game run on a mulitude of platforms, from the XBox to the PS2 to my PC, with very little binary changes. UnrealScript is the language that is compiled into that bytecode, and it is no ordinary language.

UnrealScript has a C-like syntax, making the code pretty easy to read for Java or C# developers. However, it is completely object oriented. There are no global functions, and every object extends from the base Object class. In the version I used in college, there were no static methods; although that feature has been added since the old days. But the cool thing about UnrealScript is that it is a domain specific language. That means that it has language features specifically designed to make programming a video game easier. UnrealScript supports several features of note, such as time, properties, and networking; but my favorite is its language-level support for states.

In most game programming, much of the game world is modelled as a state machine. Different types of objects have various states that they might be in, and they respond to stimuli differently depending on that state. Often, this type of thing is modelled using a switch/case statement or the ilk. In UnrealScript, states are first-class langauge features. Take a look at this code snippet from the ExplosiveRocketAmmoPickup class from my exploding ammo mutator. (I’ve left out some of the nitty-gritty for clarity.)

function TakeDamage()
{
	GotoState('Exploding');
}

state Exploding
{
   function TakeDamage() {}

Begin:
   DoExplosion();
   SetRespawn();   
}

I have declared a function called TakeDamage() that will be called by something whenever my ammo pickup gets hurt in some way, either by somebody shooting it or an explosion going off nearby. It is declared at the global scope level; when it runs, it tells my object to transition to the Exploding state. Just below is the declaration of that state. In the Exploding state, I redifine TakeDamage() to do nothing at all. The semantics are that, whenever the pickup is in the Exploding state, the TakeDamage() defined within that state is called; otherwise the global version is called. For my object, this means that once an object is exploding, it won’t be able to take more damage and somehow explode again. That makes sense, right? (Actually, it’s even easier than that. UnrealScript allows you to write ignores TakeDamage; instead of declaring an empty function.) When my state is entered, it runs the code following the Begin label, which causes the object to explode and then respawn.

Pretty cool, huh? In this way, you can build up complex state machines using a simple syntax. But it gets better. Much like you can inherit methods and fields from parent classes, you can inherit states from parent classes. In the example above, the SetRespawn() method actually causes my object to transition to a Sleeping state defined in the parent class.

But what if I wanted to modify the states of a parent object? No problem. In the next example, again taken from my mutator, I had to extend the projectile class used to model mines for the Onslaught game type. The mines in Onslaught are special in that they sit around and wait for an enemy to walk by, and then they chase him until they catch him and blow up his ankles. The problem, of course, being that they don’t blow up until they are triggered by a player. So our ammo box would blow up, and then all these mines would potentially sit around on the ground forever.

state OnGround
{
   function BeginState()
   {
      if (!self.deathTimeSet)
      {
         self.deathTime = self.Level.TimeSeconds + RandRange(1.0, 7.0);
         self.deathTimeSet = true;
      }
      
      super.BeginState();
   }

   function Timer()
   {
      if (self.Level.TimeSeconds >= self.deathTime)
      {
         self.BlowUp(self.Location);
      }
      else
      {
         super.Timer();
      }
   }
}

The OnGround state is defined in the parent class, and represents when a mine is sitting on the ground waiting for somebody to pass by. I’ve overriden it here, along with two functions within it. The BeginState() method gets called whenever the state begins, that is, whenver the mine has landed on the ground. I set a random point in time in the future at which the mine will explode, and then I call the superclass’s implementation. The Timer() method gets called whenever a pre-set timer goes off. In this case, the base class sets this timer to check if anybody is nearby so that the mine can go scurrying after them. I’ve overriden it to first check whether the mine has expired and explode it if it has. If it hasn’t, I merely call the superclass’s Timer() method so that the mines will go scurrying along appropriately.

The cool thing of this being, of course, that this code only gets called when the OnGround state is active. I can muck with just a single portion of the state machine without having to touch anything else. It’s a sort-of double-virtual dispatch for methods. You can also inherit states from other states declared in your class, but I’m not going to go into that right now.

The net result of all of this, besides the cool features, is that domain-specific languages can be an amazingly powerful tool for writing complex systems. If you create a language in which to more efficiently express a solution, then expressing those solutions becomes much easier. All too often, modern software engineers suffer from Hammer Syndrome. The huge popularity of Java, and more recently C#, in the mainstream often blinds us to the potential of a different language. An altered paradigm can dramatically reduce the difficulty of coding a working solution. Lisp is often dismissed as an academic language, or people are frightened by the large number of parenthesis. ECMAScript is virtually always relegated to web browser scripting; or worse, it is thought to be synonymous with the DOM. Different languages allow you to express your existing thoughts in a different way, and they often lead you to completely new thoughts. If you only know one language, you really need to learn another one or five.