GUI/Tips1 5
From TDN
These tips appeared originally in the tutorial Tips for Working with TGE1.5 GUIs. The game screen shots where taken from the free game MINI#37 (see http://www.mini37.com).
Decide whether you will support multiple resolutions up front.
Multiple resolution support impacts every aspect of the GUI. You need to plan carefully for this right from the beginning to avoid redoing the design or rewriting big chunks of code. If you do decide to support multiple resolutions, there are several different setups you could use. Decide which one best suits you, and follow the guidelines below.
Components clamp, do not scale.
This means at high resolutions more screen space is available. You have to design your GUI for the lowest resolution. This is the easiest one to implement.
Components scale relative to the resolution.
In this case you can design your GUI for any resolution, although you should check that fonts and other graphical elements are still clear in lower resolutions. You will need to implement scaling of fonts, border thickness, and possibly even tab stops (for ML controls). Components will look squished in wide screen format, and you will have to design around this. All assets must be created for the highest resolution.
Components scale relative to the resolution, but keep their aspect ratios.
You need to implement all the things necessary for relative resizing, as well as special features of the resizing algorithm wherever it is used. This route requires the most work, but the visual effect is superior to normal scaling for wide screen resolutions.
Don't program yourself into maze – use the GUI editor.
Layout is a labour intensive task. There are (at least) three reasons you might be tempted to script all of your GUI by hand:
1) to share field values, for example:
new GuiButtonCtrl(:GeneralButton)
{}
2)to use variables for the positions and extents of controls, for example:
new GuiButtonCtrl()
{
position = $leftMargin + $leftOffset
SPC $topMargin + $topOffset;
extent = $buttonWidth
SPC $buttonHeight;
}
and
3) to use functions:
for(%i = 0; %i < $buttonCount; %i++)
{
%button = new GuiButtonCtrl()
{
position = 100 * %i SPC 0;
extent = 100 SPC 50;
};
MyGui.add(%button);
}
The idea is to make your code more reusable, readable, and hence easier to maintain and modify. However, if your GUI is not extremely regular, you would be mistaken:
- There are too many small exceptions. You will be stranded with dozens of variables and functions that are hard to name and remember.
- The formulas you use to calculate positions and sizes will become increasingly complex, and therefore hard to understand, debug, and maintain.
- The relationship between components that share fields will cause visually simple changes to become difficult to make. Consider, for example, what needs to be done if you realise the button you have used as a base for all your dialog buttons needs a change without affecting the dialog buttons.
Whenever you feel the urge to manually create GUI components in Torque Script, consider implementing a custom widget in C++ in the engine. This way you can take advantage of programming and the GUI editor.
Do not use add and remove commands on GUI controls.
If you add and remove GUI controls at runtime to other controls,
- some components may not resize properly;
- you cannot use the GUI editor to do the layout.
To prevent these problem, build all components that may be visible in a screen (using the editor), and set their visibility and content at run time.
Implement even moderately complex widgets in the engine.
Most widgets can be build from existing controls in script. However, Torque Script is not flexible enough to let you do this in a straight-forward and maintainable fashion. The initial effort of implementing the widget as a control in C++ is totally offset by the effort saved of writing convoluted script code. You should implement widgets in the engine when:
- the widget is animated;
- the widget has children whose positions are calculated at runtime;
- the control is a selector – e.g. a colour picker or list box.
In some cases, if you use certain combinations of widgets many times, the combination should also be implemented as a single control in the engine.
How to implementing a GUI control in the engine.
Sizes and Positions
Calculate all sizes and positions relative to the component's size and position. Do not store positions of sub components in fields – this will force you to write custom resizing code for this component.
Images
Image files should be stored in scriptable fields. Such a field should be shielded so that the image is reloaded whenever the field changes. Do not load images when the component wakes up – this might make a GUI unresponsive when it is visible for the first time.
void GuiMyCtrl::initPersistFields()
{
///...
addProtectedField("bitmap",
TypeFilename,
Offset(mBitmapName, GuiMyCtrl),
&setBitmap,
&defaultProtectedGetFn, "");
}
bool GuiMyCtrl::setBitmapName( void *obj,
const char *data )
{
static_cast<GuiMyCtrl *>( obj )
->setBitmap( data );
return false;
// false, because 'setBitmap' will assign
// 'mBitmapName' to the argument we are
// specifying in the call.
}
void GuiMyCtrl::setBitmap(const char *n)
{
mBitmapName =
StringTable->insert(n);
if (*mBitmapName)
mTextureHandle =
TextureHandle(mBitmapName,
BitmapTexture, true);
else
mTextureHandle = NULL;
setUpdate();
}
Fields
Always implement fields so that:
- they can be changed at any time and have immediate effect;
- they don't depend on each other; and
- they can be modified in any order.
Never expose variables whose values are calculated to Torque Script. Instead, expose a getter function that returns the value. For instance, for a ellipse control, the radii are calculated from the extent, and should not be scriptable fields. The methods getXRaius and getYRadius can be defined to get those values.
Animation
If your GUI has many animating components, use a single system that handles the common logic. One approach that works well is to implement an abstract class (Animateable). All calculations are done in the Animateable class. A GUI control that extends Animateable (in addition to its super class) implement methods that set the animation properties and use values calculated by the Animateable class for rendering and GUI logic.
Overriding onWake, etc.
Don't forget to call the script callback functions, or the super class method:
bool MyGuiCtrl::onwake()
{
if (!Parent::onWake())
return false;
//...my custom code
return true;
}
Skinning
Don't implement skinning! A good idea in theory, but in practise a terrible time drain:
- Setting up a skin and tweaking it afterwards is much harder than to just use single images.
- Code that supports skinning must be much more general than is necessary for a single game.
- Finally, if your component must be resizeable or dynamically updateable (e.g. a container), skinning can significantly and unnecessarily complicate your rendering code.
Put all the images you'll need to render your component in fields as explained above.
Standardise methods of menu screens.
Define the same methods for every screen. This will allow you to treat your menu screens in a uniform manner, which means less code and a simpler structure. It will also make it possible to use a single key map for all screens – you can keep track of the current screen in a global variable, an call the screen methods on this variable. For example:
- Implement a build function for constructing dynamic GUIs. If you build static GUIs as described above, you need to call this function only once.
- Implement a reset function that resets a screen to its original state. Do not put this code in the onWake function – call the reset function in your onWake if necessary.
- Implement a function to change your GUI based on a given state (possibly an integer index) and functions to change the state (based on keyboard presses, for example).
- Implement select and back functions that take you to the next and previous menus. You can bind your escape and enter keys to the following two functions:
onEnterPressed()
{
$activeScreen.select();
}
onEscapressed()
{
$activeScreen.back();
}
Structure your menus so that they can be easily skipped during development.
Wading through menus during development is a waste of time. Make it easy to skip your menus and enter the game directly – use a config file, for example. Ideally, you should be able to enter into the game from any menu screen – this way, debugging menu issues can be done faster. If you do this, test your full menu system regularly. Bugs have a way of hiding in parts of the game you don't visit regularly.
When changing components in the engine, make sure you don't break editor GUIs.
It is easy to break features that are necessary for the World and GUI editors to work, especially if you are changing default values that you know you are not using. Because you are concentrating on your game's GUI, it might be a while before you discover these, by which time it might be very expensive to fix!
When switching key maps...
Be careful! The best strategy is to push key maps in the onWake function and to pop them in the onSleep function:
GuiControl1::onSleep()
{
KeyMap1.pop();
}
...
GuiControl2::onWake()
{
KeyMap2.push();
}
The following approach might lead to problems when keys are pressed in rapid succession:
GuiControl1::select()
// invoked when user presses enter
{
//...switch to GuiContol2
}
...
GuiControl2::onWake()
{
KeyMap1.pop();
KeyMap2.push();
}
GuiControl2::select()
// invoked when user presses enter
{
//...switch to GuiContol3
}
In the example above, the user might press the key fast enough so that GuiControl2 never wakes up. Thus KeyMap1 is still active in GuiControl3. This kind of mistake is not easy to spot, and hard to debug. Use as few key maps as possible to prevent having to switch them often.
Use TGAs, not PNGs, for transparent images that might scale.
When PNGs are scaled, artefacts might be introduced because of anti-aliasing with the wrong transparent colour. This can be avoided by using TGAs, and ensuring that the colour in the transparent regions match the borders in non transparent regions. (We implemented a resource to get TGA support.)
Test all resolutions frequently.
Many bugs appear only in certain resolutions. By testing your GUI in all resolutions frequently, these are discovered while they are still fresh and easy (i.e. inexpensive) to fix.
Think carefully before using the ML controls.
ML controls are great because they can contain images, text of different fonts, tabulated text, and links. However, they suffer from two problems:
- they don't scale their content (fonts, tabs, or images); and
- their content cannot be changed modularly – to change one image you have to change the entire string.
To make them resize properly can be done, but it is a tricky business. And you can write a function that will construct the content string from parameters. But in many cases it is easier to just use several smaller components, which can be changed individually and scale better. You still need to implement font scaling for text components, but this is considerably easier.
Use \c escape sequences to change colour in ML controls.
Use the \c escape sequences in ML text to change colour, instead of colour tags. This makes colours of an ML control part of the profile, which can be shared and is easier to maintain. It also makes it possible to make colour changes at runtime by changing the profiles, instead of changing the entire content string. This is useful for giving an ML control a highlighted or disabled state.
When moving widgets, use coordinates of other widgets
Do not use raw coordinates to move widgets around on the screen. You will need to adjust these for various resolutions, a task complicated by rounding problems. Instead, move a component to the position of an existing component:
widget1.position = widget2.position;
This is safe in all resolutions. If there is no component where the component should be moved, put a dummy, invisible component there. Avoid animating in script – see point 3.
How to implement a keyboard-scrollable container.
1) Place the items that will be visible on the screen. Give them the same name with a number appended, i.e. Row0, Row1... 2) Implement a function that fills the content of the item widgets given an offset, like this:
function fillRows(%offset)
{
for(%i = 0; %i < $rowCount; %i++)
{
%row = NameToId(Row @ %i);
%row.setText($rowText[%i +
%offset]);
}
}
3) Implement a function that will update highlight.
function updateHighlight(%index)
{
%row = NameToId(Row @ %index);
RowHighlight.position =
%button.position;
}
4) Implement increment and decrement functions that update the text offset and selected index:
function inc()
{
if($rowbuttonIndex < $rowCount – 1)
$rowIndex++;
else
if($rowOffset < $commandCount - 1)
$rowOffset++;
fillRows(%offset);
updateHighlight(%index);
}
function dec()
{
if($rowIndex > 0)
$rowIndex--;
else
$rowOffset--;
fillRows(%offset);
updateHighlight(%index);
}
5) Implement your select function to execute the right command:
function select()
{
exec($command[%index + %offset]);
}
Layers
There are two ways in which GUI components can be layered. We'll call these two methods hierarchical and sequential layering. Hierarchical layering is due to every component being drawn over its parent, as in the following example:
new GuiBitmapCtrl(Background)
{
position = 30 SPC 30;
extent = 100 SPC 100;
bitmap = “redâ€;
new GuiBitmapCtrl(Foreground)
{
position = 20 SPC 20;
extent = 60 SPC 60;
bitmap = “blueâ€;
};
};
Sequential layering is due to every component being drawn over previously declared components, as in the following example:
new GuiBitmapCtrl(Background)
{
position = 0 SPC 0;
extent = 100 SPC 100;
bitmap = “redâ€;
};
new GuiBitmapCtrl(Foreground)
{
position = 50 SPC 50;
extent = 60 SPC 60;
bitmap = “blueâ€;
};
Both these examples will draw the blue component over the red component. Hierarchical layered components are easier to lay out, and easier to move around. It can also be used to clip, as shown on the left below.
Sequential layered components can overlap, as shown on the right in the image above. It can also be used in transparency effects. In the example below, the dark overlay can be switched on or off without affecting the text layer.
These layering techniques are usually used together. Use hierarchical layering whenever possible to simplify layout. Sequential layering should only be used when necessary to get a desired effect.
Break big animation sequences into smaller pieces.
Animation sequences take long to load, and take up a significant amount of disk space. If a large part of the image is not animated, you can improve performance and disk space usage by breaking the sequence into smaller pieces, as in the example below:
This animation is broken into seven pieces – one static image, and six animated pieces.
Colour Modulation
Use colour modulation instead multiple images.
If you have a component that use images, and changes colour depending on its state, use colour modulation to create the colour change on a single image. This will make asset management easier.
new GuiBitmapCtrl(BlueIcon)
{
//...other fields
modulate = true;
modulationColor = “0 153 255â€;
}
Since you can only modulate one colour, you may need to break up images to separate parts of the image that do change colour from those that do not. The icons above were created from two images: one contains all the grey parts of the icon; the other contains a white dot that is modulated to the desired colour.
Use colour modulation for animating glows.
Colour modulation can also be used in customised components to animate glowing components and other colour changes, instead of using animation sequences.
Use clipping to simplify layout of image components.
Remember that a GUI component clips all its children inside its own rectangle. This can be used to simplify layout when a widget's dimensions are not powers of two. Suppose the widget you want is 100×100 pixels. Now the image will be 128×128. The trick is two make two components – the one is the size of the image (128×128), and the other the size of the widget (100×100). Make the image the child of the other, and design the image so that the visible part is in the top left hand corner.
new GuiControl(Widget)
{
position = wherever;
extent = 100 SPC 100;
new GuiBitmapCtrl(WidgetImage)
{
position = 0 SPC 0;
extent = 128 SPC 128;
};
};
This technique works well with resizing, and is easier to use than to fix all the clipping bugs in TGE's GUI rendering code.
Beware the following bugs...
Changing the resolution several times in a session can cause small discrepancies.
TGE stores all its GUI positions and sizes as integer values. When you write your own resizing code, round-off errors can cause small yet unsightly discrepancies.
Colour switches last only up to the end of line in ML controls.
If you need a colour change to span multiple lines, put one at the beginning of every line. If you can't predict where line breaks might fall, put one at the beginning of every word.
ML controls don't draw their children.
An easy bug to fix, but it exposes a clipping bug, which is harder to track down. It is best not to use ML controls as containers.
Some vertSizing and horizSizing fields values are inverted
The field values top and bottom (vertSizing) and left and right (horizSizing) are inverted. This is easy to fix, although the bug is so old that a big amount of code still uses them inverted...
For the GUI designer: design to save time and money.
The visual appearance and functionality of a GUI dramatically affects the ease with which a robust GUI system can be built. Here are a few things you can do to make it easier, and thus quicker and cheaper, to build the GUI you are designing.
Be consistent.
Mary design all 30 of her buttons to be red, with white text. John does the same, except one button is white with red text. John's buttons are about twice as expensive as Mary's. Here is why: For Mary's design, one GUI profile has to be defined – for John's design, two. Both tweak the engine to use the white on red profile as default for buttons, but John needs to do some additional work to set the special button's profile. Mary only needs to test that a button appears correctly. John, needs to test this, as well as his special button. But this is not where it ends: every time Mary wants to change the font, it can be done in one place, and tested with one button. John needs to do it in two places, and needs to examine two buttons to test it. John could refactor his code so that he too can make the font change in only one place. But this will require about three times the amount of work at the beginning! Having all your components look the same is very limiting. But variety has a price, and you as designer should understand this price, and spend the variety where it matters.
Design for GUI profiles.
This requires you to think about the design in a different way. Instead of thinking how each screen should look, think how widgets should look in general. Give widgets with different looks different names. Here is an example description:
menu-select buttons: - white, Arial, 14 pt, bold - black background - white border
dialog buttons: - white, Arial, 14 pt, normal - dark grey background - no border
icon buttons: - image background - white border
Doing this forces you to think in a way that will keep the design consistent and use variety with care. If you cant think of a good name for a widget type, it is usually a sign that making the visual appearance of that button different from others is a mistake.
Design for extensibility.
Have a plan for every contingency: What will we do if we find out we need one more button on this pane? What if a text string does not fit in this label? What if we need one more column in this table? If you let answers to these questions influence your design, it will be much more flexible and thus less expensive to change. The GUI often becomes a bottleneck in adding new features – if you document your expansion strategy, the programmers who implement the GUI can develop an expansion strategy in line with the design, which will greatly smooth out the process of change.
Design for powers of two.
Widgets are much easier to lay out, and resize if their dimensions are powers of two. This is especially true for widgets that have images, and widgets that needs centring.
Design for colour modulation.
Blending can change one colour of an image. To take advantage of blending, your colour changes should not affect two or more colours.
Avoid partially overlapping components.
Partially overlapping components are harder to lay out and change. The more hierarchical the GUI, the easier it will be to reuse code.














