Custom Menu Screen

From Valve Developer Community
Jump to: navigation, search
Русский

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

Menu: Before and After


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 Material Creation 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
}

Photoshop CS does not add transparency automatically to TGA files. Although, they can be exported in another format and then converted to TGA with a different program (e.g. PNG and ImageReady), it really isn't necessary. Just select the area that you want to become transparent. Then go to Select at the top and then hit Inverse. Now On the window where you see your layers there should be some tabs. Click on the "channels" tab. You should see a red, green, blue, and rgb channel. At the bottom there should be some buttons one of them looks like a square with a circle in it. Click that, it should create a new channel with the heading Alpha1 or Alpha. The way that alpha works is that anything that is white will show up and anything that is black will become transparent. Save this as a tga file and you have created a texture with a transparent background.


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 for this 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"
	}
}

Note that there is no Friends option; it wasn't necessary for the project this tutorial was based on. Adding it should be fairly trivial, however.

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.

An in-depth explanation follows this code.

vgui_Panel_MainMenu.cpp
Create in VS.NET 2003:

//========= Copyright © 2006, Valve 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 );
	}

	// The panel background image should 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, everything will be in different places than at the root menu!
		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;
			}
		}
	}
	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
	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 the source mod, 
// hence 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:

#include "vgui_Panel_MainMenu.h" // Menu Panel to your includes, at the top.

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.

SMenu->Destroy(); // to VGui_Shutdown().
Note:The above code was customized for a specific mod; you may need to customize this code to your need.


Logic

A panel for placing your menu images must be created. This panel will be aligned with the Menu panel, and appear behind it. Then the original Menu panel will be rendered invisible by editing GameMenu.res again 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;

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 changed the pointer names, you should do a Find & Replace, and do a "Replace All" for the old pointer name to replace 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 works.


OnThink

Here's where some clever ideas for doing rollovers and layout positioning are brought into play.

Layout Positioning

		// In-game, everything will be in different places than at the root menu!
		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 in-game. In HL2 SP, there are two new menu options, Resume Game and Save Game. This makes sure the custom menu options re-align to compensate.

For your mod, you will need to change dy to a better value. For the mod this tutorial was taken from, a 40 pixel difference between menu options was required 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 the position 40 pixels down because 'Resume Game' pushed us down.

For menu options underneath 'Save Game' the position needs 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 the mod is on the menu screen and had switched layouts earlier. If so, it needs to reset the layout to the default.

Rollovers

Creating rollovers was initially troublesome. Using just one function, passing the button and image pointers, resulted in a memory leak that eventually caused hl2.exe to crash. Individual functions were required to avoid this problem. If any of this code needs improving, it's this part. But it's functional.

// 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);

This code is fairly self-explanatory; get the cursor's coordinates, then call the individual button rollover handlers. This tutorial will only outline one handler, as they are all basically the same.

The code also gets the position of our frame on screen to make sure the "capture rectangles" are correct. Then it calls the 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 the rollover code:

If the mouse is within the bounds of our specific Button, switch the image to the rollover image. If not, keep it off.

The InRolloverBegin Boolean value is put in because when it avoids the memory leak in hl2.exe. It has something to do with the SetImage function; what, though, is the question. While this hack works for now, it definitely could use additional work.

A note on the Save and Resume rollover function: It checks 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."

Properly aligned

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 you 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 the source mod, the 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 need to add these spaces if your image is longer, otherwise the invisible label won't activate.

Proper spacing

Conclusion

So that's that. You should now have a working custom menu.