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. |