A hobby blog covering topics like comics, video games, video game development, and tabletop gaming
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 is22 23 years old! (its 2019 now...d'oh)
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
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>.
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.
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...
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...
Monday, August 27, 2018
battleMETAL - Why that was a bad idea, a terrible idea - Part 3 - Collision Systems
About those multi-part models
Collision systems a core part of any video game engine. From the first version of Pong up to the latest games on any platform, these systems are responsible for processing impacts between game objects in the game space. In general, like most computer code, collision systems have to be built at some point because a computer does not intrinsically know when two ‘objects’ have hit each other. The complexity of these systems can vary wildly depending on how detailed the game wants to get for collisions and how complicated the game itself is. Quake’s collision system is fairly simple for a 3D FPS.
The collision system for Quake starts with a series of boxes, or specifically, axis-aligned bounding boxes (AABB). Woah there’s some jargon, AABB simply means that the collision model for a game object is a box (technically a rectangle because box proportions can be altered). The axis-aligned part means that this bounding box does not rotate. The model can change angles all it wants, but in the eyes of the collision system, it never rotates - this is an important distinction.
Here we see Quake Guy and his AABB. Whenever Quake Guy hits another entity, or is hit by entity, the game doesn’t check the Quake Guy model for this collision. Instead, the game sees if Quake Guy’s AABB was contacted; which is mathematically simpler and therefore a faster calculation. The downsides to simple AABB is the lack of detail in collision detection, this can be best seen when Quake Guy is standing on a staircase or slope. Notice how only a few corners of the AABB are touching the ground?
Let’s make one quick jump back to the previous post about models, specifically the models that make up a mech in battleMETAL. These entities use a move type called movetype_follow which tells the game to always keep the entity’s center as an offset from a code-defined center in game space. There’s one important detail here, entities with movetype_follow cannot have a collision AABB. As a restriction, this exists because the use-case for movetype_follow is for cosmetic models put on the parent entity - weapons, clothes, etc. For battleMETAL, this means that the various mech parts that are seen on-screen don’t actually have a link to the collision system. Hm, but battleMETAL requires a feature where mechs various parts can be blown off the mech.
The solution implemented is a bit obtuse to manages to satisfy the requirement and still work. The player seen on screen is a heavy composite of several code pieces and models. To start, the player doesn’t actually have a model bound to its entity structure. Rather, the visual components - the mech piece models - are all separate entities using the movetype_follow functionality to appear in the correct location during gameplay. From the perspective of the collisions system however, these pieces are ‘wrapped’ inside the player’s main AABB. When a player is struck by a weapon, the following is executed in the code:
- Determine the gamespace location of the hit, this is a 3D vector of X Y Z
- Query the gamespace location of every mech piece that the player-being-hit owns.
- Find the closest mech piece to the hit origin.
- Closest mech piece becomes the area that is ‘hit’.
There are some quirks to the written algorithm as well.
- Ignores the volume of each model piece on the mech
- An imprecision thats hard to detect
- Effects on mech design
Tuesday, August 14, 2018
battleMETAL - Why that was a bad idea, a terrible idea - Part 2 - Model Mishaps
Compromises were made
Models are a good enough place to start about things that went awry. The most complicated models so far in battleMETAL are the star units themselves, the metals. The game requires that certain game units have multiple hit locations that can be destroyed before the entire unit is killed. In the case of the mechs, the locations are the center torso, left and right torsos, left and right arms, and the legs. 5 locations total for every mech unit. To better communicate damage to players, it was decided that arms and torsos should be ‘blown off’ when the health of those areas reach zero. This translates to each mech piece its own disconnect mesh, like a lego piece.
Most FPS games don’t deal with complex vehicles, they are scaled to people-sized players and monsters fighting. The models for each of this units is usually 1 solid, animated mesh. Despite complexity of the model, the fact that model is 1 contiguous piece of geometry makes working with them fairly easy. Having five sub-models that make up one complete mech model gets heavy very quickly. It increases the number of files per game unit, makes coding for game units a bit more complex. More modern game engines can handle this complication using techniques like bone-based modeling. Quake was made slightly before skeletal animation became an industry standard. Half-Life is credited with bringing skeletal animation to the mainstream and Half-Life’s engine is a heavily modified version of the Quake 1 engine.
Darkplaces does allow for skeletal animation but it comes with some severe catches. The quake c code for Darkplaces can utilize bones but only on the client-side. Now why is this a problem? I think I mentioned client-side prediction as an issue in previous posts. The snag with this approach is that the bones are only rendered on the client’s side of the game code. If the bones are only on the client then the location in game-space of those bones becomes an issue due to discrepancies between server and client game states. If Darkplaces came with out-of-the-box client-side prediction for programmers to work with, then this might be less technical debt. As it stands, client-side prediction was left up to the individual coder for some...reason...and not every programmer is up to the task of solving a such a complex issue.
The other possible option for skeletal animation is found in the Quake 3 model format called md3. This model format was made for Quake 3, itself a multiplayer-centric, fast-paced, arena FPS from late 1999. Quake 3 also made an attempt to fix client-side issues of skeletal animation but John Carmack went about it in his own unique way. MD3 models can be given little ‘tag’ triangles (a triangle being made up of 3 vertices in the model). Each ‘tag’ would then be given a unique name within the model for later reference. The technical debt of this approach is how tags are organized. When making a model, the tags have to be made, pointing in the desired direction of orientation for the mesh that will be attached there. The tags themselves are not saved when the model is exported to md3, thus any subsequent work on the mesh will require remaking the tags in their specific location.
For Quake 3 models that only ever had 1 or 2 tags per model, the ‘tag’ method was an ok solution. battleMETAL however, breaks this through complexity of the models in the game. The solution I settled on isn’t exactly performant for multiplayer gaming (thankfully battleMETAL is more a single-player game), but it does work. Remember how every model is an entity in quake c? Entities can be given specified ‘movement’ types to reflect, in the engine, how the entity is supposed to move. Player entities have MOVETYPE_WALK which is for handling player movement and physics. Darkplaces expands this functionality by adding a new MOVETYPE_FOLLOW. This movement mode is for objects that cannot collide with anything, and is for entities that, well, follow another entity.
General use-case for movetype_follow in the context of Quake is things like, having the gun model that a player is holding reflected externally by having a gun entity set to movetype_follow. Using this, battleMETAL treats all mech model pieces as separate entities with the movetype_follow. The locations of the pieces are set in relation to the controlling player’s origin point. In this way, the requirement of having hit locations, and visible damage have been satisfied without resorting to time consuming or costly programming quirks. The code uses the Factory Pattern to turn a mech data file into a mech, including the construction of the model component entities.
battleMETAL was able to use some animation for the mech models in the end. The leg models for every mech are animated in the model creation program and exported to the md3 format when animations are complete. The quake c code then controls the playback of those animations. Although this means animations are comparatively ‘stiff’ when put next to modern games, they are sufficient for the project.
Models are a good enough place to start about things that went awry. The most complicated models so far in battleMETAL are the star units themselves, the metals. The game requires that certain game units have multiple hit locations that can be destroyed before the entire unit is killed. In the case of the mechs, the locations are the center torso, left and right torsos, left and right arms, and the legs. 5 locations total for every mech unit. To better communicate damage to players, it was decided that arms and torsos should be ‘blown off’ when the health of those areas reach zero. This translates to each mech piece its own disconnect mesh, like a lego piece.
Most FPS games don’t deal with complex vehicles, they are scaled to people-sized players and monsters fighting. The models for each of this units is usually 1 solid, animated mesh. Despite complexity of the model, the fact that model is 1 contiguous piece of geometry makes working with them fairly easy. Having five sub-models that make up one complete mech model gets heavy very quickly. It increases the number of files per game unit, makes coding for game units a bit more complex. More modern game engines can handle this complication using techniques like bone-based modeling. Quake was made slightly before skeletal animation became an industry standard. Half-Life is credited with bringing skeletal animation to the mainstream and Half-Life’s engine is a heavily modified version of the Quake 1 engine.
Darkplaces does allow for skeletal animation but it comes with some severe catches. The quake c code for Darkplaces can utilize bones but only on the client-side. Now why is this a problem? I think I mentioned client-side prediction as an issue in previous posts. The snag with this approach is that the bones are only rendered on the client’s side of the game code. If the bones are only on the client then the location in game-space of those bones becomes an issue due to discrepancies between server and client game states. If Darkplaces came with out-of-the-box client-side prediction for programmers to work with, then this might be less technical debt. As it stands, client-side prediction was left up to the individual coder for some...reason...and not every programmer is up to the task of solving a such a complex issue.
The other possible option for skeletal animation is found in the Quake 3 model format called md3. This model format was made for Quake 3, itself a multiplayer-centric, fast-paced, arena FPS from late 1999. Quake 3 also made an attempt to fix client-side issues of skeletal animation but John Carmack went about it in his own unique way. MD3 models can be given little ‘tag’ triangles (a triangle being made up of 3 vertices in the model). Each ‘tag’ would then be given a unique name within the model for later reference. The technical debt of this approach is how tags are organized. When making a model, the tags have to be made, pointing in the desired direction of orientation for the mesh that will be attached there. The tags themselves are not saved when the model is exported to md3, thus any subsequent work on the mesh will require remaking the tags in their specific location.
For Quake 3 models that only ever had 1 or 2 tags per model, the ‘tag’ method was an ok solution. battleMETAL however, breaks this through complexity of the models in the game. The solution I settled on isn’t exactly performant for multiplayer gaming (thankfully battleMETAL is more a single-player game), but it does work. Remember how every model is an entity in quake c? Entities can be given specified ‘movement’ types to reflect, in the engine, how the entity is supposed to move. Player entities have MOVETYPE_WALK which is for handling player movement and physics. Darkplaces expands this functionality by adding a new MOVETYPE_FOLLOW. This movement mode is for objects that cannot collide with anything, and is for entities that, well, follow another entity.
General use-case for movetype_follow in the context of Quake is things like, having the gun model that a player is holding reflected externally by having a gun entity set to movetype_follow. Using this, battleMETAL treats all mech model pieces as separate entities with the movetype_follow. The locations of the pieces are set in relation to the controlling player’s origin point. In this way, the requirement of having hit locations, and visible damage have been satisfied without resorting to time consuming or costly programming quirks. The code uses the Factory Pattern to turn a mech data file into a mech, including the construction of the model component entities.
battleMETAL was able to use some animation for the mech models in the end. The leg models for every mech are animated in the model creation program and exported to the md3 format when animations are complete. The quake c code then controls the playback of those animations. Although this means animations are comparatively ‘stiff’ when put next to modern games, they are sufficient for the project.
Subscribe to:
Posts (Atom)



