This tutorial will enable you to out a more advanced Heads Up Display (HUD) in your GTest derived game than my previous one. Again this is based on Ewens initial work. The features of this new HUD are:
This tutorial requires that you know your way around GTest, although it should be fairly easy for beginners to pick it up.
Here is a screenshot of the new HUD you will be creating.

[The code below will enable you to create this exact HUD].
First, you will need to download the two files, Hud.c and Hud.h (zipped at the bottom of the page). Then you can start by setting up the HUD as in my previous tutorial (Please Note: there are some modifications):
In GENVS.C you must add a line near the beginning of the file to include HUD.H
Line 35: #include "HUD.h"
Next add a pointer to a HUD object (and a rectangle object we'll use to position the HUD), about line 72:
// Misc objects
static Console_Console *Console;
Host_Host *Host;
geSound_System *SoundSys;
HUD *theHUD;
geRect gR = {0,0,0,0};
Now we need some changes to the WinMain function, to set the clipping rectangle of the HUD and to create the HUD object. First we will need to declare two integers, at the very top of WinMain:
int Width, Height;
Then, at around about line 415 or so (just before the while (Running) statement):
#if 1
// <>
geEngine_SetGamma(Engine, UserGamma);
#endif
QueryPerformanceFrequency(&Freq);
QueryPerformanceCounter(&OldTick);
// Hud
VidMode_GetResolution(VidMode,&Width,&Height);
gR.Left = 0;
gR.Top = 0;
gR.Right = Width;
gR.Bottom = Height;
gHUD = CreateClientHUD(Engine, &gR); // create the HUD object
//SetCapture(GameMgr_GethWnd(GMgr));
ShowCursor(FALSE);
#ifdef CLIP_CURSOR
{
(Note - now the Hud resizes to fit your screen size).
Finally, we need to destroy the HUD on shutdown by modifying the ShowdownAll function:
//=====================================================================================
// ShutdownAll
//=====================================================================================
void ShutdownAll(void)
{
ReleaseCapture();
ShowCursor(TRUE);
....
// Free each object (sub objects are freed by their parents...)
if (MenusCreated)
{
Text_Destroy();
GMenu_Destroy();
MenusCreated = GE_FALSE;
}
// Hud
DestroyClientHUD(theHUD);
if (Host)
{
Host_Destroy(Host);
Host = NULL;
}
....etc.
In Client.C you must add a line near the beginning of the file to include HUD.H
Line 28:. #include "HUD.h"
After the includes, you must add a line to make a reference to theHUD object in GENVS.C:
Line 35: extern HUD *theHUD;
Next, you must add one line of code to the Client_RenderFrame function:
//=====================================================================================
// Client_RenderFrame
//=====================================================================================
geBoolean Client_RenderFrame(Client_Client *Client, float Time)
{
assert(Client_IsValid(Client) == GE_TRUE);
if (Client->NetState != NetState_WorldActive) // If no world currently set, then return...
return GE_TRUE;
if (!RenderWorld(Client, Client->GMgr, Time))
GenVS_Error("Client_RenderFrame: RenderWorld failed.\n");
if (Client->MultiPlayer)
{
if (!PrintClientScores(Client))
GenVS_Error("Client_RenderFrame: PrintClientScores failed.\n");
}
PrintCrossHair(Client);
UpdateStatusBar(Client);
ScreenBorderHack(Client);
// Hud
if (!DrawHUD(theHUD, Client)) // this is where we draw the HUD
return GE_FALSE;
return GE_TRUE;
}
Now, open up GMain.c and near the top, include the hud.h file
#include "HUD.h"
then beneath this, reference the hud you created in GenVS.c (It doesn't matter that this one is called gHud rather than theHud, it is just for convienience).
extern HUD *gHUD;
Now this is a tricky part. In my modified GTest, I have already added kodjis drowing code. I will restate it here so it is obvious how I modified it, but all credit for the pink code goes to him. All this happens in the Client_Control() function
...
// Get a box a little bigger than the player for doors, etc...
CMins = Player->Mins;
CMaxs = Player->Maxs;
CMins.X -= 100;
CMins.Y -= 10;
CMins.Z -= 100;
CMaxs.X += 100;
CMaxs.Y += 10;
CMaxs.Z += 100;
CameraMins= Player->Mins;
CameraMaxs = Player->Maxs;
// Start Drowning Code
CameraMaxs.X -= 100;
CameraMaxs.Y -= 10 ;
CameraMaxs.Z -= 100;
CameraMins.X += 100;
CameraMins.Y += 10;
CameraMins.Z += 100;
if (geWorld_GetContents(World,&Player->XForm.Translation, &CameraMins, &CameraMaxs,GE_COLLIDE_MODELS, 0, NULL, NULL, &Contents))
{
// You could play an 'underwater' sound here.
Player->TimeInWater += Time;
}
else if (Player->TimeInWater >= 7.0f)//player's head is no longer under water
{
if (Player->State == PSTATE_InWater)//Player's body is inwater
{
gHUD->ShowWarningIcon = GE_FALSE;
Player->TimeInWater = 0.0f;
GenVSI_SetClientHealth(VSI,Player->ClientHandle, Player->Health);
GenVSI_PlaySound(VSI,SOUND_INDEX_PLAYER_BREATH,&Player->XForm.Translation);
}
}
else Player->TimeInWater =0.0f;
if (Player->TimeInWater > 10.0f)
{
// Show warning on HUD
gHUD->ShowWarningIcon = GE_TRUE;
gHUD->WarningType = DMG_TYPE_DROWN;
Player->Health -= 10;
GenVSI_SetClientHealth(VSI, Player->ClientHandle,Player->Health);
Player->TimeInWater = 7.0f;
GenVSI_PlaySound(VSI,SOUND_INDEX_PLAYER_GASP,&Player->XForm.Translation);
}
if (Player->Health <= 0)
{
gHUD->ShowWarningIcon = GE_FALSE;
GenVSI_PlaySound(VSI,SOUND_INDEX_DIE,&Player->XForm.Translation);
Player->Control = Player_Drowned;
}
// End Drowning Code
if (geWorld_GetContents(World, &Player->XForm.Translation, &CMins, &CMaxs, GE_COLLIDE_MODELS, 0, NULL, NULL, &Contents))
{
if (Contents.Model)
{
GPlayer *TPlayer;
TPlayer = (GPlayer*)geWorld_ModelGetUserData(Contents.Model);
...
For the rest of the drowning code, please goto kodjis original tutorial (see the programming section on the World of Genesis).
Basically, what this modification does, is if the player is underwater for too long, an icon flashes on the top of the screen warning them, and take 10 off of their health. The icon is drawn in our new Hud.c file, which is as follows:
#include <Windows.h>
#include <assert.h>
#include "Genesis.h"
#include "HUD.h"
#include "Globals.h"
#include "Ram.h"
#include "GenVSI.H"
#include "Client.H"
#include "Host.H"
#include "Text.h"
#define ItemWidth 20
#define ItemHeight 20
extern geVFile *PakFS; // Pak File
extern geVFile *MainFS; // App Folder
geBoolean AmmoHack = GE_FALSE;
HUD* CreateClientHUD(geEngine *Engine, geRect *Location)
{
HUD *hud = malloc(sizeof(HUD)); // create the memory for the HUD
assert(hud);
hud->Screen = Location;
Location->Top = Location->Bottom - 25;
hud->Engine = Engine; // get a copy of the engine object
hud->Location = Location; //store the location
hud->Bitmap = geBitmapUtil_CreateFromFileAndAlphaNames(PakFS, "bttlxe\\gfx\\hud\\hud_01.bmp", "bttlxe\\gfx\\hud\\hud_01.bmp");
//geBitmap_CreateFromFileName(PakFS, "bttlxe\\gfx\\hud\\hud_01.bmp");
//hud->AlphaBitmap = geBitmap_CreateFromFileName(PakFS, "bttlxe\\gfx\\hud\\hud_01a.bmp");
// geBitmap_SetColorKey(hud->Bitmap, GE_TRUE, 100, GE_FALSE);
// geBitmap_SetAlpha(hud->Bitmap, h->AlphaBitmap);
geEngine_AddBitmap(hud->Engine, hud->Bitmap);
return hud;
}
geBoolean DestroyClientHUD(HUD * hud)
{
if (hud->Bitmap)
{
assert(hud->Engine);
geEngine_RemoveBitmap(hud->Engine, hud->Bitmap);
}
assert(hud);
free(hud); // free the memory
return GE_TRUE;
}
geBoolean DrawHUD(HUD * hud, Client_Client *client)
{
Client_ClientInfo *clientInfo;
assert(hud->Engine);
clientInfo = client->ClientInfo;
if (!clientInfo->Active)
return GE_TRUE;
// Call the drawing functions:
if (!DrawWarningDisplay(hud, client))
return GE_FALSE;
if (!DrawWeaponDisplay(hud, client))
return GE_FALSE;
if (!DrawDamageDisplay(hud, client))
return GE_FALSE;
if (!DrawCrosshair(hud, client))
return GE_FALSE;
return GE_TRUE;
}
geBoolean DrawWeaponDisplay(HUD * hud, Client_Client *client)
{
Client_ClientInfo *clientInfo;
geRect textClip, sepClip;
int32 remainder, health;
int32 armor;
uint32 Ammo;
geBoolean HasItem;
int32 hack;
assert(hud->Engine);
clientInfo = client->ClientInfo;
if (!clientInfo->Active)
return GE_TRUE;
// set up seperating lines
sepClip.Top = 50;
sepClip.Bottom = 74;
sepClip.Left = 512-2;
sepClip.Right = 512;
//===================================================================
// Draw the Health Dsiplay
//===================================================================
textClip.Top = ItemHeight;
textClip.Bottom = 2 * ItemHeight;
textClip.Left = 0;
textClip.Right = ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Left + 5, hud->Location->Top))
return GE_FALSE;
textClip.Top = 0;
textClip.Bottom = ItemHeight;
health = clientInfo->Health;
remainder = health / 100;
if (remainder > 0) // only display the first digit if health >= 100
{
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Left + ItemWidth + 5, hud->Location->Top))
return GE_FALSE;
}
health = health % 100;
remainder = health / 10;
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Left + 2*ItemWidth + 5, hud->Location->Top))
return GE_FALSE;
health = health % 10;
remainder = health;
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Left + 3*ItemWidth + 5, hud->Location->Top))
return GE_FALSE;
// Draw the seperating line between items
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &sepClip, hud->Location->Left + 5*ItemWidth + 10, hud->Location->Top - 3))
return GE_FALSE;
//===================================================================
// Draw the Armor Dsiplay
//===================================================================
textClip.Top = ItemHeight;
textClip.Bottom = 2 * ItemHeight;
textClip.Left = ItemWidth;
textClip.Right = 2 * ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Left + 6*ItemWidth + 5, hud->Location->Top))
return GE_FALSE;
textClip.Top = 0;
textClip.Bottom = ItemHeight;
// For some reason, a 32 bit value is being assigned to the armor, so subtract it here:
hack = 32768;
armor = client->Inventory[ITEM_ARMOR] - hack;
remainder = armor / 100;
if (remainder > 0) // only display the first digit if armor >= 100
{
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Left + 7*ItemWidth + 5, hud->Location->Top))
return GE_FALSE;
}
armor = armor % 100;
remainder = armor / 10;
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Left + 8*ItemWidth + 5, hud->Location->Top))
return GE_FALSE;
armor = armor % 10;
remainder = armor;
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Left + 9*ItemWidth + 5, hud->Location->Top))
return GE_FALSE;
//===================================================================
// Ammo Display (Type)
//===================================================================
Ammo = client->Inventory[client->CurrentWeapon];
HasItem = (Ammo & (1<<15));
Ammo &= 0xff;
if (HasItem)
{
textClip.Top = 0; // numbers are the top row of the HUD ITEMS texture
textClip.Bottom = ItemHeight;
remainder = Ammo / 100;
if (remainder > 0) // only draw the first digit if necessary
{
AmmoHack = GE_TRUE;
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Right - (10 + 4*ItemWidth), hud->Location->Top))
return GE_FALSE;
}
else
AmmoHack = GE_FALSE;
Ammo = Ammo % 100;
remainder = Ammo / 10;
if (remainder > 0) // only draw the second digit if necessary
{
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Right - (10 + 3*ItemWidth), hud->Location->Top))
return GE_FALSE;
}
else if (AmmoHack != GE_FALSE)
{
textClip.Left = 0;
textClip.Right = ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Right - (10 + 3*ItemWidth), hud->Location->Top))
return GE_FALSE;
}
Ammo = Ammo % 10;
remainder = Ammo;
textClip.Left = remainder * ItemWidth;
textClip.Right = remainder * ItemWidth + ItemWidth;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Right - (10 + 2*ItemWidth), hud->Location->Top))
return GE_FALSE;
}
else
{
//if (!Console_XYPrintf(Host->Console, 30, 30, " Ammo: No Weapon"))
// return GE_FALSE;
}
textClip.Top = ItemHeight;
textClip.Bottom = 2 * ItemHeight;
switch(client->CurrentWeapon)
{
case 0: // blaster (will be knife)
{
textClip.Left = 9*ItemWidth;
textClip.Right = 10*ItemWidth;
break;
}
case 1: // handgrenade
{
textClip.Left = 8*ItemWidth;
textClip.Right = 9*ItemWidth;
break;
}
case 2: // rpg
{
textClip.Left = 7*ItemWidth;
textClip.Right = 8*ItemWidth;
//if (!Console_XYPrintf(Host->Console, 30, 31, " Rockets"))
// return GE_FALSE;
break;
}
case 3: // mgun
{
textClip.Left = 4*ItemWidth;
textClip.Right = 5*ItemWidth;
break;
}
case 4: // shotgun
{
textClip.Left = 6*ItemWidth;
textClip.Right = 7*ItemWidth;
break;
}
case 5: // pistol
{
textClip.Left = 3*ItemWidth;
textClip.Right = 4*ItemWidth;
break;
}
case 6: // magnum 357
{
textClip.Left = 3*ItemWidth;
textClip.Right = 4*ItemWidth;
break;
}
default:
{
textClip.Left = 9*ItemWidth;
textClip.Right = 10*ItemWidth;
break;
}
}
// Only draw the ammo icon if the client actually has the weapon.
if (client->Inventory[client->CurrentWeapon] != GE_FALSE)
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, hud->Location->Right - (5 + ItemWidth), hud->Location->Top))
return GE_FALSE;
return GE_TRUE;
}
geBoolean DrawWarningDisplay(HUD * hud, Client_Client *client)
{
Client_ClientInfo *clientInfo;
geRect textClip;
assert(hud->Engine);
clientInfo = client->ClientInfo;
if (!clientInfo->Active)
return GE_TRUE;
if (hud->ShowWarningIcon)
{
switch (hud->WarningType)
{
case DMG_TYPE_DROWN:
{
textClip.Left = 260;
textClip.Right = 260 + 50;
break;
}
case DMG_TYPE_BURN:
{
textClip.Left = 260;
textClip.Right = 260 + 150;
break;
}
case DMG_TYPE_FREEZE:
{
textClip.Left = 260;
textClip.Right = 260 + 100;
break;
}
case DMG_TYPE_TOXIC:
{
textClip.Left = 260;
textClip.Right = 260 + 50;
break;
}
case DMG_TYPE_ELECTRIC:
{
textClip.Left = 260 + 210;
textClip.Right = 260 + 250;
break;
}
}
textClip.Top = 0;
textClip.Bottom = 50;
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, 20, 20))
return GE_FALSE;
}
return GE_TRUE;
}
geBoolean DrawDamageDisplay(HUD * hud, Client_Client *client)
{
Client_ClientInfo *clientInfo;
GE_Rect Rect;
GE_RGBA Color;
// Filled rect.
Rect.Left = 0;
Rect.Right = 20000;
Rect.Top = 0;
Rect.Bottom = 15000;
// Fill colour.
Color.r = 0.0f;
Color.g = 0.0f;
Color.b = 0.0f;
Color.a = 255.0f;
assert(hud->Engine);
clientInfo = client->ClientInfo;
if (!clientInfo->Active)
return GE_TRUE;
/*
if (hud->ShowDamage)
{
switch (hud->DamageDirection)
{
case DMG_DIR_TOP:
{
Rect->Top = 50;
Rect->Bottom = ;
Rect->Left = 50;
Rect->Right = Width - 50;
break;
}
}
textClip.Top = 0;
textClip.Bottom = 50;
*/
/* if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip, 20, 20))
return GE_FALSE;*/
geEngine_FillRect( hud->Engine, &Rect, &Color ); // Filled rect.
// }
return GE_TRUE;
}
geBoolean DrawCrosshair(HUD * hud, Client_Client *client)
{
Client_ClientInfo *clientInfo;
geRect textClip;
int Width, Height, Left;
assert(hud->Engine);
clientInfo = client->ClientInfo;
if (!clientInfo->Active)
return GE_TRUE;
Width = 20;
Height = 20;
Left = 270;
textClip.Top = 120;
textClip.Bottom = 120 + Height;
switch(client->CurrentWeapon)
{
case 0: // blaster (will be knife)
{
textClip.Left = Left + (6 * Width);
textClip.Right = Left + (7 * Width);
break;
}
case 1: // handgrenade - (no crosshair)
{
textClip.Left = Left - Width;
textClip.Right = Left;
break;
}
case 2: // rpg
{
textClip.Left = Left + (9 * Width);
textClip.Right = Left + (10 * Width);
break;
}
case 3: // mgun
{
textClip.Left = Left + (5 * Width);
textClip.Right = Left + (6 * Width);
break;
}
case 4: // shotgun
{
textClip.Left = Left + (4 * Width);
textClip.Right = Left + (5 * Width);
break;
}
case 5: // pistol
{
textClip.Left = Left + (8 * Width);
textClip.Right = Left + (9 * Width);
break;
}
case 6: // magnum 357
{
textClip.Left = Left + (10 * Width);
textClip.Right = Left + (11 * Width);
break;
}
default:
{
textClip.Left = Left + (8 * Width);
textClip.Right = Left + (9 * Width);
break;
}
}
// Only draw the crosshair if the client actually has the weapon.
if (client->Inventory[client->CurrentWeapon] != GE_FALSE)
if (!geEngine_DrawBitmap(hud->Engine, hud->Bitmap, &textClip,
(hud->Screen->Right / 2) - (Width / 2),
(hud->Screen->Bottom / 2) - (Height / 2) - 10))
return GE_FALSE;
return GE_TRUE;
}
#pragma warning ( default : 4013 4047 )
You will need to change the available weapons to match those in your game.
The hud.h file is as follows:
#include <Windows.h>
#include "Genesis.h"
#include "Ram.h"
#include "Client.H"
#include "Host.H"
#include "Text.h"
#include "Host.h"
#include "Server.h"
#include "GPlayer.h"
#include "GenVSI.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct HUD
{
geEngine *Engine;
geRect *Location;
geRect *Screen;
geBitmap *Bitmap;
geBitmap *AlphaBitmap;
geBoolean ShowWarningIcon;
int WarningType; // Water, heat, etc...
geBoolean ShowWeaponIcon;
int WeaponNumber;
geBoolean WeaponDisabled;
geBoolean ShowDamage;
int DamageDirection;
int DamageAmount;
} HUD;
HUD *CreateClientHUD(geEngine *Engine, geRect *Location);
geBoolean DestroyClientHUD(HUD * hud);
geBoolean DrawHUD(HUD * hud, Client_Client *client);
geBoolean DrawWarningDisplay(HUD * hud, Client_Client *client);
geBoolean DrawWeaponDisplay(HUD * hud, Client_Client *client);
geBoolean DrawDamageDisplay(HUD * hud, Client_Client *client);
geBoolean DrawCrosshair(HUD * hud, Client_Client *client);
geBoolean DrawPickups(HUD * hud, Client_Client *client);
#ifdef __cplusplus
}
#endif
#endif
I have included a file called globals.h at the top of hud.c, which contains all of the defines for the hud damage types, etc:
#ifndef GLOBALS_H
#define GLOBALS_H
// Damage Types
#define DMG_TYPE_DROWN 1
#define DMG_TYPE_BURN 2
#define DMG_TYPE_TOXIC 4
#define DMG_TYPE_FREEZE 8
#define DMG_TYPE_ELECTRIC 16
#define DMG_DIR_TOP 1
#define DMG_DIR_BOTTOM 2
#define DMG_DIR_LEFT 4
#define DMG_DIR_RIGHT 8
#define DMG_DIR_FRONT 16
#define DMG_DIR_BACK 32
#pragma warning ( default : 4244 )
#endif
Now, all that is left to do is to compile GTest. Inevitably you will get those small errors where I have forgotten to tell you that I have customised something, but these should not be to hard to sort out.
You are free to use this code in any way that you want. I do not require you to credit me (unless you really want to :-) ), but Ewen would like a mention in the credits as being the original brain behind the idea.
Any problems you find with this source (yes, you will find it is not fineshed - I started to add damage displays, but got nowhere fast, so I left them commented out above - if you really want to finish them, please could you email me the modified files so I can use the source), can be emailed to me here: dwulff@bttlxe.co.uk (please indicate what tutorial you are refering to).
Now, here are the zipped files. Included are the two hud.* files, but not the globals file (you can do that yourself). I have also added a larger hud icons bitmap. You can grap the files here.
Until next time,
- David Wulff.