Monday, January 28, 2019

battleMETAL - And Yet, Somehow it all works - part 3 - Animatics

Quite an animated conversation

One of the feature’s I wanted to exist in battleMETAL was that of ‘animatics.’ For background, an ‘animatic’ is like a glorified slideshow. These are usually defined as a series of static images displayed in a sequenced order - like a super slow animation. Why would battleMETAL need these exactly? Why for character transmissions of course. I wanted characters to be able to talk at the player, or about the player to other characters in a way that player could visually see and understand. Radio transmissions that don’t show some sort of image are hard to follow in the ebb/flow of a video game. The concept of a ‘radio operator character’ for players to interact with is also a long-established mechanic in gaming.


Part of the original scope of battleMETAL was that the player won’t do any interacting with characters outside of combat. This was mainly due to time and resource constraints, but also because of the story and who the player is in this world. However, the player still needs some things to go off of when playing the game, so I felt that I could implement workable animatics to cover this gap. The system I finished is ‘good enough’ but probably could use some refactoring to make it ‘best’.


I started with CSQC, the animatics being an entirely client-side event, in my mind. I created a short set of api functions to handle the flow of the animatic system. The overall design is something along the lines of:
 

    Receive event from server
    Load animation file
    Validate file
    Setup playback variables
    Render frame 1 
    Render next frame
    End playback


The kick off is the server sending a command to the client to begin an animatic event. Quake was designed from ground up as a client-server game even in single player. Single Player in vanilla Quake is just a local game server with a max player count of 1. So even though battleMETAL only has 1 player in its server, the code still treats that player like any other remote-connect client in the code, which I think is a good thing. On the server, I created a custom map object, event_animatic that can be triggered by player touch or by other map objects, which sends a command to the target client to begin the animatic.


When the player’s CSQC receives the command, in this case changing the player’s “state” variable to _ANIMATIC, the CSQC begins the playback of the animatic. First step is loading the file. I decided that storing animatics in plaintext files was super handy both for readability and performance. The game engine doesn’t need to keep possibly dozens of animatic scripts in memory during gameplay, and because animatics are a low-delta event with no read/write commands, having the game load them from text files seemed like a good approach. I’ve always enjoyed the JSON syntax for data storage, considering it a better alternative to XML. Leveraging the Darkplaces source port’s ability to parse text files, I whipped up a crude JSON-style parsing function. Besides, its always fun making your file extensions.

So in test.anim we see the following:

{
  'music' : ,
  'nomusic' : 0,
  'backimg' : ,

  'trans' : ,
}

The first { } is always the ‘metadata’ tags for the animatic file. Music is which sound you want to play when the entire animatic starts up, and this will play until the end of the animatic. NoMusic will shut off any CD music playing in the background ( don’t worry, it’ll resume the stopped music when the animatic finishes). BackImg is if you want 1 background image to be rendered beneath all subsequent image frames. Finally, Trans is for ‘transparency’, setting the global alpha value of the animatic during playback. These values are then stored in a 1-dimensional string array labelled simply named ANM_META_DATA. Once the metadata is loaded the text parser expects frame data to come next. Frames are defined by { } as well, and there’s no true limit, for the sake of brevity I imposed a 20 frame max (do you really need moar?).


{
  'image' : gfx/hud/target_box.png,
   'pos' : 0.79 0.225,
   'size' : 0.05 0.05,
   'sound' : sound/anim/t1m4_lineb.ogg,
   'text' : ,
   'alpha' : 0.85,
   'color' : 1 0 0,
   'text_color' : 0 0 0,
   'time' : 3,
}

Image is which image you want this frame to display, and you can set it to null / empty.

Pos is ‘screen position’ important: this is done in percentage of screen to make sure that the coordinates are screen-size agnostic.

Size is the size of the image. important: this is done in percentage of screen to make sure that the coordinates are screen-size agnostic.

Sound is the sound file you want to play on this frame. Its non-looping, and is not clipped by any sound on the next frame, so playback overlap is a risk here.

Text is any text you want rendered, I have this hardcoded to be rendered at the bottom of the screen and centered.

Alpha is the transparency of the frame.

Color is the color-tint you want to apply to the frame image and text. RGB values are in vector format between 0.001 - 1.0 for each color.

Text_color same as Color but overrides the color for the text.

Time how many seconds to render the frame for.


In addition to these tags, the code automatically does a fade-in/fade-out effect for each frame but I’m still on the fence if this is necessary or not. When the text parser reads these tags into the code, the frame reference number is used as the primary key for a series of arrays. I’m not particularly happy with this solution but I’ve chalked it up to the roughness of Quake C more than anything else….

string ANM_FRAME_IMG[20];
string ANM_FRAME_SND[20];
float ANM_FRAME_TIM[20];
vector ANM_FRAME_POS[20];
vector ANM_FRAME_SIZE[20];
string ANM_FRAME_MSG[20];
float ANM_FRAME_ALPHA[20];
vector ANM_FRAME_COLOR[20];
vector ANM_FRAME_TXT_CLR[20];


Did I mention that any sort of collections are non-existent in Quake C?

The outcome for all this is a simple but working system for achieving cutscenes and character transmissions for battleMETAL. There’s still some refinement to be had mostly due to a lack of use and testing of the code, but as it stands I’ve tested that the code works at all.

Monday, January 21, 2019

battleMETAL - And Yet, Somehow it all works - part 2 - HUD

Heads up!?

Now that we’re sort of familiar with CSQC and what its about, we can take a look at the HUD for battleMETAL. There were 2 distinct phases to arriving at the HUD code that is now in the game. The first step was expanding the GUI functions I had created for the in-game menus that we saw in last the article. battleMETAL’s DNA is western mech sims of the 90’s, and that genre of games loved its HUD mechanics.


a HUD from Earthsiege 2


It seems in hindsight that mastery of reading a mech’s HUD was integral to the overall gameplay experience of a mech sim, given how much information is being sent to the player. One of my opinions as to why mech games lost market share over time was their built-in complexity that scares away newcomers, much like how Starcraft II today.


My first attempt at a HUD system was to take the generic GUI functions I had made, and craft a single HUD for each mech. The entry point for the HUD system was and is a single function call in CSQC_update_view(). I pass the player unit type to the client, and if that unit type is ‘mech’ then it runs the hud_frame() function. In the first system, I created HUDs as entity objects in CSQC, believing it to be the easiest way to hold data and functions for each HUD. You can kinda see the madness here on this github link to the battleMETAL project. Each HUD object implemented the same ‘soft’ interface of each hud element function, along with an initializer function that setup each object.


Now, in a more modern engine or code base, this isn’t exactly a bad idea. A proper class object for each HUD would be a fine way of rendering the HUD. Over in Quake C land, I was not so fortunate - there’s only 1 object close to being a class, the entity, and we all know now they’re not really the same. This attempt ended up repeating a ton of boiler plate code, and overall was too unwieldy. Tacitly, I had made some out-of-scope assumptions about what the HUD should be able to accomplish as a system. It was good that I coded it in a direction towards a robust UI system, one should always code for universality. I realized later that the HUD didn’t need this universality, it didn’t need an open system for rendering layered UI graphics...it needed to be bespoke. Quake C’s limitations have a tendency to hone your design instincts to one-off solutions for each module.


The next step in coding the HUD system was to disabuse myself of trying to make an object-based, layered HUD system. Rather, I decided to reorient the design to being built up from simple functions. I realized that each mech HUD doesn’t really have unique functionality that would ever really differ from another HUD. That is to say, mech HUDs all contain the same information where the only differences are slight variety in presentation and position on-screen of the HUD elements.

It took about a weekend, but I refactored every single piece of HUD code. Rebuilt from the original pieces, I ended up with unique functions for specific pieces of the HUD. A few examples to explain what I’m getting at:

hud_renderEnergyMeter()


Each only deals with rendering a single type of HUD component. The method arguments for each also varies only by the information that each component needs for rendering. The responsibility for drawing the total HUD then shifts up to the main HUD function. This main function is named for the mech that it is supposed to go to, and I used a switch-case statement to determine which HUD is supposed to be drawn. When the player enters their mech, the server sends the mech’s id number to the client, and the switch-case statement selects the function by mech id.


Therefore, any given HUD main function becomes a short list of HUD component functions, the only important data that matters is the on-screen location of the HUD elements and the player data. This approach even allows a little bit of flexibility. To make more unique HUD elements, the code can either encapsulate the component in just the desired HUD or add it to the HUD function library which would then allow any HUD to use it if desired. I applied this principle at least once with the renderWeapon functions.



hud_renderWeapon1


hud_renderWeapon2


Both functions take the same information but render this information in a slightly different way. We can see that each weapon is rendered atomically this way which then also allows the designer to use both styles in the same HUD. In keeping with the modular approach, this entire set of code is only called by a single entry function renderHUDFrame() which keeps coupling between the main functionality and the HUD system loose. This reduces headaches in adding new features to either system, or when changing large pieces of either system. I had a decent amount of fun bringing the HUD system to life for battleMETAL, and I think the code reflects it. Adding new HUDs is straightforward and maintainable while troubleshooting existing HUDs wont outright break too much else.

Monday, January 14, 2019

battleMETAL - And Yet, Somehow it all works - part 1 - CSQC



CSQ-what?

It's hard to go into complete detail about this powerful module for Darkplaces (though I feel obligated to say this module is not exclusive to Darkplaces, and is available in other modern source ports of Quake such as FTE).

I probably already mentioned this earlier in the series, Client-Side Quake C, that is. Recall that Quake C compiles to file of bytecode called progs.dat. This file contains all of the custom game code that the Quake engine runs when the player starts up the game. Client-side Quake C (hereafter referred to as CSQC ) takes this concept and applies it to the client code during gameplay. What this means is that the programmer gets the full power of Quake C - spawning entities, File I/O, player state, player input and more; on the client-side. Want to load custom UI data but don’t want to have to broadcast over the network? CSQC. Custom sound effects played only for the specific client? CSQC. You get the idea.

One of the most powerful features CSQC has is the ability to draw images on the player’s Heads up display, or HUD. Like most features of the source ports of Quake, this is a series of api hooks that rely on the programmer to leverage effectively. The design ambiguity offers freedom however. Using the basic image draw calls, and some input tracking code, I was able to implement a basic Graphical User Interface (GUI) that runs during gameplay. I was also able to make a wonderful mech-style HUD that shows the player most of the information they’ll need to know when piloting the big stompy robots. This post deals mostly with implementing the in-game GUI which ironically is different from the Main Menu system.

CSQC has the following methods that define the skeleton of the system -

    CSQC_Init()

    CSQC_InputEvent()

    CSQC_UpdateView()

    CSQC_Parse_StuffCmd()

There are a few more, but these are outside the scope of this post. A quick breakdown of these functions. _init() is the first function called, specifically once the client connects to the server. Its when the CSQC context begins, semantically it's a great place to put any code you want to be initialized as soon as possible. The InputEvent() handles all player input during gameplay. It returns a boolean where TRUE means the input event was handled by CSQC and thus does not need to be passed onto the server. UpdateView() is the big one, this is where all render calls for custom GUI / HUD / anything else you want drawn get placed. You can also modify the view angles, location, and Field of View for the client as well. Lastly, StuffCmd() is used for handling text-based console commands that come from the server.

I built the in-game menus off a few basic presumptions: tracking player mouse input, the on-screen location of the mouse, a modest set of UI functions, and some variables to track the state of the menu. The trickiest implementations were the modest set of ui functions - end result wasn’t so modest. I was able to get things like lists to work; you can see examples in the Mech Hangar and Arming menus. However I never got ‘scrolling’ lists to work especially due to not feeling the feature was needed in the first place. I was able to ensure that the UI scaled to the player’s display resolution...though my approach was hacky.


I chose a resolution, 1240 x 960, and created all the UI’s to this scale including their screen coordinates. Next, I converted the size or location value into a percentage of (x / 1240), (y / 960). Then, these percentages are applied to the player’s screen resolution. Therefore, a UI element that is 75% of the total screen width will always be 75% of the total screen width regardless of the actual number that screen width is. Its brute-force, but it worked solidly enough to base a menu system on.

The core of any given menu is a set of functions that in a real language would be an ‘interface’, a contract of functions that each menu object would have to implement. Each menu has a _DrawFrame(), _Listener() functions. The menu system uses a switch() control and a global variable MENU_CHOICE to determine which _DrawFrame and Listener function to call. Let’s use the hangar example, we see mechHangarDrawFrame() and mechHangarListener(). Inside each of these functions, we put all the UI elements we’d like to draw for that menu; mechHangarDrawFrame() ends up looking like

     mechHangarDrawFrame(){

         local vector topleftroot;
         topleftroot = VIEW_ORG;

        drawpic(topleftroot, UI_DEF_BOX_512, VIEW_MAX, CLR_DEF_UI_HANGAR, 1,0);

        menu_hangar_MechDisplay(topleftroot + gui_percentToPixelRawVec('0 24'));
        menu_hangar_MechList(topleftroot + gui_percentToPixelRawVec('0 24'));
        menu_hangar_MechInfo(topleftroot);

         menu_hangar_MechFluff(topleftroot);

         menu_hangar_MechHPoints(topleftroot);

     }


drawPic() is a CSQC function for drawing 2D images onto the player’s screen. The other functions are made by me, and each renders a different part of the Mech Hangar GUI so that the end result looks like:

 

The second part is reading player input when the menu is active. For each menu, there’s a distinct function, and in our example its mechHangarListener(). This function contains all the input-related behavior that the code will call when the player inputs something.

     mechHangarListener(){
         mechSelectListener()
     }

Where mechSelectListener() is ‘listens’ for player input on a specific UI element. Quake C is fairly rudimentary, so the means for capturing player input in-context of a GUI was crude. For starters, the code I wrote breaks the screen up into rectangles. The specific Listener function will give the coordinates for the rectangle. When the player clicks the mouse, the listener code then checks to see if the mouse’s screen location is inside the rectangle that it defined. If the mouse is inside the rectangle, it returns the TRUE value, and again it's up to the listener function to handle this result. For lists of objects, such as the list of playable mechs, the ListListener function actually returns the index number of the item on the list. So if the player chose the second Light Mech, then the listener return value would be 2.

There are some drawbacks to such a simple menu setup; the biggest being items cannot be layered on top of each other. Each menu is only ever 1 ‘layer’ of UI elements deep. Another one is scrolling lists, I’m sure with some extra effort, getting the scrolling lists wouldn't be too bad. Finally, there is no way to drag-and-drop items (because there aren’t any ‘layer’ of menu). If I had more time for polish, I would have added some drag-and-drop functionality, especially for the Arming menu.



Monday, January 7, 2019

battleMETAL - And Yet, Somehow it all works!

And Yet it moves…

       So the last set of blog posts laid out some of the daunting challenges that I faced over the course of the project. Darkplaces / Quake when the project started in mid-2016 was fairly old, but only when working through battleMETAL did I realize how old. 


      However, all is not doom and gloom! In this next sections I will talk about the cool success stories. We will get into neat things like the various iterations of the player’s Heads-Up Display, or the game save system. Overall I’d say that in many ways, Quake gives you enough rope to trip yourself with...over a cliff...into the ravine. It's a shoddy metaphor but it is sort of true. Compared to modern game design sensibilities, Quake is clearly lacking but because it starts out so empty you end up with an odd freedom of action to implement your own stuff.

       I’m not going to say it was entirely smooth sailing, but I’d like to believe that because of the inherent freedom of the code base, I was able to get a lot of mileage out of such an old engine. I would also like to credit the folks over at insideqc. They have created a great repository of go-to examples and an extensive forum post history that answers most of the straightforward questions you might have about Quake C. If I ran into a situation where I couldn’t find an answer to my question online, I’d go to the fallback - reading Darkplaces source code over on github


So onto the showcase of features that I bashed into an engine whose core is 22 23 years old! (its 2019 now...d'oh)

Monday, December 31, 2018

battleMETAL - Why that was a bad idea, a terrible idea - Part 6 - Game Logic

A series of events;
 
    An important part of any game is level design and the game play loop of the level. One of the reasons I chose Quake / Darkplaces is because of how mod-friendly the map making process is...and how well documented it is. Back in my high school days, before learning to code, I had dabbled with making maps for Quake II. iD Tech games all generally used a tool called GTKRadiant for level creation, this was an all-in-one map maker for their games. The tool allowed users to created the full 3D spaces of a Quake level, place map objects like weapons, monsters, and player start zones. It also is where the logic of a map was created. These logic map objects were under sections called trigger_ and func_. They are responsible for making doors that move, elevators, changing maps, etc.


    There’s an old joke about iD games using the map design called a ‘monster closet.’ A monster closet is a part of the map where, behind a seemingly normal piece of wall, a monster or two is just sitting there. When the player gets close, or triggers a specific event, the wall opens up surprising the player and dropping monsters on them. This is a joke because if you look at the wall, it doesn’t look like it should logically have any space behind it and what exactly were the monsters doing behind the wall? Taking a smoke break?


    For this article, let’s focus on the how of the monster closet. The user in GTKRadiant would create a space behind the wall. They would place monster entities into that space. Next they’d turn the ‘normal’ wall piece into a door. The door, like the monsters, is another entity. Rather than relying on a specified model though, the door entity uses the map geometry its assigned as its model. Then the map designer would create another map object; trigger_once, point it to the ‘wall’. The Quake C would then say, “when the player entity touches trigger_once, open the door.” In this regard, we can say that the map file holds a lot of the game state and logic. 


    I believed that at least for map design and creation, Quake was a good move. Creating custom entities that the editor can place was fairly straightforward. GTKRadiant uses a text file to define all the possible map entities that can be placed before compiling the map into a format that Quake can read. By adding my own battleMETAL objects to this text file, I found it was easier to create maps specifically for battleMETAL - including the really cool aspects like Nav Points and Objectives ( we’ll get to those in a bit). Unlike AI which I covered previously, the map logic schema for Quake is surprisingly flexible. The map editor GTKRadiant uses a series of ‘definition’ files containing text, usually called entities.def. This file contains the definitions for entities to be placed on the map.


    The definitions must link to function names in the Quake C code. For example, the definition for an enemy AI looks like this in the .def file:
   
/*QUAKED unit_human_mech_sniper (1 0 0) (-14 -14 -20) (14 14 20) <spawn flags>
        <description text here>
    */
   
    The “QUAKED” is the head tag for an entity definition, the next piece is the function name “unit_human_mech_sniper” which should be defined in the Quake C code exactly the same wording. From what I can tell, the map compiles the entities and converts the function name either to a reference or compares the function name to all the functions in the code to find a match. Once the game finds the right function, it creates an entity and then calls this function on that entity. Whatever code is written in unit_human_mech_sniper is then run immediately.

    The next series of arguments are as follows: color in GTKRadiant, and the bounding box size. The color is for color coding the entity in the map editor, making it easier to tell entities apart from each other. As for bounding box size, that is also shown in the editor, but will also be passed to the engine when the entity is spawned. Quake C code can override the bounding boxes if it is needed. Spawnflags are an important piece of data for both the editor and the engine. Flags are a data storage concept used in general coding. They hold a series of “yes or no” sets in a single variable usually by using binary math. For our purposes here, every entity has a spawnflags field which can set up certain “yes or no” choices when the entity is created. The exact wording is up to the coder, so one entity might have DROP_TO_FLOOR for a spawnflag while another has START_INACTIVE.  The map editor will render these as checkboxes.


    Finally there’s a the descriptor section, it is here that normal text will be rendered in the map editor but has no relation to code in the game. This section is for any relevant info about creating, placing , and using a map entity. Altogether this system is fairly easy to understand and extend for modding. Debugging is nice as well, if engine can’t parse the entity from the map, it simply removes the entry and logs the result into the console.


    Now that we have context for the maps, we can do quick overview of the game logic itself. Every gameplay instance in Quake takes place on one of these maps files. The map is loaded up when the server switches to the desired map file. In Quake C this before  the Main() function is run, the main is the entry function for the Quake C. StartFrame() is any code you want to run before the engine runs all other code in that slice of server time. From here the game instance if open-ended, where the ‘end’ of a map can be determined in a few different ways. Vanilla Quake used round timers, kill counts, and map entities to end a game instance and load the next map. For battleMETAL I needed something a bit more sophisticated, seeing as how the game is descended from different DNA.


    battleMETAL is inspired by mech sims, in which part of the simulation was of more authentic military scenarios. This meant that the player is given a set number of ‘objectives’ to achieve before the scenario is ended. The objectives sometimes were destroying things, but other times it was just visiting a location on the map or protecting a base of buildings from an attacking enemy. So, to achieve a similar setup as I created some custom map objects - the Objective, and the Nav Point. I also extended the game engine to load in a text file called a “mission file.” The world map entity would have a variable that pointed to a specified mission file that was loaded to the player’s view when they connect to a map. The code I wrote also sends a unique objective ID for each objective on the map. This is linked to a list of objectives in the mission file. Together, they communicate objectives to the player both during the briefing and game play.


    Of all the things I found to be difficult with battleMETAL, this was surprisingly not one of those things. The configuration of GTKRadiant is fairly well documented and extending the functionality of making maps for my specific game turned out to not be a lovecraftian nightmare, unlike other parts of the project….well except for those 3D terrain meshes…<sigh>.

Wednesday, December 12, 2018

battleMETAL - Why that was a bad idea, a terrible idea - Part 5 - AI deux

Ah, that army...well played.

    Now that we have some context about how the Quake AI code works, we can look into why it was a no-go for battleMETAL. The first hiccup is how battleMETAL builds its AI entities. Like the player, AI entities are built using the same factory pattern code. The entity is actually a bunch of entities strung together - things like torsos, arms….legs. battleMETAL units are not animated as a complete distinct model. Because they are collection of parts, each part has its own animation. This breaks the original Quake C architecture and its reliance on the frame macros (as was explained in the last post).


    The unit piece with the most animation was the leg entity for mechs. However, the animations for the legs are not authoritative to other entity states. Therefore most code hooked up to leg frame animations is not decisive, but rather cosmetic; reacting to game events as they happen. Just having this one attached entity threw off the need for using Quake frame macros. A different solution was needed, and that was using a state machine. State machines are abstract computer designs that describe a series of ‘states’ for an arbitrary object. An AI object is a good candidate for state machines, where the state in the state machine could be a set of behavior or actions that the AI performs.


    Mind you, this was after a few other attempts to split the difference with Quake’s original AI implementation. The original code also had a set of generic AI behaviors that most monsters would invoke in about the same sequence. There existed functions like ai_walk, ai_run and so on, these were called on a per-frame basis and packed into a frame macro for the monsters. Additionally, other functions were used by the AI during gameplay to assist in calculating attacks, spotting the player, and moving around defined node entities.


    All of this existing functionality was put off to the side, to be reworked into the new AI design. Using a state machine, I broke down almost all the possible actions that an AI could take into 2 categories - states, and actions. I then declared a state to be a collection of actions. I credit my cousin Eamonn with the idea of making actions were atomic as possible ie each action as a function call would only do 1 specific thing. For example, the walk state would be the following actions:
    Scan for enemy
    Move to patrol node

 
And that would be just the walk state. If the AI spotted a valid target in Scan For Enemy, the the AI would transition to the Run state. The code marks each state with an integer, 0 = Walk, 1 = Run. Each entity then has a series of member function pointers to each state like mech.st_walk(). When the AI is created, the code tells the AI which version of of st_walk to use. In the case of mech units, they’d be assigned the mech_walk() function. 


    Then, the AI keeps track of which state it is in by having a variable called .attack_state, when the AI executes its code every frame it looks up which function to run by using its state variable. Once this paradigm was implemented, I took the idea to the next step - organizing collections of states into ‘behaviors’ or ‘unit types’. Tanks and Mechs would clearly need their own sets of state functions because of how different these units are. Using this approach I found it was easy to implement even more unit types like turrets and non-combat vehicles. Each behavior set, although using the same list of actions has their own logical flow for states, and changing the state they are on. The result is a fairly flexible and open-ended design for adding AI and its behavior.


    There are still some limits due to the foundation of this code being on Quake. A big one is AI movement. The Quake C uses an engine function to actually execute the movement of bots and no physics operations are carried out, this results in stiff movement of bots that doesn’t really look natural. This also results in bots running more slowly during gameplay, their per-frame updates are less because of how proportionally expensive it is for the game engine to move bots. In the end I’m ok with the outcome of the AI, despite its limitations I was able to add a few diverse unit types to the game. This diversity helped make the game more fun and engaging to play.


Monday, December 10, 2018

battleMETAL - Why that was a bad idea, a terrible idea - Part 4 - AI

Oh yeah? You and what army?
Another tarpit that consumed many months of dev time during this project was the AI. Quake was never known for its AI, which was only ever serviceable. The problem was thinking that the AI was extensible for the needs of battleMETAL. Quake AI is good enough for ‘basic’ FPS games. The code available allows for AI that can follow defined paths, attack and ‘fight’ with the player, and some limited hunting routines. All of this code is laid out in a few generic functions available to all AI and any monster-specific code is defined in that monster’s code file. Quake monsters are fairly capable of engaging the player in the tighter, more confined environments that Quake uses (big surprise). However I estimated that expanding level geometry size would not have had a great impact on the AI code, not realizing just how tightly engineered the AI code was.

The first wrinkle encountered was one part of how the AI code is implemented in Quake itself. Quake C functionality sits atop the C code of the engine, with limited access to engine-level code (by design). This equates to the Quake C doing the bulk of the AI work here. This means that AI execution is a tad slower due to the nature of Quake C being a parsed language versus running straight C code. I accepted these limitations due to lack of foresight and with the mistaken assumption that I could reuse a lot of the original code.

First hurdle turned out to be understanding the way the AI code was written. For example, in the m_grunt we see the following function:

                   void() army_stand1 [ $stand1, army_stand2 ] { ai_stand(); };

For me, that looked really wierd, and I had not encountered similar code in the project other than some simple animations for sprites. Let’s break this one down; what we’re looking at is a code macro. Code macros are like syntactic sugar, which is lingo for ‘shorthand’. Much like other abstract languages, such as math, have shorthand notations when writing large segments and code can also have shorthand. Most of the code shorthand is usually already baked into the language syntax itself, such as var foo = a_value. This is shorthand for the computer assigning the value a_value to the variable foo.

But what if you found yourself writing the same code many times, and would rather reduce this code to a simpler syntax? This is where macros come in. Macros can be user-defined, and customized according to the needs of the programmer. For Quake C, iD created the macro I mentioned earlier, for use with animations being synchronized to game code. Originally the animation frame rate was tied to the game’s frame rate, mostly because computing power was very limited back in 1996. It didn’t matter that the animation was tied to the game’s update rate because usually that update rate didn’t surpass 20FPS on most systems. The second part was tying game code to the animations themselves; what should happen when the Frame X of the animation is played?

So looking back to the example macro, this is what it means

        Army_stand1 is a function definition.

         The [ and ] are part of the Frame macro, this has 2 arguments, the frame number of the current frame and the next function to go to.

         $stand1 is the frame number of the first frame of the ‘stand’ animation.

         Army_stand2 is the name of the function that should be executed next after this function( army_stand1 )
 

        The { and } then define the function body for army_stand1, what code should be executed on this frame.

When dealing with models that have dozens of frames and multiple sets of animations, this is a clever way of organizing the overall flow of the entity’s animation and code. Knowing the schema for the macro, you can now read through a model’s game logic quickly and easily. There is a cost however.

The macro is more rigid than writing the code out yourself every time. What I mean by this is that the frame macro is laser-focused on solving the animation / code synchronization issue. What happens when I don’t need any code to run on a frame of animation? I still need to define the macro for that frame. This creates a problem at-scale when model animations start reaching the dozens or hundreds of frames. That was the main reason why I, once understanding what the macro does, ditched it in favor of my own approach.


I won’t go into super detail about all the attempts to bring the AI to life - there were 9 major runs of work to arrive at the solution I went with, spread across 3 distinct solutions. The 3rd solution was what ended up working, and even then I have ways it could have been improved. This topic will continue into at least a second post so I can discuss the working solution...