Wavelength Logo
tl.jpg (2351 bytes) blank.gif (837 bytes) tr.jpg (2446 bytes)
blank.gif (837 bytes)
New Menuing System Tutorial by Phillip Peterson
blank.gif (837 bytes)

Description
In this tutorial, we are going to go through the steps necessary to create a menuing system for Half-Life similar to the one for Quake 2.


In The Client DLL

Open up hud.h and somewhere in there add this code:

typedef enum
{
MENU_ALIGN_LEFT,
MENU_ALIGN_CENTER

} menu_align_t;

// What we just created was a new type to store the alignment of certain line in the menu


typedef struct
{
char Text[128];
// text of the menu item
menu_align_t Align; // the items alignment
int Selectable; // can it be selected
int ReturnNumber; // if so, what number is it

} menu_struct_t;
// this is the basic structure for the menu, our menu�s will be arrays for menu_struct_t�s

class CNewMenu
{
public:
int Init( void );
void InitHUDData( void );
int VidInit( void );
void Reset( void );
int Draw( float flTime );
int MsgFunc_MenuOpen( const char *pszName, int iSize, void *pbuf );

void SelectNextOption ( void );
// moves the cursor down
void SelectPreviousOption ( void ); // moves the cursor up
void SelectItem ( void ); // slects the menu item
int m_iCurrentSelection;
// index to the current item selected
int m_iTotalSlots;
// total number of entries in the menu, include everything
BOOL m_bMenuDisplayed; // true if the menu is displayed
BOOL m_bCanCancelMenu; // true if the menu can be closed
menu_struct_t *m_CurrentMenu; // pointer to the current menu
int sRed, sGreen, sBlue;
// colors for the item that are selected
int Red, Green, Blue;
// colors for the rest of the items in the menu
};

Now the explanation of the class CNewMenu.

These following functions are basic HUD functions:

Init - as you can probably guess is the point where we get everything started

InitHUDData - in this function we�ll set all our member variables up, stuff like that

VidInit - isn�t really needed, but it�s here for us incase you want to add a sprite or something

Reset - reset the data

Draw - we�ll draw the menu here, hence its name

MsgFunc_MenuOpen - is the function called when we send the message to open a new menu


These next functions are ones we are going to create, we�ll need these later:

SelectNextOption - this function will advance the menu selection one

SelectPreviousOption - this function will select the previous item

SelectItem - this one will be called when the player selects an item


The Member Variables of CNewMenu

m_iCurrentSelection - this is the index in our menu array to the item that is currently selected or highlighted

m_iTotalSlots - this is the number of items in the menu array

m_bMenuDisplayed - boolean, true if there is a menu

m_bCanCancelMenu - boolean, true if the menu can be exited ( like for a voting system, probably wouldn�t be used for a team selection menu )

*m_CurrentMenu - pointer to the menu being displayed

sRed, sGreen, sBlue - will hold the RGB color for the selected item

Red, Green, Blue - will hold the RGB color for the rest of the items


OK, now our class is declared, so go down a bit in hud.h and find the definition of CHud, and add this code:

CHudStatusIcons m_StatusIcons;
// add the code after m_StatusIcons is declared
CNewMenu m_NewMenu;

After that is declared, go through hud.cpp and the others add make sure out Init() is called and VidInit() is called.

Now we write some real code

Create a new file called newmenu.cpp and add it to your project. (Assuming you�re using MSVC++)

Now in newmenu.cpp do the includes:
#include "hud.h"
#include "util.h"
#include "parsemsg.h"

#include "string.h"
#include "stdio.h"

These are the basic header files we need. Now, we need to declare our message using DECLARE_MESSAGE so the server�s dll can communicate with the client dll.

DECLARE_MESSAGE ( m_NewMenu, MenuOpen );

The m_NewMenu is the name of the member variable in CHud. That sounded really confusing, sorry, it�s late and I�m tired.

Now we define our Init() function.

int CNewMenu::Init ( void )
{
gHUD.AddHudElem ( this );

HOOK_MESSAGE ( MenuOpen );

InitHUDData ();

return 1;
}

By calling gHUD.AddHudElem ( this ) we add our new menu to the list of all the other HUD elements, so we can receive our new message, and get updated, etc.

HOOK_MESSAGE ( MenuOpen ) just lets HL know that our class wants the MenuOpen message.

Finally, we init our variables by calling InitHUDData() and then we return 1 to show everything is OK.

void CNewMenu::InitHUDData ( void )
{
UnpackRGB ( sRed, sGreen, sBlue, RGB_REDISH );
UnpackRGB ( Red, Green, Blue, RGB_YELLOWISH );

m_CurrentMenu = NULL;
m_bMenuDisplayed = FALSE;
m_bCanCancelMenu = FALSE;
m_iTotalSlots = 0;
m_iCurrentSelection = 0;

}

Here, we are initializing all the variables. The thing of most interest to you would be the setting of the RGB values for the highlighted and non-highlighted items in the menu. We�re making them variables so it will be easier for you to modify the colors if you don�t want yellow and red.

void CNewMenu::Reset ( void )
{

}

int CNewMenu::VidInit ( void )
{
return 1;
}

Not much going on here, we�re just putting them here just incase you want them later.

The Draw function

int CNewMenu::Draw ( float flTime )
{
int x_pos = ScreenWidth / 4;
int y_pos = (ScreenHeight - (12 * m_iTotalSlots))/2;

int char_widths = gHUD.m_scrinfo.charWidths[' '];

int x_rel;

for ( int i = 0; i < m_iTotalSlots; i++ )
{
x_rel = x_pos;
if ( m_CurrentMenu[i].Align == MENU_ALIGN_CENTER )
{
for ( int cs = 0; m_CurrentMenu[i].Text[cs] != '\0'; cs++ ) { }

x_rel = (ScreenWidth - (15*cs))/2;
}

if ( i == m_iCurrentSelection )
{
gHUD.DrawHudString ( x_rel, y_pos, ScreenWidth, m_CurrentMenu[i].Text, sRed, sGreen, sBlue );
}
else
gHUD.DrawHudString ( x_rel, y_pos, ScreenWidth, m_CurrentMenu[i].Text, Red, Green, Blue );

y_pos += 12;
}

return 1;
}

This is pretty simple. All that is going on is that we�re getting the X and Y coords for the menu so we can draw it centered, and then x_rel is used for center aligned items. Other than that, we�re just cycling through the array and drawing the Text.

The Message Function


int CNewMenu::MsgFunc_MenuOpen ( const char *pszName, int iSize, void *pbuf )
{
BEGIN_READ ( pbuf, iSize );

int menu = READ_BYTE ();
m_bCanCancelMenu = READ_BYTE ();


m_CurrentMenu = TestMenu;
m_iTotalSlots = 10;
m_iCurrentSelection = 0;

if ( !m_CurrentMenu[m_iCurrentSelection].Selectable )
SelectNextOption ();

// switch ( menu )
// {
// case 1:
// break;

// default:
// break;
// }

m_iFlags |= HUD_ACTIVE;
m_bMenuDisplayed = TRUE;


return 1;
}

In this menu, we�re getting the menu to be displayed (which is sent as a byte, you can set up a switch statement to set up the CurrentMenu, TotalSlots, etc ) and whether or not it can be canceled ( another byte ). In the server dll, the protocall would be something like this:

WRITE_BYTE ( menu_number );
WRITE_BYTE ( TRUE or FALSE );

After that, we set up the menu so its active so it can be drawn.

The Next/Previous/Selection Functions

void CNewMenu::SelectItem ( void )
{
if ( (m_CurrentMenu) && (m_bMenuDisplayed) && ( m_CurrentMenu[m_iCurrentSelection].Selectable) )
{
char cmd[32];

sprintf ( cmd, "menuselect %d\n", m_CurrentMenu[m_iCurrentSelection].ReturnNumber );

ClientCmd ( cmd );

m_CurrentMenu = NULL;
m_iTotalSlots = 0;
m_iCurrentSelection = 0;

m_bMenuDisplayed = FALSE;
m_bCanCancelMenu = FALSE;

m_iFlags &= ~HUD_ACTIVE;
gHUD.m_iKeyBits &= ~IN_ATTACK;
}
}

Before we send the "menuselect" command, we do some basic checking to make sure there is a menu. Etc. Then we find out which one is currentley highlight and return menuselect and its ReturnNumber. Then we clear the keybits of IN_ATTACK so if you have successive menus, the select command wouldn�t be carried onto the next one, and finally, we make the menu inactive.

void CNewMenu::SelectNextOption ( void )
{
if ( m_iCurrentSelection == (m_iTotalSlots - 1) )
m_iCurrentSelection = 0;

else
m_iCurrentSelection++;

while ( m_CurrentMenu[m_iCurrentSelection].Selectable != TRUE )
{
if ( m_iCurrentSelection == (m_iTotalSlots - 1) )
m_iCurrentSelection = 0;

else
m_iCurrentSelection++;
}
}

void CNewMenu::SelectPreviousOption ( void )
{
if ( m_iCurrentSelection == 0 )
m_iCurrentSelection = (m_iTotalSlots - 1);

else
m_iCurrentSelection--;

while ( m_CurrentMenu[m_iCurrentSelection].Selectable != TRUE )
{
if ( m_iCurrentSelection == 0 )
m_iCurrentSelection = (m_iTotalSlots - 1);

else
m_iCurrentSelection--;
}
}


Both these functions just cycle though the array and find a new selectable item, and change the currentselection to its index. Nothing fancy, you can probably figure out whats going on here just by looking at the code.

The Selection/Highlighting of our menu

Now that we have finished or newmenu.cpp, open up ammo.cpp and find CHudAmmo::Think (). At the very top of the function, add this code:

if (gHUD.m_iKeyBits & IN_ATTACK)
{
if ( gHUD.m_NewMenu.m_bMenuDisplayed )
{
gHUD.m_NewMenu.SelectItem ();
return;
}
}

Now, whenever the menu is displayed, and the player presses their fire button, it�ll select a menu item, cool huh?

Now go find void CHudAmmo::UserCmd_Close () and add this code at the very top.

if ( gHUD.m_NewMenu.m_bCanCancelMenu )
{
if ( gHUD.m_NewMenu.m_bMenuDisplayed )
{
gHUD.m_NewMenu.m_CurrentMenu = NULL;
gHUD.m_NewMenu.m_iCurrentSelection = 0;
gHUD.m_NewMenu.m_bMenuDisplayed = FALSE;
gHUD.m_NewMenu.m_iTotalSlots = FALSE;
gHUD.m_NewMenu.m_iFlags &= ~HUD_ACTIVE;
gHUD.m_NewMenu.m_bCanCancelMenu = FALSE;
return;
}
}

This checks to see if the menu is displayed, and if so, can it be canceled; if it can, them we get rid of the menu. If you want to, you can modify the menu so you can "Hide" it and bring it back later with TAB or some other command.

Now go find void CHudAmmo::UserCmd_NextWeapon(void) and add this code at the top:

if ( gHUD.m_NewMenu.m_bMenuDisplayed )
{
gHUD.m_NewMenu.SelectNextOption ();
return;
}

This Code will advance the menu item, blah blah blah.

And again, in void CHudAmmo::UserCmd_PrevWeapon(void) add this at the top:

if ( gHUD.m_NewMenu.m_bMenuDisplayed )
{
gHUD.m_NewMenu.SelectPreviousOption ();
return;
}

This makes the menu go back one.

The Test Menu and Defining a New Menu

At the top of newmenu.cpp is where you should create your new menu�s, here is the test menu, this is basically how all others should be created.

menu_struct_t TestMenu[] =
{
{ "T e s t M e n u", MENU_ALIGN_CENTER, FALSE, -1 },
{ "", MENU_ALIGN_LEFT, FALSE, -1 },
{ "Please Choose a Team", MENU_ALIGN_CENTER, FALSE, -1 },
{ "Red Team", MENU_ALIGN_LEFT, TRUE, 1 },
{ "Blue Team", MENU_ALIGN_LEFT, TRUE, 2 },
{ "Green Team", MENU_ALIGN_LEFT, TRUE, 3 },
{ "Yellow Team", MENU_ALIGN_LEFT, TRUE, 4 },
{ "", MENU_ALIGN_LEFT, FALSE, -1 },
{ "Use \'[\' and \']\'", MENU_ALIGN_LEFT, FALSE, -1 },
{ "To Move The Selection", MENU_ALIGN_LEFT, FALSE, -1 }
};

The FALSE�s and TRUE�s are the bools for whether or not that specific item is selectable, the FALSE�s are accompanied by -1�s, it doesn�t matter for those, but for the TRUE�s make sure you get the right number.

In the server dll, register the new message like this:

At the top of player.cpp, around all the other gmsg�s add this:

int gmsgMenuOpen = 0;

Then down by all the REG_USER_MSG�s, add this

gmsgMenuOpen = REG_USER_MSG ( �MenuOpen�, -1 );

And if you want to call the menu, this is how you would do it:

MESSAGE_BEGIN ( MSG_ONE, gmsgMenuOpen, NULL, pev );
// the pev is the player the menu is going to
WRITE_BYTE ( menu );
// the number of the menu
WRITE_BYTE ( canbecanceled ); // true/false
MESSAGE_END ();

That�s it! We�re done! Feel free to email with questions or comments about anything, or if you would like the full source.

 

blank.gif (837 bytes)
bl.jpg (2471 bytes) blank.gif (837 bytes) br.jpg (2132 bytes)