Warning: fopen(menu/tut_sbar_scale.php) [function.fopen]: failed to open stream: Not a directory in /mnt/115/sdb/f/f/benoar/phf/tutorials/menu.php on line 94

Main page
Static sized HUD

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 ?).
So, our new function will replace the old one (Re.DrawPic), but we'll have to pass new arguments : the scale factors.

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.
I've also decided that if the factor is 0, it means that we mustn't scale the HUD, so it behaves like the normal HUD.

For calculating the x and y factors, let do some maths :
The coordinates given by the server are calculated to fill a 320x240 screen with the entire HUD at the bottom. Moreover, coordinates are given relative to different points (not always upper-left, like one could think), but this won't be a problem as we will scale these origins points' coordinates too.
So, if our screen width is 640, for example, the coordinates will be multiplied by 2 if we want the HUD to take the entire place. And if you abstract it a bit, you get :
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.
Here, we just compute the factors, as the calculus for the coordinates isn't always the same, depending on the originating point. There's 3 different origins for both x and y axis : left ("xl"), right ("xr") and center ("xv") for the x, and top ("yt"), bottom ("yb") and middle ("yv") for the y. If you wanna see how coordinates are calculated, go further in the function and look at the code after the strncmp() ...

I'm quite sure that nobody did understand my explanation, as I'm not a native english speaker and I'm not very good at explaining things. But you should understand it by looking at the code, it's quite simple in fact.

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.

That's OK for calculating our coordinates, so now we use our brand new DrawScaledPic function, where it's needed (well, were pictures are drawn, a little further):
		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.
I've also added the xfact and yfact variables, as they're used in the DrawScaledPic function.

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

Top of this page