EDITOR NOTES:
* This tutorial exists on the old site; all I did was convert it, added
formatting and colors. If you discover any mistakes I may have missed,
please let me know =)
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 our 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:
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. |