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
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_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.
No comments:
Post a Comment