Tuesday, April 7, 2020

Mechanical Review: You can't go home again.

This post is an addendum to the fantastic blog post here:
       "Don’t remake an old game" by Eamonn McHugh-Roohr

Ask yourself honestly, what is the remake for your Old Favorite Game?


Is it a graphical bump? expanded player abilities? re-balanced item stats?

Regardless of your choice; you are not remaking a game. In fact, you have begun to make a new game. This new game might look like Old Favorite Game but, there is no possible way to actually remake this game.



The passage of time on game technology


The Old Favorite Game was beholden to incredibly contemporaneous things like:
  • processor clock speeds.
  • GPU clock speeds.
  • Memory caps.
  • Built-from-scratch movement and physics code.
  • Item stats either not refined enough, or balanced to clockspeed-influenced gameplay.
The Old Favorite Game upon final compile and shipment is sealed in stone. A result of decisions and code made for its time. This is an important perspective, because it plays into the next part: game-feel.

You will not be able to actually recreate Old Favorite Game's game-feel. This will happen in both explicit and subtle ways. The explicit comes from simply using a newer engine. The new engine will have different clock speeds, update cycles, memory management. Trying to achieve a straight port of Old Favorite Game to this new engine will. not. work.

The advancement of technology will also play into many design decisions you and/or your team will make; some subtle changes may be required because to do otherwise would be making work harder on yourself.

 

A short exposition on Game-Feel


Obligatory wikipedia entry: Game Feel
"the intangible, tactile sensation experienced when interacting with video games."
Big emphasis on intangible. What you experienced when playing Old Favorite Game is part of this Game-Feel. You can't adequately describe, neither can I, and worse still - you and I could potentially have a sharp divide in what we believe the Game-Feel is for this Old Favorite Game

A preliminary diagnosis of The Remakes fatal team dynamics


Already the vision of remaking Old Favorite Game has fractured. We haven't even reached the design decisions! Even if you assemble a team of mostly unpaid game fans to remake the Old Favorite Game, and big if you get them to mostly agree on the Game Feel, we run straight into the design brick wall. It gets a bit meta here, but its important for understanding the overall point I want to make.

The moment you or the team decide to alter Old Favorite Game's stats, game environment, or say move to a new engine - you've fundamentally altered the game.

You will not be able to actually recreate Old Favorite Game's game-feel. This will happen in both explicit and subtle ways. The explicit comes from simply using a newer engine. The new engine will have different clock speeds, update cycles, memory management. Trying to achieve a straight port of Old Favorite Game to this new engine will. not. work.

The subtle changes will come from you or the team. In the energetic frenzy of development, you will say "Why don't we 'fix' this?" or another goes "I always believe that the game should have done that". Whether you are willing to admit this or not; these questions are in fact creating a new game. This new game may look very similar to Old Favorite Game, but it will never be the same. The new game will offer new ideas to implement, new translations of mechanics to gameplay. Further and further you will wander from the narrow vision of remake Old Favorite Game.


This sounds sort of obvious right? It does, and it is, so why mention it? I am looking to help you - given the above likely scenario you should leave behind the notion of remaking the Old Favorite Game. This is a notion similar to an iceberg, small on the surface, but incredibly deep. The phrase itself is an attempt to check that all parties in the conversation merely agree with your idea of the game-feel of Old Favorite Game.

Instead of willingly becoming Sisyphus, the mortal doomed to forever push a single boulder up a hill - only for the boulder to roll back down again. You should start with what are the defining things you like about the game-feel of Old Favorite Game. What moments stand out? what aspects of the play-by-play kindle fond memories for you? Capture those. From these ideas will spring a new game.

Maybe the new game is a sequel in a franchise, maybe a fan game, but for pure pragmatism it should be your own creation. You will own this wonderful project, reaping a bunch of benefits including creative control.

A summary:

It is mostly impossible to simply remake Old Favorite Game

The process to do so would either be pointless, ie the amount of effort required to remake is equivalent with just making a new game .
Or
The new game will diverge sharply over development time from Old Favorite Game so much as to basically be a brand new game that may appear similar to the old.

best case: you make a fun, fresh, new game inspired-by the old 

worst case: you've wasted your time fighting heat-death itself to recreate something that never really existed concretely. You will be constantly disappointed as each build of the remake fails to spark your joy of playing Old Favorite Game.
 
Why narrow your game design vision so ruthlessly right at the critical start point! Would you claim that Old Favorite Game is the unassailable example of any game? You know the Old Favorite Game isn't even halfway perfect, you'll admit this readily. Don't use Old Favorite Game's perceived infallibility as a shield against new ideas, or interpretations.
 
A parting note: enjoy your memories of your old game and replay this old game if possible! 
it was a lot fun to play, after all!

But disabuse yourself of the notion that remaking this Old Favorite Game will ever bring those feelings back. They were special and unique for you, and functionally impossible to adequately recreate or experience. Instead, look to the future, bring forward what you loved about those games into your efforts.

Monday, March 23, 2020

Mechancial Review: Leg-destruction in Mechwarrior-style video games.


(a  BL-6-KNT Black Knight engages a EXC-B2 Excalibur in desperate melee, circa 2780...)


This post was started off a conversation I participated in on one of the mechwarrior discords, when the subject turned to balancing the Mechwarrior experience to that of the Battletech tabletop game, and to 'legging' an opponent (that is; destroying one or both of a target's legs in the Mechwarrior games).

I asked:
should legging matter?  

Is legging in a PvP game fun? is it fun to win via legging?
Is it ok to lose because of legging?
I don't think it is. Try applying legging to most other FPS action games, and it gets annoying, quick.
If a player is accurate enough to target someone's legs, then making the target even slower is just feeding a run-away-winner scenario.


and one of the responses really stood out:
You’re comparing quite different genres there. Those are FPS games, this is a vehicular combat simulation game.
Stop thinking like an MLG and start thinking like a hardcore simmer :wink:
Also, using that logic, maybe we should remove arm destruction and weapon critical hits since if things like that were put in most action FPS games, it’s get annoying Quick
So would the heat mechanic,
But, I think you get the point
You can’t use those kinds of games as a comparison to MechWarrior as a basis against mechanics such as leg destruction
They play very differently, and therefore the mechanic might work great in one genre, but bad in the other
Could you imagine having section-based damage on TitanFall?
Or an FPS-style, aggressive cone-of-fire mechanic in MechWarrior?
It just wouldn’t work
But section-based damage readings work well in MechWarrior
And the cone of fire system, if done right, works quite nicely in games like Call of Duty, Battlefield, etc.
Which then led me to write up this response:

It's not exactly 'MLG or not', this (Mechwarrior)being a game it's all about players and the decisions they can make , or incentivized to make.

Also, Mechwarrior is not as sim-like as we want to believe.

Mechwarrior exists in a game genre like any other game does, open to criticism as well as evolution and experimentation.

re: Titanfall section-based damage, why not try it?

re: Cone of fire, this is actually more sim-like than Mechwarrior's pin-point accuracy. no targeting computer based on hard-scifi could compensate for the the source gun changing shooting position every split second of a mech's walk-cycle. The positional height difference would be insane, not to mention the pilot being shaken like paint in a can.

Originally I had a longer post about genre conventions and simulations, wanting to say that yes mecha-sims are a 'sim' but they handwave away of massive amounts of realism to make sure the experience is fun.

take it this way: you ever play World of Tanks? it's a great example of unfun simulation. It gets all rivet-counter on shot penetration and deflection mechanics, but somehow your invisible crew can jump out to repair a blown track while under fire...but wait there's more!

if you are hit again in the tracks, your repair meter is reset allowing opponents to functionally immobilize you after a single hit to your tracks. Sim-like? a little, but is it fun? god no. Player experience becomes sitting there, waiting to be eliminated.

re: "make legging more like tabletop" , that has some interesting stuff - like your speed and stability degrading over time as your legs are damaged, but do you really want to sit there in a hex, on your mechs stomach, immobilized for the next 20min? Players do not like it when game scenarios escape their control, and this effect needs to be managed (note: I didn't say removed).

End of the day, I'm saying that certain mechanics should be gamed out to their logical conclusion, assessed, and then adjusted to make sure the player has fun, or that most of the players have fun.

Limping along a giant map for even 5min with a busted leg isn't fun. If the player is playing a campaign, they will probably save-scum at that point, merely restarting the mission.

In multiplayer? might as well just slap the respawn button because functionally their vehicle is dead-in-the-water.

As it exists today: legging is a shallow mechanic that feeds into the run away winner problem. You either leg someone, then control the rest of the fight completely from there - to wit, you've already 'won' by this point. or you don't leg someone, and they don't leg you, and both of
yous are playing as if legging doesn't exist.

so what's the answer? honestly I don't know. I think we'll need a bunch of new mech games to take up the challenge to explore these trickier aspects of Mechwarrior style gameplay, and I look forward to playing them!

Wednesday, May 15, 2019

battleMETAL - And Yet, Somehow it all works - part 5 - Arming Menu

Load and lock…


On the heels of talking about the Deployment Menu, there’s another menu that I had a lot of fun building and consider a technical achievement: the Arming Menu. This menu is a staple of old vehicle combat games, and mech games in particular like in Battletech, it's called the ‘Mech Lab’. This is the menu where the player gets to exercise a level of creativity, creating a loadout of weapons and equipment that they are most comfortable with. In battleMETAL, every playable ‘Metal (mech) has a set of Equipment Slots. These slots may contain Weapons or Equipment but limited to a set of factors such as item size and the type of item. Here’s a few examples from old school games to showcase what my starting ideas were.

Mech Lab as seen in Mechwarrior 4: Mercenaries


Weapon Menu as seen in Earthsiege 2


These menus can be fairly daunting for a newer player or someone not used to this genre of game. A ton of information is thrown at the player all-at-once, and many decisions here will directly impact the player’s experience when they actually deploy to the game space. However, this is a menu that the game will return to many times, and Players are encouraged to spend all the time they want here testing out ideas. To that end, I tried to make the Arming Menu as straightforward as possible given the limited UI tools at hand.



Proof of Concept Arming Menu ~2016



...Ok, we can do a little better than that, heh. This image was the very first take on the Arming Menu. Although it lacks a lot of graphical icons, one can see the roots of the menu are here. Weapons and Equipment have a UID or unique ID that the game uses when looking up data for the item. When the Arming Menu is called, it grabs all relevant data for each UID, and the data is stored in separate .item files. These .item files are text files renamed to be better understood, and contain a JSON-style data language.



Add in data loaded for every mech in the game and the end result is a wonderful menu that looks like this:





Now this is looking pretty good. We can see the list of available equipment for the Player. We can see the hardpoints that the chosen mech possesses. There’s some other important panels here as well: the ‘Weapon Group’ panel in the lower left allows the Player to set their fire groups. A fire group is a collection of weapons bound to a single fire key. Next to this panel, is the ‘Energy Drain’ panel. This important panel ties into battleMETAL’s Player resource mechanics. In addition to health, Players must be aware of how much Energy their ‘Metal currently has. Energy is used to power weapons, shields (a rechargeable health pool), and sprinting. This panel tells the player how much energy their Weapon Groups are going to draw when used. This is helpful to Player if they’re trying to balance their equipment selections.


In the middle-right of the screen is a very important set of panels, the Weapon Info panels. The top one shows the statistics for the Weapon that a Player has chosen in the menu. The bottom panel is for the Weapon in the selected hardpoint. They’re over/under so that Players can compare directly, the stats of 2 different weapons before mounting any weapon to a hardpoint.


Now for some neat code notes. Every mech has a list of Hardpoints that can hold a piece of Equipment. The data for this is stored in a text file with a custom .unit extension. When the Player selects a mech in the Hangar Menu, that data is loaded into a set of global vars. These vars are not static, if the Player chooses a different mech, the globals are updated with the data of the next mech the Player chose. Part of the data loaded are the hardpoints, Quake C being more primitive means that hardpoint data is loaded into a series of Arrays[] dedicated to each data point. The index of the array is used as the UID of the Hardpoint:


float MECH_DATA_H_TYPE[10];denotes the allowable Types of equipment for the Hardpoint.

float MECH_DATA_H_SIZE[10]; maximum Size of the Equipment.

vector MECH_DATA_H_ARM[10];

vector MECH_DATA_H_HNG[10];


The two vector arrays are specifically for the UI. Currently, I hardcoded the screen-position of the Hardpoints when the Arming Menu needs to draw these on-screen. The Arming Menu code loops through all the hardpoints (game maximum of 9 possible hardpoints) and pulls the position for each Hardpoint from those vector arrays. When the Player clicks the mouse, the code checks to see if the mouse position is within the area near of each vector to determine if the Player has clicked on a Hardpoint. If the Player has, the code uses the UID of the Equipment in the Hardpoint to load the .item data file for that Equipment.


Another great feature I implemented recently was the ability for the Player to save up to 6 different configurations per ‘Metal. I realized that Players would prefer to be able to save their changes to the mechs for later use. Well if we’re going through the trouble of saving something once, then we now have the functionality to save anytime (isn’t programming grand?). The save feature is autonomic and triggered by natural Player interaction with the Arming Menu. Things like tabbing between configs, or switching from different menus and the Arming Menu. The save files are then stored in folders labelled via ‘Metal UID, and then their own number. The save files itself is a plaintext file, and is loaded on-demand. This means that savvy Players can noodle around with just the text file and have it load by simply changing configs in-game. I did put in some validations though, so no breaking a ‘Metal’s Hardpoint values by putting in larger-than-allowed weapons or such.


Finally, once the Player has tweaked the ‘Metal to their liking, they proceed to the Deployment Menu. When the Player hits the ‘Launch’ button, the CSQC takes all their selection data and pushes it up to the Server for parsing. The Player’s chosen mech UID is sent, along with Hardpoint data and Weapon Group options, the server than takes this data and builds the Player’s entity into that mech. Overall, this was a really fun feature to build for battleMETAL, and fairly radical for any Quake mod at all.

Wednesday, May 8, 2019

battleMETAL - And Yet, Somehow it all works - part 4 - Deployment Menu

Prepare for drop!

Checking my post timeline and realized that this blog series has now been running for a year, huzzah! Also, core coding of battleMETAL appears complete. The last few months have been cleaning up the map pipeline (don’t worry friends, we’ll get there…) which indicates that the main coding is stable! It does feel a little anticlimactic, so many weekends spent hammering this game together in a cave with a box of scraps….


Anyhoo, this article is about one of the in-mission menus. These menus are located in the CSQC module which is run exclusively client side. As described in a previous post about CSCQ, this menu system is brought up with the tab key (default key) after the player has connected to the server. One of these menus is the Deployment menu.


The design scope for the Deployment menu went something like this - Originally I wanted the player to be able to pick from a few different spawn points on any given mission. Previous mech games either lacked this feature or featured it loosely. 2 main impulses pushed me to implement such a feature in battleMETAL with the first being multiplayer. Originally I believed it was possible to make battleMETAL work in multiplayer as well as it did in singleplayer. Unfortunately due to engine limitations across multiple features, this was discovered to not be feasible given the time and manpower limits of myself (I’ll do a post eventually on what happened to multiplayer).


The second impetus came from adapting the multiplayer idea. I considered the Player’s ability to pick their spawn point to be a nice layer of tactical control to give the player in lieu of trying something like an open map or semi-open world. The spawn point selection also allows for levels to contain more than 1 scenario setup - the mission briefing can give clues to the player about the combat conditions at any of the spawn points. Once the player chooses their spawn point, they press the [launch] button, become the mech they chose in the Hangar menu and begin play.


So, how did the code get there? As seen in a previous article, a battleMETAL CSQC menu can be quickly spun up from the functional map. The file acts like a pseudo class where the primary menu functions for the desired menu are kept. I’ll cover the most interesting tidbit which are the deploy points.

Pictured is the Deployment menu with Deploy Points depicted as (A), (B), ( C).


To begin, when the Player connects to the Server, there’s a function call;

client_push_mapobjects()

This queries all the entities on the server that have the FL_NODE flag in their .flags member variable. When such entities are found, their data is piped to the Client via client_send_deployPoint() which bundles the relevant deploy point info into a SVC_TEMPENTITY game packet. The client then receives this data, checking its own global DEPLOY_POINTS_ACTIVE[]. This global array maps its array index to the Deploy Point Id of deploy point entities on the server. The game has an artificial limit of 32 total deploy point entities because of this (not sure if any given map needs more than 32 deploy points).


The ‘important info’ that the server sends to the client is the following:

        Deploy id - float, the assigned id number for the node ( this is not the same as the entity id)

         isActive - boolean, 1 or 0. Players can only see and use ‘
isActive == true’ deploy points.

        Origin - the server-side location of the entity in game space as a vector, 'X Y Z'


Quick side note, deploy points can be given an .faction member var. The server only sends deploy points whose faction value matches the specified Player’s faction value. This is a holdover from the multiplayer functionality where originally PvP was going to be a thing.


Once the client has the list of deploy points, the next step is how to render them. I really wanted a clean and programmatic way to render any deploy points anywhere on the given map space. Other considerations included the fact the player in a singleplayer-context only really ever sees the Deployment Menu once per map (even if they die, the map just restarts). Due to this, there wasn’t a need to constantly send live updates of deploy point data to the client. Knowing this, drawing the deploy points onto the menu screen was fairly straightforward.


Of the few things that CSQC gets ‘for free’ from the framework is data about the world entity. I can’t remember in the C-code all of the vars in world that are sent over, but there’s one important set - .mins and .maxs. These two variables contain the size of world’s total bounding box, just like other entities have bounding boxes. Knowing the bounding box of the world allows us to compare entity locations to some sort of pseudo-constant for game space. Using this info, I created an algorithm for projecting a map coordinate onto a 2D UI panel.

  1. Find the map’s total size
    1. Take the mins value as a positive number and add the maxs value
    2. Find the center of this new size
  2. Take the mins value as a positive number, then divide it over the total size
    1. This returns a percentage value ( see where I’m going here?)
  3. Apply the percentage to the UI panel’s screen size
  4. profit.

This algorithm allows us to display any game coordinates onto any-sized 2D space and you can see this functionality on the Briefing menu as well. Nav Points are player guide posts for navigating around the map and the same algorithm renders these points onto the Briefing menu just like the Deployment menu. Now that the Player can see where on the map a Deploy Point will place them, the next part is spawning the Player at the point they have chosen. When the player clicks the Launch button in the upper right-hand corner, the CSQC fires off a message back to the server. This message contains the player’s chosen mech, the mech’s equipment id’s, and the Deploy Point to spawn at.


When the Server receives this message, it goes and looks for the desired Deploy Point, and if it finds the point, will then move the Player to that point. The fallback scenario is that the Deploy Point is not found, so the code defaults to the map object Info Player Start which every map needs regardless of usage. That’s all I have for this post, an interesting small feature that was fun to implement and fairly unique for Quake mods. There’s probably some refactoring that could be done on the algorithm but I figure its low-cost enough to stay as it is.


Monday, April 15, 2019

battleMETAL - Quick update 4/15/2019

Where the hell have you been? 

I know I know, I'm really bad at maintaining a blog. So I've been busy with a slice of life, some major tech fighting with battleMETAL (oh we'll get into it soon), and starting a new job soon. I do want to return to the 'METAL Monday formula, its just that blog writing was cut from the schedule because it was something that could be sacrificed. I'll return to blog posts on May 6th, 2019 and we'll keep diving into the bucket of fun that is battleMETAL. 

In the mean time, the game itself is pretty much feature complete and I've moved onto level generation. I have a tentative Beta release date of Q3 2019, and then we'll see how things look from there. Stand fast, DEAD HANDS, your activation is pending.

 

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.