Scripting in Quake 2

Over the past two weeks I’ve been working on a game using Quake 2’s engine, Basilisk Xorsonim. The game part hasn’t come anywhere yet due to a lack of resources, so while I ponder how to motivate myself to make or acquire models, textures, sounds, music, levels, designs.. uh.. Well, everything for a game that should look at least half way decent, I’ve been working on the engine to make sure it’s as easy to work with as possible with decent pipelining, build system and, of course, code.

In general, Quake 2 is a great engine code-wise. I had to rewrite the build system, and I had to fiddle with configuring the fork I’ve been working off of to enable things like zip file support and PNG support. When all that was done, and I had reacquainted myself with the modifications I had previously (about a year ago) made to the engine, the first thing that stuck out to me was that it was extremely difficult – and quite annoying – to add, modify, or even delete entities in the game code.

Quake 2’s game library code is a bit of a mess, contrasting heavily with the clean and blissful server and client. If I had to guess, at least 3/4ths of it was written by the third party producing the game itself rather than id Software’s cast of geniuses. But it could be one of many reasons that it’s rather unclean, and more importantly: extremely hard-coded. Ignoring the fact that the game library is pure C (a great thing) and has no external scripting or ‘soft-modding’ to speak of, the design inside the game library barely allows for modularity or quickly iterating ideas in code. It feels quite rushed compared to everything else.

The obvious solution to a knife fight is to bring a shotgun. Or an anti-materiel rifle. There’s no kill like over kill, so what I’ve done to fix this has been embed a scripting engine and port all of the game code to MoonScript, simultaneously cleaning up the API along the way. Most of it was tedious rather than difficult, and I even started writing a Squirrel backend before having to throw it out because it refused to work under any circumstances.

So, everything’s run-time now. Animations, entity registration, etc. Entities have a table associated with them that can hold any arbitrary data, reducing the memory footprint of simpler entities with no real run-time cost. Entity fields are grabbed from the engine with a nearly constant-time lookup (calculated ahead of time) and engine functions are exposed mainly as-is, while taking advantage of Lua features like multiple return values. Here’s a sample of my port of monster_infantry from Q2:

SP_edict_test = (ent) ->
   return nil if deathmatch!

   export sound_pain1 = soundindex("infantry/infpain1.wav")
   export sound_pain2 = soundindex("infantry/infpain2.wav")
   export sound_die   = soundindex("infantry/infdeth1.wav")

   export sound_gunshot     = soundindex("infantry/infatck1.wav")
   export sound_weapon_cock = soundindex("infantry/infatck3.wav")
   export sound_punch_swing = soundindex("infantry/infatck2.wav")
   export sound_punch_hit   = soundindex("infantry/melee2.wav")

   export sound_sight  = soundindex("infantry/infsght1.wav")
   export sound_search = soundindex("infantry/infsrch1.wav")
   export sound_idle   = soundindex("infantry/infidle1.wav")

   e = edict_of ent
   self = :e

   -- Physics
   e.movetype   = MOVETYPE_STEP
   e.solid      = SOLID_BBOX
   e.modelindex = modelindex("models/monsters/infantry/tris.md2")
   e.mins       = {-16, -16, -24}
   e.maxs       = {16, 16, 32} = 1000
   e.mass   = 200

   -- Callbacks
   e.pain   = pain
   e.die    = die
   e.stand  = stand
   e.walk   = walk    = run  = dodge
   e.attack = attack
   e.sight  = sight
   e.idle   = fidget

   -- Init
   link_entity e.ent

   e.currentmove = move_stand
   e.scale = MODEL_SCALE

   start_walk e.ent


object_type SP_edict_test, "edict_test"

The scripting engine is about 920 lines of code at the moment. It’s as optimized as a vanilla Lua 5.3 embedding can be, and should significantly speed up development… when I figure out what to do about resources.

There’s a few things I can think of that this could be used for besides making development easier, such as:

  • ECS (Entity Component Systems.)
  • Soft server mods.
  • More dynamic/procedural AI.
  • Map-specific monsters/weapons/decorations/etc.
  • Randomizing values or behaviour per map instance.
  • Creating randomly generated weapons.
  • Advanced map scripting.
  • Dynamically generating animation sets.

So, hopefully I’ll finish this project some day.