Jump to content

  • Log In with Google      Sign In   
  • Create Account

Like
11Likes
Dislike

Creating a Very Simple GUI System for Small Games - Part II

By Martin Prantl | Published May 13 2014 11:48 AM in Game Programming
Peer Reviewed by (Dave Hunt, jbadams, Servant of the Lord)

gui tutorial graphics touch control mouse

In the first part of the GUI tutorial (link), we have seen the positioning and dimension system.

You can also look at other chapters:

Today, before rendering, we will spend some time and familiarize ourselves with basic element types used in this tutorial. Of course, feel free and design anything you like. The controls mentioned in this part are some sort of a standard, that every GUI should have. Those are
  • Panel - usually not rendered, used only to group elements with similar functionality. You can easily move all of its content or hide it.
  • Button - what else to say. Button is just a plain old button
  • Checkbox - in basic principles similar to button, but has more states. We all probably know it.
  • Image - can be used for icons, image visualization etc.
  • TextCaption - for text rendering

Control logic


The control logic is maintained from one class. This class is taking care of changes of states and contains reference to the actual control mechanism - mouse or touch controller. So far, only single touch is solved. If we want to have a multi-touch GUI control, it will be more complicated. We would need to solve problems and actions, if one finger is down and the other is “moving” across screen. What happens if a moving finger crosses an element, that is already active? What if we release the first finger and keep only the second, that arrived on our element during movement? Those questions can be solved by observing existing GUI systems how they behave, but what if there are more systems and every one of them behaves differently? Which one is more correct? Due to all those questions, I have disabled multi-touch support. For main menu and other similar screens, it is usually OK. Problems can be caused by the main game. If we are creating for example a racing game, we need to have multi-touch support. One finger controls pedals and the other steering, and third one maybe shifting. For these types of situations, we need multi-touch support. But that will not be described here, since I have not used it so far. I have it in mind and I believe the described system can easily be upgraded to support this.

For each element we need to test a position of our control point (mouse, finger) against the element. We use positions calculated in the previous article. Since every element is basicly a 2D AABB (axis aligned bounding box), we can use a simple interval testing in axes X and Y. Note, that we test only visible elements. If a point is inside an invisible element, we usually discard it and continue.

We need to solve one more thing. If elements are inside each other, which one will receive the action? I have used a simple depth testing. A screen, as a parent to all other elements, has depth 0. Every child within the screen has depth = parentDepth + offset. And so on, recursively for children of children. A found element with the biggest depth and point inside is called “with focus”. We will use this naming convention in later parts.

I have three basic states for a user controller
  • CONTROL_NONE - no control button is pressed
  • CONTROL_OVER - controller is over, but no button is pressed
  • CONTROL_CLICK - controller is over and a button is pressed
This is 1:1 applicable to a mouse controller. For fingers and a touch control in general, CONTROL_OVER state has no real meaning. To keep things simple and portable, we preserve this state and handle it in a code logic with some condition parts. For this I have used a prepsocessor (#ifdef), but it can also be decided in a runtime with a simple if branch.

Once we identify an element with current focus, we need to do several things. First of all, compare last and actual focused elements. I will explain this idea on a commented code.

if (last != NULL) //there was some focused element as last
{
    //update state of last focused element as currently no control state
	UpdateElementState(CONTROL_NONE, last);
}

if (control->IsPressed())
{
	//test current state of control (mouse / finger)
	//if control is down, do not trigger state change for mouse over
	return;
}

if (act != NULL)
{
	//set state of current element as control (mouse / finger) over
	//if control is mouse - this will change state to HOVERED, with finger
	//it will go directly to same state as mouse down
	UpdateElementState(CONTROL_OVER, act);
}

If last and actual focused elements are the same, we need to do a different chain of responses.

if (act == NULL)
{
	//no selected element  - no clicking on it => do nothing
	return;
}

if (control->IsPressed())
{
	//control (mouse / finger) is down - send state to element
	UpdateElementState(CONTROL_CLICK, act);
}

if (control->IsReleased())
{
	//control (mouse / finger) is released - send state to element
	UpdateElementState(CONTROL_OVER, act);
}

In the above code, tests on NULL are important, since if no element is focused at the moment, NULL is used for this state. Also, control states are sent in every update, so we need to figure how to change them into element states and how to correctly call triggers.

An element changes and trigger actions are now special for a different types of elements. I will sumarize them in a following section. To fire up triggers, I have used delegetes from FastDelegate library / header (Member Function Pointers and the Fastest Possible C++ Delegates). This library is very easy to use and is perfectly portable (iOS, Android, Win...). In C++11 there are some built-in solutions, but I woud rather stick with this library.

For each element that need some triggers, I add them via Set functions. If the associated action is triggered the delegate is called. Instead of this, you can use function pointers. Problem with them is usually associated with classes and member functions. With delagates, you will have easy to maintain code and you can associate delagets with classic functions or meber functions. In both cases, the code remains the same. Only difference will be in a delegate creation (but for this, see article about this topic on codeproject - link above).

In C#, you have delegates in language core support, so there is no problem at all. In Java, there is probably also some solution, but I am not Java positive, so I dont know this :-) For other languages, there will also be some similar functionality.

Elements


First, there is a good reason to create an abstract element that every other will extend. In that abstract element, you will have position, dimensions, color and some other useful things. The specialized functionality will be coded in separate classes that extend this abstract class.

1. Panel & Image


Both of them have no real functionality. A panel exists simply for grouping elements together and an image for showing images. That's all. Basically, both of them are very similar. You can choose its background color or set some texture on it. The reason why I have created two different elements is for better readibility of code and Image has some more background functionality, like helper methods for render targets visualization (used in debugging shadow maps, deferred rendering etc.).

1.1 Control logic


Well... here it is really simple. I am using no states for these two elements. Of course, feel free to add some of them.

2. Button


One of the two more interesting elements I am going to investigate in detail. A button is reccomended as a first for what you should code, when you are creating a GUI. You can try various scenarios on it - show texture, change texture, control interaction, rendering etc. Other elements are basically just a modified button :-)

Our button has three states
  • non active - classic default state
  • hovered - this is correct only for mouse control and indicates that a mouse pointer is over the element, but no mouse button is pressed. This state is not used for a touch control
  • active - button has been clicked or mouse / finger has been pressed on top of it
You could add more states, but those three are all you need for basic effects and a nice looking button.

You should have at least two different textures for each button. One that indicates the default state and the one used for an action state. There is often no need to separate active and hovered state, they can look the same. On a touch controller there is even no hovered state, so there is no difference at all.

Closely related to the state changes are triggers. Those are actions that will occur when a button state changes from one to another or if it is in some state. You can think of many possible actions (if you don't know, the best way is to look for example into C# properties for UI button). I have used only a limited set of triggers. My basic used ones are
  • onDown - mouse or finger has been pressed on the button
  • onClick - click is generated after releasing pressed control (with some additional prerequisites)
  • onHover - valid only for mouse control. Mouse is on the button, but not pressed
  • onUp - mouse or finger has been released on the button (it can be seen similar to onClick without additional prerequisites)
  • whileDown - called while button mouse or finger is pressed on the button
  • whileHover - called while button mouse is on the button, but not pressed
I have almost never seen “while” triggers. In my oppinion, they are good for repeating actions, like a throttle pedal in a touch-based racing game. You are holding it most of the time.

Sometimes, you need functionality similar to a checkbox with a button. Typical case is a "play / pause" button in a media player. Once you hit the button, an action is trigerred and also the icon is changed. You can either use a real checkbox or alter the button a little bit (that is what I am using). In a trigger action code, you just simply change the icon set used for the button. See a sample code. In this I am using a button as a checkbox to enable / disable sound.

void OnClickAction(GUISystem::GUIElement * el)
{
	//emulate check box	behaviour with button
    
	if (this->sound_on)
	{		
        //sound is currently on - we are turning it off
        //change icon set
        
		GUISystem::GUIButtonTextures t;
		t.textureName = "soundoff"; //default texture
		t.textureNameClicked = "soundon"; //on click
		t.textureNameHover = "soundon"; //on mouse over
		el->GetButton()->SetTextures(t);		
	}
	else
	{	
        //sound is currently off - we are turning it on
        //change icon set
        
		GUISystem::GUIButtonTextures t;
		t.textureName = "soundon"; //default texture
		t.textureNameClicked = "soundoff"; //on click
		t.textureNameHover = "soundoff"; //on mouse over
		el->GetButton()->SetTextures(t);
	}
    
    //do some other actions needed to enable / disable sound

}


2.1. Control logic


Control logic of a button seems relatively simple from already mentioned states. There are only three basic ones. Hovewer, main code is a bit more complex. I have divided implementation into two parts. First is a “message” (basically it is not a message, it's just some function call, but it can be seen as a message) sending on a state change to the button from a controller class and a second part handles a state change and trigger calls based on a received “message”. This part is coded directly inside a button class implementation.

First part, inside a control class, that is sending “messages”.

if (ctrl == CONTROL_OVER) //element has focus from mouse
{
	#ifdef __TOUCH_CONTROL__
		//touch control has no CONTROL_OVER state !
		//CONTROL_OVER => element has been touched => CONTROL_CLICK
		//convert it to CONTROL_CLICK
		ctrl = CONTROL_CLICK;
	#else
		//should not occur for touch control
		if (btn->GetState() == BTN_STATE_CLICKED) //last was clicked
		{
			btn->SetState(BTN_STATE_NON_ACTIVE); //trigger actions for onRelease
			btn->SetState(BTN_STATE_OVER); //hover it - mouse stays on top of element after click
			//that is important, otherwise it will look odd
		}
		else
		{
			btn->SetState(BTN_STATE_OVER); //hover element
		}
	#endif
}

if (ctrl == CONTROL_CLICK) //element has focus from mouse and we have touched mouse button
{
	btn->SetState(BTN_STATE_CLICKED); //trigger actions for onClick
}

if (ctrl == CONTROL_NONE) //element has no mouse focus
{
	#ifndef __TOUCH_CONTROL__
		btn->SetState(BTN_STATE_OVER); //deactivate (on over)
	#endif

	if (control->IsPressed())
	{
		btn->SetState(BTN_STATE_DUMMY); //deactivate - use dummy state to prevent some actions
									    //associtaed with releasing control (most of the time used in touch control)
	}
	btn->SetState(BTN_STATE_NON_ACTIVE); //deactivate
}

Second part is coded inside a button and handles received “messages”. Touch control difference is also covered (a button should never receive a hover state). Of course, sometimes you want to preserve hover state to port your application and keep the same functionality. In that case, hover trigger is often called together with onDown.

if (this->actState == newState)
{
	//call repeat triggers
	if ((this->hasBeenDown) && (this->actState == BTN_STATE_CLICKED))
	{
		//call whileDown trigger
	}

	if (this->actState == BTN_STATE_OVER)
	{
		//call while hover trigger
	}

	return;
}

//handle state change

if (newState == BTN_STATE_DUMMY)
{
	//dummy state to "erase" safely states without trigger
	//delegates associated with action
	//dummy = NON_ACTIVE state
	this->actState = BTN_STATE_NON_ACTIVE;
	return;
}

//was not active => now mouse over
if ((this->actState == BTN_STATE_NON_ACTIVE) && (newState == BTN_STATE_OVER))
{
	//trigger onHover
}

//was clicked => now non active
if ((this->actState == BTN_STATE_CLICKED) && (newState == BTN_STATE_NON_ACTIVE))
{
	if (this->hasBeenDown)
	{
		//trigger onClick
	}
	else
	{
		//trigger onUp
	}
}

#ifdef __TOUCH_CONTROL__
	//no hover state on touch control => go directly from NON_ACTIVE to CLICKED
	if ((this->actState == BTN_STATE_NON_ACTIVE) && (newState == BTN_STATE_CLICKED))
#else
	//go from mouse OVER state to CLICKED
	if ((this->actState == BTN_STATE_OVER) && (newState == BTN_STATE_CLICKED))
#endif
{
	this->hasBeenDown = true;
	//trigger onDown
}
else
{
	this->hasBeenDown = false;
}

this->actState = newState;

Code I have shown is almost everything that handles a button control.

3. Checkbox


Second complex element is a checkbox. Its functionality is similar to a button, but it has more states. I will not describe state changes and handling of those as detailed as I have done for a button. It's very similar, you can learn from button code and extend it. Plus it would take a little bit more space.

Our checkbox has six states
  • non active - classic default state
  • hovered - this control is correct only for a mouse control and indicates that mouse pointer is over element, but no mouse button is pressed. This state is not used in touch controls
  • clicked - state right after it has been clicked => in next “frame” state will be checked
  • checked - checkbox is checked. We go to this state after clicked state
  • checked + hovered - for checked state we need to have different hover state. It makes sense, since icon is usually also different
  • checked + clicked - state right after it has been clicked in checked state => next “frame” state will be non active
You will need two different sets of textures, one for unchecked and one for checked states. As for triggers, you can use the same as for a button, but with two additional ones.
  • onCheck - state of the checkbox has been changed to checked
  • onUncheck- state of the checkbox has been changed to unchecked
“While” triggers can be also used together with check state, like whileChecked. Hovewer, I don't see a real use for this at the moment.

3.1. Control logic


Control logic is, in its basic sense, similar to a button. You only need to handle more states. If you are lazy, you can even discard the checkbox all together and simulate its behavior with a simple button. You will put a code into an onClick trigger action. In there you will have to change the texture of the button. The is one set of textures for non-checked states and the second set for checked states and you will just swap them if one or the other state occurs. This will only affect visual appereance of the element, you will have no special triggers, like onCheck. You can emulate those with button triggers and some temporary variables.

4. Text caption


Text caption is a very simple element. It has no specific texture, but contains words and letters. It's used only for small captions, so it can be added on top of a button to create a caption. If you need texts that are longer, you have to add some special functionality. This basic element is only for very simple texts (one line, no wrap if text is too long etc).

More advance text elements should support multi-lines, auto wrap of a text if it is too long, padding or just anything else you can think of.

4.1. Control logic


Text caption has no control logic. Its only purpose is to show you some text :-)

Discussion


In the second part of our “tutorial” I have covered basic elements that you will need most of the time. Without those, no GUI can be complete. I have shown more details for a button, since a checkbox is very similar and can be emulated with a simple button and some temporary variables (and some magic, of course).

If you think something can be done better or is not accurate, feel free and post a comment. In an attachment, you can download source code (C++) for a descibed functionality. Code is not usable as it is, because of the dependencies on the rest of my engine.

In future parts, we will investigate rendering and some tips & tricks.

Article Update Log


22 May 2014: Added links to other parts of tutorial
10 May 2014: Added missing code, added description of checkbox emulation with button
9 May 2014: Initial release





License


GDOL (Gamedev.net Open License)




Comments

I've already reminded Martin he forgot to upload the code attachment

Another common 'state' for widgets is a 'disabled' state.

 

Sometimes this is displayed as either [Checked && Disabled] [Unchecked && Disabled] depending on the current checked state. Visually, you could probably just get away with making any disabled widget look unchecked and disabled, instead of having two separate disabled appearances.

 

This can be useful when one option depends on another option, the dependent widgets are disabled (greyed out) until the primary option is enabled.

 

Can also be used in-game for things like only making certain buttons visible when the player has selected a valid element - e.g. keeping the 'Purchase' button in an NPC shop greyed out until the player has actually selected an item to buy.

 

For checkboxes, some checkboxes are tri-states. That is to say, they can be set to 'Checked', 'Unchecked', or the state typically called 'Indeterminate'.

When thinking of these for games, I like to think of them as useful for: [Override option to 'Yes', Override option to 'No', or Automatically choose based on context (i.e. the 'third' state)]

 

Checkboxes are usually just special visual appearances for buttons, because normal buttons can often be checkable as well.

 

Radio buttons are just a group of checkboxes/buttons that are connected to each other so that only one can be selected at a time.

 

Buttons, when checked, sometimes need to even change their icons and text. For example, in the horrible (but common-place) design decision of making play/pause buttons the same button, when in the 'checked' state, the Play button changes appearance to show a pause icon, and when in the 'unchecked' state, the button appearance changes to show the play icon.

 

Other gameplay-related uses may include a button that swaps between two different tools. Mario's jump-boots vs hammer, for example. When the hammer is equipped, the button shows the shoes. When the shoes are equipped, the button shows the hammer.

Also, it'd be nice if collections of articles like this had 'Article table's at the top or bottom of the article, with all the links to previous (and later) articles. Not just a link to the previous article, but the previous article should also be updated to have links to the later articles.

@Gaiiden

Thanks for info, I have forget this. I will add the code probably after the weekend.

 

@Servant of the Lord

Button vs checkbox - they are wevy similar, but from my point of view button should not have "checked" state. I see button like eg. keyboard keys :-) (up / down). Plus you can change the icon set for button if it is in some other state. This can simulate "checkbox" behaviour. I use it sometimes in scenarios like in your mentioned Mario :-) I will add this note to the article when I will be adding the code.

As for "play & pause" button, you can use checkbox instead of it and you will have the same functionality, if you create onClick trigger or use onStateChange trigger and test actual state (checked / nonChecked).

 

 

Also, it'd be nice if collections of articles like this had 'Article table's at the top or bottom of the article, with all the links to previous (and later) articles. Not just a link to the previous article, but the previous article should also be updated to have links to the later articles.

 

I am going to put links to next parts when the "whole" tutorial will be released (I didnt want to add links to part I to "under review" article)

There's a spelling mistake in the third sentence.

Starting with buttons I think you venture in different user expectances (as in different OS behaviour).

 

Under Windows, if you push the button over it, keep it pushed, move the pointer off (which captures the mouse input on the button!) and release the button the button push is not executed. I don't know if the same behaviour is available on other OS/GUIs.

 

How do you tackle this?

 

Usually I'd advise to mimick the expected behavious (thus the behaviour of the underlying OS). This also includes clipboard for edit controls etc.

Starting with buttons I think you venture in different user expectances (as in different OS behaviour).

 

Under Windows, if you push the button over it, keep it pushed, move the pointer off (which captures the mouse input on the button!) and release the button the button push is not executed. I don't know if the same behaviour is available on other OS/GUIs.

 

How do you tackle this?

 

It is working as you pointed out. This behaviour is handled by checking last / actual element with mouse "over" it. if those are different, states are cleared, so there will be no onClick after releasing mouse. Of course, moving mouse back will have no effect since you erased the previous state.

 

I think, that hecking every possible combination of states is usually pointless for simple GUI. Most of the time, my design works fine and it is kept simple. Plus on mobile device there is little bit different logic and have both of them in one GUI system requires some simplification on both sides.


Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS