Main page
Static sized HUD
The default Quake2 HUD has a big flaw when you're playing with a high screen resolution: it doesn't adapt itself to your resolution, and it gets smaller as you're raising screen res, especially when you get above 1024 (OK, it's not quite common, but with today computers, Quake2 will run at a very high speed even with a 1600x1200 screen res).
What we'll do is adding a cvar that will control the size of our HUD,
whatever resolution you are, so that you can get a HUD that is as big
as the 320x240 one's (maybe not that big if you wanna see something
else than half the screen of player's health/armor/ammo ...) in all
resolution modes.
I also added a small bonus : a cvar to control the transparency of the
HUD. That's easy to do that with OpenGL functions.
NOTE : the transparency is only available in slightly modified
Quake2 engine, like
Quake2Max one. I did all my modifications to this engine, but they
should also work on the original engine, or on others.
To start, open the cl_scrn.c file in the client directory. Our work will modify this file only. Go to the SCR_ExecuteLayoutString function and look at it : it's where the client recieve the instructions from the server which describe where to draw what on the HUD. It may sound strange to you that the server sends commands to the client to describe how is drawn the HUD, if you worked on Quake3 code for example, but it's the way it's done in Q2.
First, we add our cvars, at the begining of the file, around line 60 after the other ones : (I call them sbar_* because the HUD is often called "status bar" in the code)
cvar_t *cl_demomessage;
cvar_t *cl_loadpercent;
/* static sized HUD cvars */
cvar_t *sbar_scale;
cvar_t *sbar_alpha;
And then, we assign them a default value, line 430 :
scr_graphscale = Cvar_Get ("graphscale", "1", 0);
scr_graphshift = Cvar_Get ("graphshift", "0", 0);
scr_drawall = Cvar_Get ("scr_drawall", "0", 0);
sbar_scale = Cvar_Get ("sbar_scale", "0.5", 0);
sbar_alpha = Cvar_Get ("sbar_alpha", "1.0", 0);
//
// register our commands
//
A scale value of 0.5 will give a half-screen HUD, which is a correct
default size, and the alpha (transparency) is set to 1.0, just to
look like the original Q2 HUD when begining (but we'll change it once
in game ;-))
Let's begin the serious work :
To draw our new HUD, we need to have a function that can draw the HUD's pics
scaled to the correct size. As you can see in the
SCR_ExecuteLayoutString function, the function
that is used to draw the different pics is re.DrawPic.
But there's another builtin function that do the same thing except that it first
scale the pic to a given size : re.DrawStretchPic.
That's exactly what we were looking for.
But there's one small problem : if we must give as an argument to this function the
new size, we must first know the size and the scale factor for this image. If you
search a bit in the source, you'll find the re.DrawGetPicSize
function that do what we want. So here is our new function, you can add it just before
SCR_DrawField, because we'll use our new function in it.
NOTE : If you don't use a modified Quake2 Engine like Quake2Max, remove the last
argument of the re.DrawStretchPic function, as the original Quake2 doesn't have an alpha
parameter.
/*
==============
DrawScaledPic
==============
*/
void DrawScaledPic (int x, int y, float xfact, float yfact, char *name)
{
int w, h;
re.DrawGetPicSize (&w, &h, name);
if (!w || !h)
return;
re.DrawStretchPic (x, y, w*xfact, h*yfact, name, atof(sbar_alpha->string));
}
/*
==============
SCR_DrawField
==============
*/
This function is quite simple : it first gets the pic size, see if it is not null
(if it's null, it means that the file doesn't exists / returned an error), and
then draw the pic, stretched to the new size, i.e. old_size*factor (we separated
x and y factor so that we can stretch our HUD in only one way, horizontaly or
vertically ... someone to find this feature usefull ?).Now, go to the begining of the SCR_ExecuteLayoutString, where we'll do most of our work. First, we add the two scale factors (one for the horizontal, xfact, and one for the vertical, yfact), near line 960.
int width;
int index;
clientinfo_t *ci;
/* for scaled status bar only, not for text drawing */
int scaledx, scaledy;
float xfact, yfact, fact;
if (cls.state != ca_active || !cl.refresh_prepped)
return;
And then we compute them :
y = 0;
width = 3;
scaledx = scaledy = 0;
fact = atof(sbar_scale->string);
/* 0 means no scale */
if (fact == 0)
{
xfact = yfact = 1;
}
else
{
xfact = viddef.width / 320.0 * fact;
yfact = viddef.height / 240.0 * fact;
}
while (s)
{
token = COM_Parse (&s);
The scaledx and scaledy will contain the x and y coordinates of the HUD
elements, scaled to the stretch factor.new_coordinate = old_coordinate * (screen_width / 320)The new coordinates are stored into scaledx and scaledy, and are calculated from the known x and y "old" coords. The screen width and height are stored in viddef.width and viddef.height. And as we want to be able to choose a scale factor that will control the proportionnal size of our HUD (this is the role of the sbar_scale cvar), we'll mutliply this by the cvar value. And we finally get our new coords.
Now we've got to do some boring work : calculate all our new coordinates, and
there's a lot of "functions" (server commands to draw things by different
manner) to modify.
NOTE : I've just modified coordinates for graphical elements, not textual
ones. So for now, you'll just get scaled graphics, but all the HUD's text will
remain at the same place. I'll try to correct that when I'll have some time.
Let's go :
if (!strcmp(token, "xl")) { token = COM_Parse (&s); x = atoi(token); scaledx = atoi(token)*xfact; continue; } if (!strcmp(token, "xr")) { token = COM_Parse (&s); x = viddef.width + atoi(token); scaledx = viddef.width + atoi(token)*xfact; continue; } if (!strcmp(token, "xv")) { token = COM_Parse (&s); x = viddef.width/2 - 160 + atoi(token); scaledx = viddef.width/2 + (-160 + atoi(token))*xfact; continue; } if (!strcmp(token, "yt")) { token = COM_Parse (&s); y = atoi(token); scaledy = atoi(token)*yfact; continue; } if (!strcmp(token, "yb")) { token = COM_Parse (&s); y = viddef.height + atoi(token); scaledy = viddef.height + atoi(token)*yfact; continue; } if (!strcmp(token, "yv")) { token = COM_Parse (&s); y = viddef.height/2 - 120 + atoi(token); scaledy = viddef.height/2 + (-120 + atoi(token))*yfact; continue; }I haven't deleted the old coordinates as they're used by the text drawing functions.
if (!strcmp(token, "pic")) { // draw a pic from a stat number token = COM_Parse (&s); value = cl.frame.playerstate.stats[atoi(token)]; if (value >= MAX_IMAGES) Com_Error (ERR_DROP, "Pic >= MAX_IMAGES"); if (cl.configstrings[CS_IMAGES+value]) { SCR_AddDirtyPoint (x, y); SCR_AddDirtyPoint (x+23, y+23); re.DrawPic (x, y, cl.configstrings[CS_IMAGES+value]); SCR_AddDirtyPoint (scaledx, scaledy); SCR_AddDirtyPoint (scaledx+23*xfact, scaledy+23*yfact); DrawScaledPic (scaledx, scaledy, xfact, yfact, cl.configstrings[CS_IMAGES+value]); } continue; }Forget the "client" and "ctf" drawing routines, as they're not part of the HUD, but used to draw the score.
if (!strcmp(token, "picn")) { // draw a pic from a name token = COM_Parse (&s); SCR_AddDirtyPoint (x, y); SCR_AddDirtyPoint (x+23, y+23); re.DrawPic (x, y, token); SCR_AddDirtyPoint (scaledx, scaledy); SCR_AddDirtyPoint (scaledx+23*xfact, scaledy+23*yfact); DrawScaledPic (scaledx, scaledy, xfact, yfact, token); continue; }And if you look always further, you'll find "functions" to draw the health, armor and ammo values. But when you look better at them, you find a new function that is used : SCR_DrawField. Remember it ? We put our function just before it because I said we would need it in this function. But what does it do ? It "simply" draw a number given as an integer. These numbers are drawn with images of the 0 to 9 numbers, pasted next to each other. And to do that, it uses the now well known re.DrawPic func, that we'll just replace by ours. Go around line 900 and modify this function to suit our needs :
void SCR_DrawField (int x, int y, int color, int width, int value) void SCR_DrawField (int x, int y, float xfact, float yfact, int color, int width, int value) { char num[16], *ptr; int l; int frame; if (width < 1) return; // draw number string if (width > 5) width = 5; SCR_AddDirtyPoint (x, y); SCR_AddDirtyPoint (x+width*CHAR_WIDTH+2, y+23); SCR_AddDirtyPoint (x+(width*CHAR_WIDTH+2)*xfact, y+24*yfact); Com_sprintf (num, sizeof(num), "%i", value); l = strlen(num); if (l > width) l = width; x += 2 + CHAR_WIDTH*(width - l); x += (2+CHAR_WIDTH*(width - l))*xfact; ptr = num; while (*ptr && l) { if (*ptr == '-') frame = STAT_MINUS; else frame = *ptr -'0'; re.DrawPic (x,y,sb_nums[color][frame]); x += CHAR_WIDTH; DrawScaledPic (x, y, xfact, yfact, sb_nums[color][frame]); x += CHAR_WIDTH*xfact; ptr++; l--; } }What we modified is relative to the width of the text, but not x and y coordinates, as those we'll pass to this function will already be scaled.
So now, we can make our last modifications (line 1150), not forgetting the new arguments :
if (!strcmp(token, "num")) { // draw a number token = COM_Parse (&s); width = atoi(token); token = COM_Parse (&s); value = cl.frame.playerstate.stats[atoi(token)]; SCR_DrawField (x, y, 0, width, value); SCR_DrawField (scaledx, scaledy, xfact, yfact, 0, width, value); continue; } if (!strcmp(token, "hnum")) { // health number int color; width = 3; value = cl.frame.playerstate.stats[STAT_HEALTH]; if (value > 25) color = 0; // green else if (value > 0) color = (cl.frame.serverframe>>2) & 1; // flash else color = 1; if (cl.frame.playerstate.stats[STAT_FLASHES] & 1) re.DrawPic (x, y, "field_3"); DrawScaledPic (scaledx, scaledy, xfact, yfact, "field_3"); SCR_DrawField (x, y, color, width, value); SCR_DrawField (scaledx, scaledy, xfact, yfact, color, width, value); continue; } if (!strcmp(token, "anum")) { // ammo number int color; width = 3; value = cl.frame.playerstate.stats[STAT_AMMO]; if (value > 5) color = 0; // green else if (value >= 0) color = (cl.frame.serverframe>>2) & 1; // flash else continue; // negative number = don't show if (cl.frame.playerstate.stats[STAT_FLASHES] & 4) re.DrawPic (x, y, "field_3"); DrawScaledPic (scaledx, scaledy, xfact, yfact, "field_3"); SCR_DrawField (x, y, color, width, value); SCR_DrawField (scaledx, scaledy, xfact, yfact, color, width, value); continue; } if (!strcmp(token, "rnum")) { // armor number int color; width = 3; value = cl.frame.playerstate.stats[STAT_ARMOR]; if (value < 1) continue; color = 0; // green if (cl.frame.playerstate.stats[STAT_FLASHES] & 2) re.DrawPic (x, y, "field_3"); DrawScaledPic (scaledx, scaledy, xfact, yfact, "field_3"); SCR_DrawField (x, y, color, width, value); SCR_DrawField (scaledx, scaledy, xfact, yfact, color, width, value); continue; }And now, go compile your modified Quake2, drop down the console and play with our new sbar_scale and sbar_alpha variables. I recommand not setting a more than 1 value to the scale var, as the result may be ... quite unexpected.
the result should look like that :
And don't hesitate to mail me if you wanna ask questions, expose
some problems or new ideas.
benoar