Background info: While working on Fortress Live I decided to try and update the interfaces for quake Team Fortress to make them appear more "modern" to attract new players. My goal was to "mimic" some of the appearance and functionality of Half-Life's VGUI. The result, however, is a pretty dynamic and highly modable GUI system.
Half-Life's VGUI:
My GUI:
The Code:
Before we begin, I assume you have a working/compiling CSQC mod. If you are new to this or have never heard of CSQC, I suggest you take a look at these tutorials:
http://qexpo.tastyspleen.net/booth.php?id=165&page=308
http://qexpo.tastyspleen.net/booth.php?id=165&page=317
Please note that this tutorial was designed for FTE's CSQC. The CSQC implementation in Darkplaces differs in some areas, but those areas should be easily tweakable.
Also please note that I will not be giving lengthy details about every bit of code we're adding here because the code is heavily commented to supply you with the specifics.
To start off, we need to make sure all the defs are in place. Go in your defs.qc (or system.qc) all the way at the bottom and add these lines:
Code: Select all
// CSQC GUI
float in_menu, menu_selected, menu_impulse, mouse_branch;
float global_bt_no, global_menu_flags;
vector mousepos, testsize_one;
.string bt_text, bt_cmd, bt_img;
.float bt_flags, bt_no;
float vid_realwidth, vid_realheight; // the *real* width and height of the 2d rendered scene
float () MouseInBox;
void (string menu_name, float menu_flags) InitMenu;
void (string button_text, string button_cmd, vector button_spawnpos, string button_image, vector button_size, float button_flags) CreateButton;
#define MENU_TEST1 666
// global menu flags
#define MFG_NOCURSOR 1
// menu flags
#define MENUFLAG_DISABLED 1
#define MENUFLAG_BRANCH 2
Code: Select all
void(...) localcmd = #46;
float(string s) tokenize = #441;
string(float argnum) argv = #442;
float(string s) cvar = #45; // return cvar.value
float(string s) stof = #81;
string(float f) ftos = #26;
float(string st) strlen = #114;
entity() spawn = #14;
string(string s) cvar_string = #448;
string (vector v) vtos = #27;
string(string s, float start, float length) substring = #116;
entity (entity start, .string fld, string match) find = #18;
void (entity e) remove = #15;
Now that the definitions are in, create the following 2 files and add them in this order after defs.qc in your progs.src:
gui_intercept.qc
gui_draw.qc
Now open gui_intercept.qc and lets get biz-ay. Copy and paste the following into that one QC file:
Code: Select all
// Handle GUI events in menu-specific manners
// A button is clicked..
void ( entity t_button ) CSQC_GUI_ButtonClick =
{
switch(in_menu) {
default:
if (!menu_impulse) { // if the menu didn't already send the reply to the server regarding the menu..
menu_impulse = TRUE;
if (t_button.bt_flags & MENUFLAG_BRANCH)
mouse_branch = TRUE;
if (t_button.bt_cmd == "default")
localcmd("impulse ", ftos(t_button.bt_no), "\n");
else
localcmd(t_button.bt_cmd, "\n");
}
}
};
// When a mouse hovers over a button, apply this effect..
void (string button_image, vector button_position, vector button_size, string button_text) CSQC_GUI_MouseOverButton =
{
local float selected_button, stl;
selected_button = MouseInBox();
switch(in_menu) {
default:
drawpic(button_position, button_image, button_size, '1 1 1', .4);
drawstring(button_position + '10 12 0', button_text, '8 8 0', '1 1 1', 1);
break;
}
};
// Called before the menu buttons are rendered
void () CSQC_GUI_MenuBackground =
{
switch(in_menu) {
default: // No default background image
break;
}
};
// Called after the menu buttons are rendered
void () CSQC_GUI_MenuForeground =
{
switch(in_menu) {
default: // No default foreground image
break;
}
};
Now open up gui_draw.qc. This is where we tell quake to draw your images/texts. As before, copy and paste in the following:
Code: Select all
// CSQC GUI Stuff
// draws the selection box with text inside it (only draws one box style for now)
void (vector boxpos, vector boxsize, string boxtext, vector boxrgb, float boxalpha) spawnbox =
{
drawpic(boxpos, "progs/csqc/csqcguiback.tga", boxsize, boxrgb, boxalpha);
drawstring(boxpos + '10 12 0', boxtext, '8 8 0', boxrgb, 1);
};
void () CSQCGUI_ClearMenu =
{
local entity button;
// Remove all buttons
button = find(world, classname, "gui_button");
while (button)
{
remove(button);
button = find(button, classname, "gui_button");
}
global_bt_no = 0; // clear globally assigned button #s
menu_selected = 0;
in_menu = 0;
menu_impulse = 0;
// reset mouse pos
if (!mouse_branch) {
mousepos_x = vid_realwidth*.5;
mousepos_y = vid_realheight*.5;
}
else
mouse_branch = FALSE;
};
void () CSQCGUI_Render =
{
local float args, boxno, i;
entity button;
if (!in_menu)
return;
cprint("\n"); // clear the currently printed menu
CSQC_GUI_MenuBackground ();
button = find(world, classname, "gui_button");
while (button)
{
boxno = button.bt_no;
if (MouseInBox() == boxno) {
CSQC_GUI_MouseOverButton( button.bt_img, button.origin, button.size, button.bt_text );
if (menu_selected) {
CSQC_GUI_ButtonClick( button );
}
}
else {
if (button.bt_flags & MENUFLAG_DISABLED)
spawnbox(button.origin, button.size, button.bt_text, '.4 .4 .4', .2);
else
spawnbox(button.origin, button.size, button.bt_text, '1 1 1', .2);
}
button = find(button, classname, "gui_button");
}
CSQC_GUI_MenuForeground ();
if (!(global_menu_flags & MFG_NOCURSOR))
drawstring(mousepos, "X", '8 8 0', '1 0.91 0.51', .6); // Draw the "cursor"
};
void (string menu_name, float menu_flags) InitMenu =
{
localcmd("csqcgui_menu ", menu_name, " ", ftos(menu_flags), "\n");
global_menu_flags = menu_flags;
};
void (string button_text, string button_cmd, vector button_spawnpos, string button_image, vector button_size, float button_flags) CreateButton =
{
local entity newbutton;
global_bt_no++;
newbutton = spawn();
newbutton.classname = "gui_button";
newbutton.origin = button_spawnpos;
newbutton.size = button_size;
newbutton.bt_img = button_image;
newbutton.bt_text = button_text;
newbutton.bt_cmd = button_cmd;
newbutton.bt_flags = button_flags;
newbutton.bt_no = global_bt_no;
};
// Which box the mouse is hovering over currently
float () MouseInBox =
{
float in_box, boxval;
entity button;
button = find(world, classname, "gui_button");
while (button)
{
if (mousepos_y >= button.origin_y && mousepos_y <= button.origin_y+button.size_y)
if (mousepos_x >= button.origin_x && mousepos_x <= button.origin_x+button.size_x) {
if (!button.bt_flags & MENUFLAG_DISABLED)
boxval = button.bt_no;
break;
}
button = find(button, classname, "gui_button");
}
return boxval;
};
Now there are two key functions to any decent CSQC mod. The first, and most important, is CSQC_UpdateView(). CSQC_UpdateView, in a nutshell, runs each frame and renders pretty much everything you see. You need to add 3 lines of code to this function, right after renderscene(); is called:
Code: Select all
vid_realwidth = width;
vid_realheight = height;
CSQCGUI_Render();
Now in this same function, we also want to make sure that the crosshair is not drawn when we're in a menu. If you have a line like:
Code: Select all
setviewprop(VF_DRAWCROSSHAIR, 1);
Code: Select all
if (!in_menu || global_menu_flags & MFG_NOCURSOR)
setviewprop(VF_DRAWCROSSHAIR, 1);
Code: Select all
void(float width, float height, float menushown) CSQC_UpdateView =
{
clearscene(); //wipe the scene, and apply the default rendering values.
setviewprop(VF_MIN_X, 0); //set the left of the view
setviewprop(VF_MIN_Y, 0); //set the top of the view
setviewprop(VF_SIZE_X, width); //set how wide the view is (full width)
setviewprop(VF_SIZE_Y, height); //set how high the view is (full height)
setviewprop(VF_DRAWENGINESBAR, 1);
if (!in_menu || global_menu_flags & MFG_NOCURSOR)
setviewprop(VF_DRAWCROSSHAIR, 1);
// setviewprop(VF_FOV, cvar("fov")); //this is entirely optional FIXME: fov is x,y
//if you use prediction, you'll need the following four lines
// setviewprop(VF_ORIGIN, myvieworg); //change where the view is drawn
// setviewprop(VF_ANGLES, myviewang); //change the angle the view is drawn at
// makevectors(myviewang); //calc the listener directions
// setlistener(myvieworg, v_forward, v_right, v_up); //change where sounds come from
addentities(4|1|2);
renderscene();
// Set the console size vars
vid_realwidth = width;
vid_realheight = height;
CSQCGUI_Render();
};
CSQC_InputEvent tracks key presses to mouse movements and everything in between. You can do a ton of stuff with this function. For purposes of my GUI mod, it hijacks the mouse so we can select things in menus.
I'm just going to supply the whole function the way I have it, but if you have your own then just add the parts in the brackets from if ( in_menu && !(global_menu_flags & MFG_NOCURSOR) ) { to somewhere accessible in your code.
Code: Select all
// Catch inputs from the client like pressing a key or using the mouse (used for menus)
float(float eventtype, float param1, float param2) CSQC_InputEvent =
{
if ( in_menu && !(global_menu_flags & MFG_NOCURSOR) ) {
if (eventtype == 0) {//keypress down inputs
if (param1 == 512) { // Left Click
if (MouseInBox())
{
menu_selected = MouseInBox(); // Actual menu selection is handled in CSQCGUI_Render() for modability purposes
//print("You clicked box number ", ftos(MouseInBox()), "\n");
}
return TRUE;
}
else if (param1 == 513) { // Right click
print(vtos(mousepos),"\n");
}
else if (param1 == 514) { // Middle click
if (testsize_one == '0 0 0')
testsize_one = mousepos;
else {
print(vtos(mousepos-testsize_one),"\n");
testsize_one = '0 0 0';
}
}
}
if (eventtype == 2/* && in_menu*/) //mouse
{
mousepos_x += param1;
mousepos_y += param2;
if (mousepos_x < 0)
mousepos_x = 0;
if (mousepos_y < 0)
mousepos_y = 0;
if (mousepos_x > vid_realwidth)
mousepos_x = vid_realwidth;
if (mousepos_y > vid_realheight)
mousepos_y = vid_realheight;
return TRUE;
}
}
return FALSE;
};