Custom Menu Screen
Introduction
Requirements
- An understanding of the VGUI Documentation.
- Beginning/Intermediate knowledge of C++.
- Ready-made images for use on your menu.
- VS.NET 2003 and HL2 SP SDK
- Knowledge and familiarity with the SDK
Note: This was made with the HL2 SP SDK, but I'm sure it would take little to no editing for it to work in a MP game.
What You Will Learn
- To manipulate VGUI and emulate the effect of a custom menu.
- To create image rollovers
- How to create a new panel
Known Bugs
None; however, there is probably room for code optimizations.
Use of this Page
Please feel free to update the code once you've tested it and made sure there are no memory leaks. As of right now, this code does work.
Tutorial
Step 1: Your Images
This tutorial assumes you want to create a menu that uses images you created instead of the default text-only menu. As such, you will need to have made ready and prepared the VTF and VMT files for the following:
- New Game
- Load Game
- Options
- Quit
- Save Game
- Resume Game
- Friends (Note: This tutorial does not include using Friends, but the implementation is easy enough)
Each menu option should have two images. An image that will be shown when the mouse is off it, and an image that is shown when the mouse is on it. If you DO NOT want rollovers, you merely need images for your menu options.
This tutorial will not cover how to create TGAs, VTFs, or VMTs. Please read Creating Materials for information on how to create materials. You must put these images in the materials/vgui/
folder (or, at least your VMTs) for use in this tutorial.
Here's an example file list for "New Game":
menu_newgame.vtf
menu_newgame.vmt
menu_newgame_over.vtf
menu_newgame_over.vmt
Notes
When compiling your TGA file, in your image.txt file you will need:
"nonice" "1" "nolod" "1" "nomip" "1"
This will prevent your images from have a Level of Detail setting (so as not to degrade in quality when Texture Quality is set to low).
In your VMT, you should have "$translucent" 1
to make sure your images are transparent. Example:
"UnlitGeneric" { "$baseTexture" "vgui/menu_newgame" "$translucent" 1 }
While I was making my TGA files, I noticed that there was no transparency when I created them with Photoshop CS. I had to save them first as PNGs, then open them in ImageReady and export them as TGA files for it to work.
Step 2: GameMenu.res
If you do not have the file your_mod/resource/GameMenu.res
in your mod directory, you will need to create one. Here is the one we'll be using for the tutorial:
"GameMenu" { "1" { "label" "Resume Game" "command" "ResumeGame" "OnlyInGame" "1" } "5" { "label" "New Game" "command" "OpenNewGameDialog" "notmulti" "1" } "6" { "label" "Load Game" "command" "OpenLoadGameDialog" "notmulti" "1" } "7" { "label" "Save Game" "command" "OpenSaveGameDialog" "notmulti" "1" "OnlyInGame" "1" } "12" { "label" "Options" "command" "OpenOptionsDialog" } "13" { "label" "Quit" "command" "Quit" } }
You might notice there is no Friends option. You can add it anywhere you want, but for my purposes I didn't need it so this tutorial assumes you don't either.
Step 3: Creating Your Invisible Panel
There is a good tutorial on creating your own panels, if you don't know the explanations you should read VGUI2: Creating a panel to help understand the following code.
We will go into depth after this code segment.
vgui_Panel_MainMenu.cpp
Create in VS.NET 2003:
//========= Copyright © 2006, Valde Productions, All rights reserved. ============// // // Purpose: Display Main Menu images, handles rollovers as well // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "vgui_Panel_MainMenu.h" #include "vgui_controls/Frame.h" #include <vgui/ISurface.h> #include <vgui/IVGui.h> #include <vgui/IInput.h> #include "vgui_controls/Button.h" #include "vgui_controls/ImagePanel.h" using namespace vgui; // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" //----------------------------------------------------------------------------- // Purpose: Displays the logo panel //----------------------------------------------------------------------------- class CMainMenu : public vgui::Frame { DECLARE_CLASS_SIMPLE(CMainMenu, vgui::Frame); public: CMainMenu( vgui::VPANEL parent ); ~CMainMenu(); virtual void OnCommand(const char *command); virtual void ApplySchemeSettings( vgui::IScheme *pScheme ) { BaseClass::ApplySchemeSettings( pScheme ); } // We want the panel background image to be square, not rounded. virtual void PaintBackground() { SetBgColor(Color(0,0,0,0)); SetPaintBackgroundType( 0 ); BaseClass::PaintBackground(); } virtual void PerformLayout() { // re-position SetPos(vgui::scheme()->GetProportionalScaledValue(defaultX), vgui::scheme()->GetProportionalScaledValue(defaultY)); BaseClass::PerformLayout(); } void CMainMenu::PerformDefaultLayout() { m_pButtonBegin->SetPos(0, 0); m_pImgBegin->SetPos(0,0); m_pButtonLoad->SetPos(0, 40); m_pImgLoad->SetPos(0,40); m_pButtonOptions->SetPos(0, 80); m_pImgOptions->SetPos(0,80); m_pButtonLeave->SetPos(0, 120); m_pImgLeave->SetPos(0,120); m_pImgSave->SetVisible(false); m_pButtonSave->SetVisible(false); m_pImgResume->SetVisible(false); m_pButtonResume->SetVisible(false); InRolloverResume=false; InRolloverBegin=false; InRolloverLoad=false; InRolloverOptions=false; InRolloverLeave=false; } virtual void OnThink() { // In-game, we need to move everything around a bit! if (InGame() && !InGameLayout) { DevMsg("Performing menu layout\n"); int dy = 40; // delta y, shift value int x,y; // Resume m_pButtonResume->SetPos(0,0); m_pImgResume->SetPos(0,0); m_pButtonResume->SetVisible(true); m_pImgResume->SetVisible(true); m_pButtonBegin->GetPos(x,y); m_pButtonBegin->SetPos(x,y+dy); m_pImgBegin->GetPos(x,y); m_pImgBegin->SetPos(x,y+dy); m_pButtonLoad->GetPos(x,y); m_pButtonLoad->SetPos(x,y+dy); m_pImgLoad->GetPos(x,y); m_pImgLoad->SetPos(x,y+dy); // Save game m_pButtonSave->SetPos(x,y+(2*dy)); m_pImgSave->SetPos(x,y+(2*dy)); m_pButtonSave->SetVisible(true); m_pImgSave->SetVisible(true); m_pButtonOptions->GetPos(x,y); m_pButtonOptions->SetPos(x,y+(2*dy)); m_pImgOptions->GetPos(x,y); m_pImgOptions->SetPos(x,y+(2*dy)); // Options moves under Save game, so twice as far m_pButtonLeave->GetPos(x,y); m_pButtonLeave->SetPos(x,y+(2*dy)); m_pImgLeave->GetPos(x,y); m_pImgLeave->SetPos(x,y+(2*dy)); // Leave game moves under Save game, so twice as far InGameLayout = true; } if (!InGame() && InGameLayout) { PerformDefaultLayout(); InGameLayout = false; } // Get mouse coords int x,y; vgui::input()->GetCursorPos(x,y); int fx,fy; // frame xpos, ypos GetPos(fx,fy); CheckRolloverBegin(x,y,fx,fy); CheckRolloverResume(x,y,fx,fy); CheckRolloverLoad(x,y,fx,fy); CheckRolloverSave(x,y,fx,fy); CheckRolloverOptions(x,y,fx,fy); CheckRolloverLeave(x,y,fx,fy); BaseClass::OnThink(); } void CheckRolloverBegin(int x,int y, int fx, int fy) { int bx,by,bw,bh; // button xpos, ypos, width, height m_pButtonBegin->GetPos(bx,by); m_pButtonBegin->GetSize(bw,bh); bx = bx+fx; // xpos for button (rel to screen) by = by+fy; // ypos for button (rel to screen) // Check and see if mouse cursor is within button bounds if ((x > bx && x < bx+bw) && (y > by && y < by+bh)) { if(!InRolloverBegin) { m_pImgBegin->SetImage("menu_begin_over"); InRolloverBegin = true; } } else { if(InRolloverBegin) { m_pImgBegin->SetImage("menu_begin"); InRolloverBegin = false; } } } void CheckRolloverResume(int x,int y, int fx, int fy) { if(m_pButtonResume->IsVisible()) { int bx,by,bw,bh; // button xpos, ypos, width, height m_pButtonResume->GetPos(bx,by); m_pButtonResume->GetSize(bw,bh); bx = bx+fx; // xpos for button (rel to screen) by = by+fy; // ypos for button (rel to screen) // Check and see if mouse cursor is within button bounds if ((x > bx && x < bx+bw) && (y > by && y < by+bh)) { if(!InRolloverResume) { m_pImgResume->SetImage("menu_Resume_over"); InRolloverResume = true; } } else { if(InRolloverResume) { m_pImgResume->SetImage("menu_Resume"); InRolloverResume = false; } } } } void CheckRolloverLoad(int x,int y, int fx, int fy) { int bx,by,bw,bh; // button xpos, ypos, width, height m_pButtonLoad->GetPos(bx,by); m_pButtonLoad->GetSize(bw,bh); bx = bx+fx; // xpos for button (rel to screen) by = by+fy; // ypos for button (rel to screen) // Check and see if mouse cursor is within button bounds if ((x > bx && x < bx+bw) && (y > by && y < by+bh)) { if(!InRolloverLoad) { m_pImgLoad->SetImage("menu_load_over"); InRolloverLoad = true; } } else { if(InRolloverLoad) { m_pImgLoad->SetImage("menu_load"); InRolloverLoad = false; } } } void CheckRolloverSave(int x,int y, int fx, int fy) { if(m_pButtonSave->IsVisible()) { int bx,by,bw,bh; // button xpos, ypos, width, height m_pButtonSave->GetPos(bx,by); m_pButtonSave->GetSize(bw,bh); bx = bx+fx; // xpos for button (rel to screen) by = by+fy; // ypos for button (rel to screen) // Check and see if mouse cursor is within button bounds if ((x > bx && x < bx+bw) && (y > by && y < by+bh)) { if(!InRolloverSave) { m_pImgSave->SetImage("menu_Save_over"); InRolloverSave = true; } } else { if(InRolloverSave) { m_pImgSave->SetImage("menu_Save"); InRolloverSave = false; } } } } void CheckRolloverOptions(int x,int y, int fx, int fy) { int bx,by,bw,bh; // button xpos, ypos, width, height m_pButtonOptions->GetPos(bx,by); m_pButtonOptions->GetSize(bw,bh); bx = bx+fx; // xpos for button (rel to screen) by = by+fy; // ypos for button (rel to screen) // Check and see if mouse cursor is within button bounds if ((x > bx && x < bx+bw) && (y > by && y < by+bh)) { if(!InRolloverOptions) { m_pImgOptions->SetImage("menu_Options_over"); InRolloverOptions = true; } } else { if(InRolloverOptions) { m_pImgOptions->SetImage("menu_Options"); InRolloverOptions = false; } } } void CheckRolloverLeave(int x,int y, int fx, int fy) { int bx,by,bw,bh; // button xpos, ypos, width, height m_pButtonLeave->GetPos(bx,by); m_pButtonLeave->GetSize(bw,bh); bx = bx+fx; // xpos for button (rel to screen) by = by+fy; // ypos for button (rel to screen) // Check and see if mouse cursor is within button bounds if ((x > bx && x < bx+bw) && (y > by && y < by+bh)) { if(!InRolloverLeave) { m_pImgLeave->SetImage("menu_Leave_over"); InRolloverLeave = true; } } else { if(InRolloverLeave) { m_pImgLeave->SetImage("menu_Leave"); InRolloverLeave = false; } } } // Are we in-game? bool CMainMenu::InGame() { C_BasePlayer *pPlayer = C_BasePlayer::GetLocalPlayer(); if(pPlayer && IsVisible()) { return true; } else { return false; } } private: vgui::ImagePanel *m_pImgBegin; vgui::ImagePanel *m_pImgResume; vgui::ImagePanel *m_pImgLoad; vgui::ImagePanel *m_pImgSave; vgui::ImagePanel *m_pImgOptions; vgui::ImagePanel *m_pImgLeave; vgui::Button *m_pButtonBegin; vgui::Button *m_pButtonResume; vgui::Button *m_pButtonLoad; vgui::Button *m_pButtonSave; vgui::Button *m_pButtonOptions; vgui::Button *m_pButtonLeave; int defaultX; int defaultY; bool InGameLayout; bool InRolloverBegin; bool InRolloverResume; bool InRolloverLoad; bool InRolloverSave; bool InRolloverOptions; bool InRolloverLeave; }; //----------------------------------------------------------------------------- // Purpose: Constructor //----------------------------------------------------------------------------- CMainMenu::CMainMenu( vgui::VPANEL parent ) : BaseClass( NULL, "CMainMenu" ) { LoadControlSettings( "resource/UI/MainMenu.res" ); // Optional, don't need this SetParent( parent ); SetTitleBarVisible( false ); SetMinimizeButtonVisible( false ); SetMaximizeButtonVisible( false ); SetCloseButtonVisible( false ); SetSizeable( false ); SetMoveable( false ); SetProportional( true ); SetVisible( true ); SetKeyBoardInputEnabled( false ); SetMouseInputEnabled( false ); //ActivateBuildMode(); SetScheme("MenuScheme.res"); // These coords are relative to a 640x480 screen // Good to test in a 1024x768 resolution. defaultX = 60; // x-coord for our position defaultY = 240; // y-coord for our position InGameLayout = false; // Size of the panel, I made it big enough... SetSize(512,512); SetZPos(-1); // we're behind everything // Load invisi buttons // Initialize images m_pImgBegin = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Begin")); m_pImgResume = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Resume")); m_pImgLoad = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Load")); m_pImgSave = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Save")); m_pImgOptions = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Options")); m_pImgLeave = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Leave")); // New game m_pButtonBegin = vgui::SETUP_PANEL(new vgui::Button(this, "btnBegin", "")); m_pButtonBegin->SetSize(300, 28); m_pButtonBegin->SetPaintBorderEnabled(false); m_pButtonBegin->SetPaintEnabled(false); m_pImgBegin->SetImage("menu_begin"); // Resume m_pButtonResume = vgui::SETUP_PANEL(new vgui::Button(this, "btnResume", "")); m_pButtonResume->SetSize(170, 28); m_pButtonResume->SetPaintBorderEnabled(false); m_pButtonResume->SetPaintEnabled(false); m_pImgResume->SetImage("menu_resume"); // Load m_pButtonLoad = vgui::SETUP_PANEL(new vgui::Button(this, "btnLoad", "")); m_pButtonLoad->SetSize(190, 28); m_pButtonLoad->SetPaintBorderEnabled(false); m_pButtonLoad->SetPaintEnabled(false); m_pImgLoad->SetImage("menu_load"); // Save m_pButtonSave = vgui::SETUP_PANEL(new vgui::Button(this, "btnSave", "")); m_pButtonSave->SetSize(190, 28); m_pButtonSave->SetPaintBorderEnabled(false); m_pButtonSave->SetPaintEnabled(false); m_pImgSave->SetImage("menu_save"); // Options m_pButtonOptions = vgui::SETUP_PANEL(new vgui::Button(this, "btnOptions", "")); m_pButtonOptions->SetSize(170, 28); m_pButtonOptions->SetPaintBorderEnabled(false); m_pButtonOptions->SetPaintEnabled(false); m_pImgOptions->SetImage("menu_options"); // Leave m_pButtonLeave = vgui::SETUP_PANEL(new vgui::Button(this, "btnLeave", "")); m_pButtonLeave->SetSize(180, 28); m_pButtonLeave->SetPaintBorderEnabled(false); m_pButtonLeave->SetPaintEnabled(false); m_pImgLeave->SetImage("menu_leave"); PerformDefaultLayout(); } void CMainMenu::OnCommand(const char *command) { BaseClass::OnCommand(command); } //----------------------------------------------------------------------------- // Purpose: Destructor //----------------------------------------------------------------------------- CMainMenu::~CMainMenu() { } // Class // Change CSMenu to CModMenu if you want. Salient is the name of my mod, so I did SMenu. // If you change CSMenu, change ISMenu too where they all appear. class CSMenu : public ISMenu { private: CMainMenu *MainMenu; vgui::VPANEL m_hParent; public: CSMenu( void ) { MainMenu = NULL; } void Create( vgui::VPANEL parent ) { // Create immediately MainMenu = new CMainMenu(parent); } void Destroy( void ) { if ( MainMenu ) { MainMenu->SetParent( (vgui::Panel *)NULL ); delete MainMenu; } } }; static CSMenu g_SMenu; ISMenu *SMenu = ( ISMenu * )&g_SMenu;
vgui_Panel_MainMenu.h
#include <vgui/VGUI.h> namespace vgui { class Panel; } class ISMenu { public: virtual void Create( vgui::VPANEL parent ) = 0; virtual void Destroy( void ) = 0; }; extern ISMenu *SMenu;
vgui_int.cpp
Open up vgui_int.cpp
and add the following:
Add #include "vgui_Panel_MainMenu.h" // Menu Panel
to your includes, at the top.
Add SMenu->Create( GameUiDll );
to the VGui_CreateGlobalPanels
function. If you don't have GameUiDll defined, then add: VPANEL GameUiDll = enginevgui->GetPanel( PANEL_GAMEUIDLL);
in the same function.
Add SMenu->Destroy();
to VGui_Shutdown()
.
Note: The above code is customized for my mod. You may need to customize this code to your need.
Logic
We are creating a panel that will be aligned with the Menu panel and appear behind it.
The purpose of this is to create a panel to place your menu images. Then we will edit GameMenu.res once again at the end to get rid of the text but not the menu items. That way, it will appear to the user as if you created a new menu system when in reality all you did was put an invisible panel behind the GameMenu panel.
Customizing To Your Needs
You should edit the following:
vgui::ImagePanel *m_pImgBegin; vgui::ImagePanel *m_pImgResume; vgui::ImagePanel *m_pImgLoad; vgui::ImagePanel *m_pImgSave; vgui::ImagePanel *m_pImgOptions; vgui::ImagePanel *m_pImgLeave; vgui::Button *m_pButtonBegin; vgui::Button *m_pButtonResume; vgui::Button *m_pButtonLoad; vgui::Button *m_pButtonSave; vgui::Button *m_pButtonOptions; vgui::Button *m_pButtonLeave; int defaultX; int defaultY; bool InGameLayout; bool InRolloverBegin; bool InRolloverResume; bool InRolloverLoad; bool InRolloverSave; bool InRolloverOptions; bool InRolloverLeave;
And replace the Buttons and ImagePanels with the options you need. The boolean values concern the individual rollover states of your menu options.
Once you've done that, if you've changed the pointer names, you should do a Find & Replace, and do a "Replace All" for the old pointer name to change it with your new one.
Next, you will need to initialize your Buttons and ImagePanels:
// Load invisi buttons m_pImgBegin = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Begin")); m_pImgResume = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Resume")); m_pImgLoad = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Load")); m_pImgSave = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Save")); m_pImgOptions = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Options")); m_pImgLeave = vgui::SETUP_PANEL(new vgui::ImagePanel(this, "Leave")); // New game m_pButtonBegin = vgui::SETUP_PANEL(new vgui::Button(this, "btnBegin", "")); m_pButtonBegin->SetSize(300, 28); m_pButtonBegin->SetPaintBorderEnabled(false); m_pButtonBegin->SetPaintEnabled(false); m_pImgBegin->SetImage("menu_begin"); // Resume m_pButtonResume = vgui::SETUP_PANEL(new vgui::Button(this, "btnResume", "")); m_pButtonResume->SetSize(170, 28); m_pButtonResume->SetPaintBorderEnabled(false); m_pButtonResume->SetPaintEnabled(false); m_pImgResume->SetImage("menu_resume"); // Load m_pButtonLoad = vgui::SETUP_PANEL(new vgui::Button(this, "btnLoad", "")); m_pButtonLoad->SetSize(190, 28); m_pButtonLoad->SetPaintBorderEnabled(false); m_pButtonLoad->SetPaintEnabled(false); m_pImgLoad->SetImage("menu_load"); // Save m_pButtonSave = vgui::SETUP_PANEL(new vgui::Button(this, "btnSave", "")); m_pButtonSave->SetSize(190, 28); m_pButtonSave->SetPaintBorderEnabled(false); m_pButtonSave->SetPaintEnabled(false); m_pImgSave->SetImage("menu_save"); // Options m_pButtonOptions = vgui::SETUP_PANEL(new vgui::Button(this, "btnOptions", "")); m_pButtonOptions->SetSize(170, 28); m_pButtonOptions->SetPaintBorderEnabled(false); m_pButtonOptions->SetPaintEnabled(false); m_pImgOptions->SetImage("menu_options"); // Leave m_pButtonLeave = vgui::SETUP_PANEL(new vgui::Button(this, "btnLeave", "")); m_pButtonLeave->SetSize(180, 28); m_pButtonLeave->SetPaintBorderEnabled(false); m_pButtonLeave->SetPaintEnabled(false); m_pImgLeave->SetImage("menu_leave");
You will need to make sure everything is labeled correctly and the image names are correct. Images are relative to your materials/vgui/
directory.
When testing the width and height of your buttons, you will need to set SetPaintBorderEnabled(false)
to SetPaintBorderEnabled(true)
so you can make sure your button is big enough to fit around your image. After everything is finished, you can set it to false.
Now, about PerformDefaultLayout:
void CMainMenu::PerformDefaultLayout() { m_pButtonBegin->SetPos(0, 0); m_pImgBegin->SetPos(0,0); m_pButtonLoad->SetPos(0, 40); m_pImgLoad->SetPos(0,40); m_pButtonOptions->SetPos(0, 80); m_pImgOptions->SetPos(0,80); m_pButtonLeave->SetPos(0, 120); m_pImgLeave->SetPos(0,120); m_pImgSave->SetVisible(false); m_pButtonSave->SetVisible(false); m_pImgResume->SetVisible(false); m_pButtonResume->SetVisible(false); InRolloverResume=false; InRolloverBegin=false; InRolloverLoad=false; InRolloverOptions=false; InRolloverLeave=false; }
This function is for resetting your menu to the default layout; for example when disconnecting from a game or on startup.
Again, make sure your labels are all correct. The Save and Resume buttons and images are not set to a default position as they only appear when in-game. Thus, in another function do we set the position. All this makes sure of is that they are invisible whenever we're on the main menu.
About the PerformLayout function: It is just making sure the menu is aligned properly for each resolution. It's not perfect, but it's the best I could do.
OnThink
Here's where we use some clever ideas in doing rollovers and layout positioning.
Layout Positioning
// In-game, we need to move everything around a bit! if (InGame() && !InGameLayout) { //DevMsg("Performing menu layout\n"); int dy = 40; // delta y, shift value int x,y; // Resume m_pButtonResume->SetPos(0,0); m_pImgResume->SetPos(0,0); m_pButtonResume->SetVisible(true); m_pImgResume->SetVisible(true); m_pButtonBegin->GetPos(x,y); m_pButtonBegin->SetPos(x,y+dy); m_pImgBegin->GetPos(x,y); m_pImgBegin->SetPos(x,y+dy); m_pButtonLoad->GetPos(x,y); m_pButtonLoad->SetPos(x,y+dy); m_pImgLoad->GetPos(x,y); m_pImgLoad->SetPos(x,y+dy); // Save game m_pButtonSave->SetPos(x,y+(2*dy)); m_pImgSave->SetPos(x,y+(2*dy)); m_pButtonSave->SetVisible(true); m_pImgSave->SetVisible(true); m_pButtonOptions->GetPos(x,y); m_pButtonOptions->SetPos(x,y+(2*dy)); m_pImgOptions->GetPos(x,y); m_pImgOptions->SetPos(x,y+(2*dy)); // Options moves under Save game, so twice as far m_pButtonLeave->GetPos(x,y); m_pButtonLeave->SetPos(x,y+(2*dy)); m_pImgLeave->GetPos(x,y); m_pImgLeave->SetPos(x,y+(2*dy)); // Leave game moves under Save game, so twice as far InGameLayout = true; } if (!InGame() && InGameLayout) { PerformDefaultLayout(); InGameLayout = false; }
This code is for when we're in-game. In HL2 SP, we have two new menu options, Resume Game and Save Game. This makes sure our menu options re-align to compensate.
For your mod, you will need to change dy
to a better value. For my mod, I needed a 40 pixel difference between menu options for the images to be aligned correctly. You will need to make this more or less depending on your needs.
For this tutorial all the layout changes reflect the GameMenu.res. If you added options or removed options, you need to edit this code. Basically the logic is this:
Get Previous position (x,y) Set new position to be same x distance, but move to y + our shift value. So, move us 40 pixels down because 'Resume game' pushed us down.
For menu options underneath 'Save game' we need to move twice as far down because 'Save game' took up a slot above us.
Otherwise, the rest shouldn't need to be edited (again, make sure labels are correct). The second 'if' statement checks to see if we're on the menu screen and we had switched layouts earlier. If so, we need to reset our layout to the default.
Rollovers
I had some trouble doing rollovers. I tried making one function and passing the button points and image pointers, but hl2.exe would keep using up more and more memory and eventually crash. So I had to resort to individual functions. If any of this code needs improving, it's this part. Alas, this works, so here we go.
// Get mouse coords int x,y; vgui::input()->GetCursorPos(x,y); int fx,fy; // frame xpos, ypos GetPos(fx,fy); CheckRolloverBegin(x,y,fx,fy); CheckRolloverResume(x,y,fx,fy); CheckRolloverLoad(x,y,fx,fy); CheckRolloverSave(x,y,fx,fy); CheckRolloverOptions(x,y,fx,fy); CheckRolloverLeave(x,y,fx,fy);
So, we want to get our cursor's coordinates. Then we call the individual button rollover handlers. In this tutorial, I'll only explain one because they are all basically the same only with their respective parts replaced.
We also get the position of our frame on screen to make sure our "capture rectangles" are correct. Then we call our rollover functions:
void CheckRolloverBegin(int x,int y, int fx, int fy) { int bx,by,bw,bh; // button xpos, ypos, width, height m_pButtonBegin->GetPos(bx,by); m_pButtonBegin->GetSize(bw,bh); bx = bx+fx; // xpos for button (rel to screen) by = by+fy; // ypos for button (rel to screen) // Check and see if mouse cursor is within button bounds if ((x > bx && x < bx+bw) && (y > by && y < by+bh)) { if(!InRolloverBegin) { m_pImgBegin->SetImage("menu_begin_over"); InRolloverBegin = true; } } else { if(InRolloverBegin) { m_pImgBegin->SetImage("menu_begin"); InRolloverBegin = false; } } }
Here's the logic for our rollover code:
If the mouse is within the bounds of our specific Button, we switch the image to the rollover image. If not, we keep it off.
The InRolloverBegin boolean value is put in because when I was examining the hl2.exe process, I noticed without it the memory usuage keeps going up gradually. When I put it in, it stays the same. I don't know what's causing it... I know for a fact it has to do with the SetImage function. Without that, it's just fine. Please update this Wiki if you can figure out the problem. For right now, this is my hack for it.
A note on the Save and Resume rollover function: We just check to make sure the buttons are visible before executing the code. There's no use if the user can't see it.
Aligning
To align your Menu panel correctly, it will take require some trial and error where defaultX
and defaultY
is concerned. defaultX
and defaultY
are the coordinates where your top-left corner of the menu will be located. It basically needs to be aligned to the main menu so your first image (New Game) appears underneath the text "New Game."
If your images are bigger than the default font size, you will need to increase the font size to compensate. This can be done in SourceScheme.res. Edit the font MenuLarge
and change the "size" accordingly.
Other tips:
In GameMenu.res, change the labels to "N_________". Meaning, put the first letter of the menu option and then underscore until you find the correct length. Keep change defaultX and defaultY until your first image aligns with the first menu option text.
It is also useful to use PaintBorderEnabled(true)
for your buttons so you can see the border of your buttons.
You might also like to change the height (spacing) of the HL2 menu items. To do so, open up SourceScheme.res
and change MainMenu.MenuItemHeight to an appropriate value.
Then, for positioning your next image (in PerformDefaultLayout
), try using the MenuItemHeight value. You only need to find the spacing once and then replace my values with the double, triple, etc. For example, suppose you figure out you need your second image positioned at (x, 20). Then your third will be (x, 40), and your fourth, (x,60), etc. The dy
value in OnThink
will be 20 then.
Finalization
Once you've got everything aligned and tested, we need to replace your GameMenu.res labels with spaces. This is important, because otherwise the user can't click your images. Since the labels appear on top of your images, you need to make your label span your image using spaces. The spaces act like invisible letters, so that when the user mouses over your image, they can click it.
For example:
"label" " " "command" "OpenNewGameDialog"
If I have an image for New Game that is 200 pixels long, the corresponding label should be about that long, so the user can click the menu option. This involves some trial and error, but the best way to get it right is to pay attention to when and where the sound appears. You want to be able to hear the sound when the mouse enters the bounds of your image.
For my mod, my GameMenu.res ended up like this:
"GameMenu" { "1" { "label" " " "command" "ResumeGame" "OnlyInGame" "1" } "5" { "label" " " "command" "engine ToggleNewGame" "notmulti" "1" } "6" { "label" " " "command" "OpenLoadGameDialog" "notmulti" "1" } "7" { "label" " " "command" "OpenSaveGameDialog" "notmulti" "1" "OnlyInGame" "1" } "12" { "label" " " "command" "OpenOptionsDialog" } "13" { "label" " " "command" "Quit" } }
Note: If you leave the label blank, "label" " "
, this will only work if your image isn't too long. By default, blank menu items span about 60 pixels, so you neeed to add these spaces if your image is longer, otherwise the invisible label won't activate.
Conclusion
So that's that. You should now have a working custom menu. I think we covered everything. And if there are any missing pieces, feel free to add your comments.