-
Notifications
You must be signed in to change notification settings - Fork 1
GUI Components
At the highest level of abstraction in the Zenderer engine is the GUI subsystem. It provides convenient wrappers around the graphics systems in order to facilitate easy menu-creation and in-game GUI components.
Menus are a common, boring task in the polishing stage of game development. Zenderer provides an extremely simplistic, but effective, wrapper around the scene component of the graphics API to make things easier. Adding buttons takes a single line of code, and they automatically provide roll-over effects when using the mouse.
I adopted an event-driven approach to handle menu actions, using callbacks to trigger actions when a menu button is selected by the user. When adding a button to the menu, you pass the button's text, as well as a lambda function.
Creating a menu involves quite a bit of boilerplate setup code, but that's what's necessary for adequate customization and ease-of-use later on. Let's create a basic main menu.
// Standard Zenderer boilerplate.
Init(); asset::zAssetManager Assets;
gfx::zWindow Window(800, 600, "Menu Test", Assets, false);
Window.Init();
// Create a menu object.
gui::zMenu MainMenu(Window, Assets);
// Set it up with various customizations.
// We assume Windows for the simplicity of using one of the default fonts.
MainMenu.SetFont("C:\\Windows\\Fonts\\Arial.ttf", 24);
// We want our buttons to align the left-hand side of the screen.
MainMenu.SetInitialButtonPosition(math::vector_t(20, 300));
// Make inactive buttons white and active ones gray.
MainMenu.SetActiveButtonTextColor(color4f_t());
MainMenu.SetNormalButtonTextColor(color4f_t(0.7, 0.7, 0.7));That takes care of the initial setup code. Now we add our buttons. They will be placed starting at (20, 300) as we specified above, and will each be just as far apart (vertically) as they need to be to not overlap/touch. This is based upon the size of the font we loaded (24pt here).
// Let's have a game state so we can manipulate things easily with the callbacks.
enum class GameState { PLAY, MENU, LOAD, PAUSE, QUIT };
GameState state = GameState::MENU;
MainMenu.AddButton("Play Game", [&state](const size_t i) { state = GameState::PLAY; });
// Set the button spacing to be larger than what it would be by default
// just to demonstrate the option's effect. This has an affect on the button
// *after* the next one. Thus, "Quit" will be spaced further than the rest of them.
MainMenu.SetSpacing(80);
MainMenu.AddButton("Load Game", [&state](const size_t i) { state = GameState::LOAD; });
MainMenu.AddButton("Quit", [&state](const size_t i) { state = GameState::QUIT; });Now our menu is ready and waiting for the render loop. It will take care of event handling as needed, but we need to pass it any event objects that aren't related to the parent loop, which in this case will be everything. Read up mnore on event handling here.
evt::zEventHandler& Evts = evt::zEventHandler::GetInstance();
evt::event_t Evt;
// Main menu loop.
while (state == GameState::MENU)
{
Evts.PollEvents();
while (Evts.PopEvent(Evt))
{
// Handle quit events.
if (Evt.type == evt::EventType::WINDOW_CLOSE)
state = GameState::QUIT;
// Pass off everything to the menu handler.
MainMenu.HandleEvent(Evt);
}
Window.Clear();
// Update the menu based on actions.
MainMenu.Update();
Window.Update();
}
Quit();
return 0;Our resulting menu will look like this, with the mouse hovering over the "Quit" button:
Boring, right? Let's spice it up a bit. One of the largest benefits of using the zMenu wrapper is that it allows for you to have a fully customizable menu scene, but without dealing with the actual menu actions and events. Let's add a menu background and an overlay for the navigation pane.
The following code will come before the button-adding procedures, but after the initial setup code.
obj::zEntity& Background = MainMenu.AddEntity();
obj::zEntity& Overlay = MainMenu.AddEntity();
// Let's assume we made a sweet menu background.
// Source: http://media.desura.com/images/games/1/17/16340/Menu_Background.jpg
Background.LoadFromTexture("menu.jpg");
// And add a semi-transparent overlay for the buttons to distinguish them.
Overlay.AddPrimitive(gfx::zQuad(Assets, 256, Window.GetHeight()).SetColor(0, 0, 0, 0.4).Create());Our menu is also lacking a title... Let's add one of those, too.
obj::zEntity& Title = MainMenu.AddEntity();
MainMenu.RenderWithFont(Title, "Main Menu"); // unique, right?
Title.Move(Window.GetWidth() / 2 - Title.GetW() / 2, 200);Here's our result now, much prettier!
As a final demonstration of the menus capabilities, we will add a light to represent the "sun" in the top-left corner of the menu. This is a primitive example, but adding lights to a menu could allow for nice, dynamic effects that make the menu come alive, such as a flickering torch. The code is trivial.
// Add a "sun" to the menu just to demonstrate its effects.
gfx::zLight& Sun = MainMenu.AddLight(gfx::LightType::ZEN_POINT);
Sun.Enable();
Sun.SetBrightness(5.0);
Sun.SetColor(0.8, 0.8, 0.0);
Sun.Disable();As you can see with the lighting effect, the buttons take on the effect as well. This is an intentional design decision, as all resources of the menu are attached to the same scene object. I've not had a reason to separate the buttons from the menu, but if there is a desire it can be easily implemented.
Both the AddEntity() and AddLight() methods are literally direct calls to the underlying scene object, so all nuances of those methods apply here, as well.
Zenderer uses the FreeType 2 API in order to render fonts. Fonts are converted to bitmap representations and uploaded to the GPU in RGBA texture format. TODO
The aforementioned menu API uses the low-level zButton object to implement menu buttons, but this class can also be used independently in order to create GUI overlays in-game. For example, this API can be used to render a "Pause Game" button on top of a game world scene, as demonstrated in the Introduction. Naturally, this requires a lot more preliminary setup on behalf of the user.
Buttons attach to scene objects. If you want your buttons to behave independently of the scene that they overlay, you'll want to create a separate object exclusively for GUI components, and call gfx::zScene::SetSeeThrough(true), so that the game window actually renders the things beneath the GUI scene.
Buttons have two states: normal, and active. The normal button state is applied in all cases except when the active state is (go figure). By default, as implemented by the menu API, the active state applies when the user is hovering the mouse over the button's collision box. Detecting this is the purpose of the IsOver family of methods. The menu API switches button states automatically, but implementing raw buttons requires this to be done by hand by the user.
Let's look at some code. Assuming that the necessary Zenderer boilerplate has already been executed, here's how we'd prepare a GUI overlay.
gfx::zScene GUIOverlay(Window.GetWidth(), Window.GetHeight(), Assets);
GUIOverlay.Init(); GUIOverlay.SetSeeThrough(true);
// Create a font and a button.
gui::zFont& GUIFont = *Assets.Create<gui::zFont>("C:\\Windows\\Fonts\\Arial.ttf");
gui::zButton PauseButton(GUIOverlay);
// Configuration options.
PauseButton.SetFont(GUIFont);
PauseButton.SetActiveColor(color4f_t(0, 0, 1));
PauseButton.SetNormalColor(color4f_t(1, 1, 1));
// Prepare the button for rendering.
PauseButton.Prepare("Pause Game");
PauseButton.Place(Window.GetWidth() - GUIFont.GetTextWidth("Pause Game") - 12, 20);Now that we've prepared the button for rendering comes the handling of events and actions within the main rendering loop. I've left off everything but the bare necessities to get the overlay drawn on the screen.
// Our state tracker.
bool pause = false;
// -snip-
// Standard main game loop code.
// -snip-
evt::zEventHandler& Evts = evt::zEventHandler::GetInstance();
evt::event_t Evt;
Evts.PollEvents();
while (Evts.PopEvent(Evt))
{
switch(Evt.type)
{
// quit handling, etc. omitted.
case evt::EventType::MOUSE_MOTION:
if (PauseButton.IsOver(Evt.mouse.position))
PauseButton.SetActive();
else
PauseButton.SetDefault();
break;
case evt::EventType::MOUSE_DOWN:
if (PauseButton.IsOver(Evt.mouse.position))
pause = true;
break;
}
}
if (pause)
{
std::cout << "Game paused.\n";
}
Window.Clear();
GUIOverlay.Render();
Window.Update();

