User interface, standard controls, viewports

Using our engine you can create nice user-interface for your applications. The look and behavior of everything is very customizable, as you often want a special-looking UI in your games. Our user-interface is rendered inside a container like TCastleWindow or TCastleControl, and of course works without any modifications on all the platforms we support — desktop, mobile...

A complete program using the concepts shown here is in the engine examples, in the examples/2d_standard_ui/zombie_fighter/ directory.

Dialog box composed from simple UI elements 2D animated scene inside a button UI dialog, in a state over the game UI

1. Introduction

Our game window is covered by 2D user-interface controls. All of the classes representing a user-interface control descend from the TUIControl class. They can render, they can handle inputs, they can perform any time-dependent task, and much more.

All of the currently used controls are added to the list of TCastleWindowCustom.Controls (if you use TCastleWindow) or TCastleControlCustom.Controls (if you use Lazarus forms with TCastleControl). The controls on this list are ordered from the back to front. But usually you don't need to remember that, as you add them using the aptly-named methods InsertFront or InsertBack, indicating the visual "front" (in front of other controls) or "back". The TUIControl can be arranged in hierarchy: one instance of TUIControl can be a children of another, which makes it visually "contained" inside the parent, and moved along with parent.

Note that you can also render and handle inputs using the TCastleWindow or TCastleControl callbacks, like Window.OnRender, Window.OnPress and Window.OnUpdate. But this is usually only useful for simple cases. Most of your rendering and input handling should be done inside some TUIControl descendant. Note that a scene manager by default acts also as a viewport into the 3D world, and it's also a descendant of TUIControl. It makes sense, if you think about it: inside a 2D screen, you create a 2D viewport, that allows to view the 3D world inside. So, almost everything is "inside" some TUIControl in our engine.

While TUIControl is basically a way to render anything, in this chapter we will focus on the most traditional use of this class: to create simple 2D user interfaces. Our engine includes a large number of TUIControl descendants for this. The most often used controls are:

  • TCastleLabel - Label with text. As with all our text controls, the font family and size is customizable (see chapter about text). May have a frame. May be multiline. May contain some HTML tags.
  • TCastleButton - Clickable button. The look is highly configurable with custom images and tint colors. May contain an icon inside (actually, by inserting other UI controls as children, it may contain anything inside). The size may be automatically adjusted to the inside caption (and icon), or may be explicitly given.
  • TCastleImageControl - Image. May stretch the content, or adjust to the content size. Image may be partially transparent, with blending or alpha-testing. Image can be rotated or clipped by an arbitrary line. Image may be "tinted" (multiplied) by given color. Underneath, the image is a full-featured TCastleImage, so you can process it in a myriad of ways.
  • TCastleRectangleControl - Rectangle filled with solid color. Good for a general-purpose background. The color may be partially transparent, in which case the content underneath is still visible.
  • TUIControlSizeable - General-purpose container (or ancestor) for other UI controls. Does not do or show anything by itself, but it has a configurable position and size.

These are just the basic UI classes. Find the TUIControl in our class hierarchy diagram and look at all it's descendants to discover more:)

2. Using the 2D controls

You simply create an instance of any class you like, and add it as a children of Window.Controls, CastleControl.Controls or another (parent) TUIControl. Note that TUIControl is a descendant of the standard TComponent property, so you can use the standard "ownership" mechanism to take care of freeing the UI control. In simple cases, you can make the Application or Window a parent of your UI control.

An example below shows a simple button and a label:

uses CastleWindow, CastleUIControls, CastleControls;
  Window: TCastleWindow;
  MyLabel: TCastleLabel;
  MyButton: TCastleButton;
  Window := TCastleWindow.Create(Application);
  MyButton := TCastleButton.Create(Application);
  MyButton.Caption := 'Click me!';
  MyButton.Anchor(vpBottom, 10);
  MyLabel := TCastleLabel.Create(Application);
  MyLabel.Caption := 'Click on the button!';
  { position label such that it's over the button }
  MyLabel.Anchor(vpBottom, 10 + MyButton.CalculatedHeight + 10);

Remember that at any time, you can add and remove the controls. You can also make a control temporarily "not existing" (not visible, not handling inputs and so on — just like it would not be present on the controls list at all) by flipping it's Exists property.

Here's the previous example expanded, showing how to handle button click, to toggle the visibility of a rectangle:

uses CastleWindow, CastleUIControls, CastleControls, CastleColors;
  Window: TCastleWindow;
  MyRect: TCastleRectangleControl;
  MyButton: TCastleButton;
  TEventHandler = class
    class procedure ButtonClick(Sender: TObject);
class procedure TEventHandler.ButtonClick(Sender: TObject);
  MyRect.Exists := not MyRect.Exists;
  Window := TCastleWindow.Create(Application);
  MyButton := TCastleButton.Create(Application);
  MyButton.Caption := 'Toggle rectangle';
  MyButton.Anchor(vpBottom, 10);
  { use a trick to avoid creating a useless instance
    of the TEventHandler class. }
  MyButton.OnClick := @TEventHandler(nil).ButtonClick;
  MyRect := TCastleRectangleControl.Create(Application);
  MyRect.Color := Yellow;
  MyRect.Width := 200;
  MyRect.Height := 200;

3. Parents and anchors

Dialog box composed from simple UI elements

Every UI control may have children, which are positioned relative to their parent. The children are always drawn on top of their parent. (Although the parent has an additional chance to draw over the children in it's RenderOverChildren method, but that's rarely used.)

  • The children receive input events (key and mouse presses) before their parent. So the innermost children get the first chance to process an event (like a click), and only if they do not handle it (their Press method will return false) then the event is passed to the parent. If no UI control processes a press event, it is passed to the TCastleWindowCustom.OnPress or TCastleControlCustom.OnPress.

    Note that the input events are only send to the controls under the pointer (mouse or touch position). Although this is configurable using the CapturesEventsAtPosition method, but usually it's saner to leave it at default. A useful trick to capture the events from the whole window is to use a TUIControlSizeable with FullSize = true, this will capture mouse clicks and key presses from everything.

  • Any control can be a parent. For example, you can insert arbitrary images and labels inside a TCastleButton, to make it's content look in any way you want. If you want to group a couple of controls, but don't have a natural "parent" control, it's often a good idea to use a new instance of an TUIControlSizeable as a parent.

  • Controls are positioned relative to the parent, using just the Left and Bottom properties by default. Remember that our engine in 2D uses a coordinate system where the Y grows from zero (bottom) to maximum height (at the top). This is contrary to a convention of various GUI libraries that designate Top as zero, and make Y grow downward. We decided to follow the convention Y grows up for a couple of reasons, mostly because it naturally matches the 3D situation too (in 3D, our engine also follows the convention that Y grows up by default; so in 3D you get one additional dimension, Z, going "outside" of the screen, while X and Y axes are oriented the same in 2D and 3D).

    Usually, instead of assigning the positions using the Left and Bottom properties, it's better to use anchors. Anchors specify the position of some border (or center) of the control, relative to some border (or center) of it's parent. When the parent control is resized (e.g. when user resizes the window), children are automatically repositioned correctly. This usually avoids the need to react to window size changes in callbacks like Window.OnResize or TUIControl.Resize implementations.

  • Note that the parent does not clip the visibility of the children. That is, we assume that you will set the size of children small enough to make them fit within the parent. If you don't, the result will be a little unintuitive: the overflowing contents of children will be drawn outside of the rectangle of the parent, but they will not receive input (like mouse clicks). For this reason, it's best to make children actually fit within the parent.

    If you actually want to clip the children, use the TScissor. The TCastleScrollView implementation is a good example of how to use it properly.

With all this knowledge about parents and anchors, let's make a simple dialog box, showing off what we learned, and something extra (note the use of TCastleLabel.Html and HexToColor below). Below are the contents of the ApplicationInitialize procedure, you can just use this code to setup UI in the main program block (like in the simple examples above on the same page), or you can set it as the Application.OnInitialize callback following the chapter about developing cross-platform applications.

If in doubt, take a look at the examples/2d_standard_ui/zombie_fighter/game.pas code that contains the final application we will make in this manual! It uses the ApplicationInitialize procedure.

uses SysUtils, CastleControls, CastleUtils, CastleFilesUtils,
  CastleColors, CastleUIControls;
  Rect: TCastleRectangleControl;
  InsideRect: TCastleRectangleControl;
  Image: TCastleImageControl;
  LabelStats: TCastleLabel;
  ButtonRun, ButtonFight: TCastleButton;
procedure ApplicationInitialize;
  Rect := TCastleRectangleControl.Create(Application);
  Rect.Width := 400;
  Rect.Height := 500;
  Rect.Color := HexToColor('5f3939'); // equivalent: Vector4Single(95/255, 57/255, 57/255, 1.0);
  InsideRect := TCastleRectangleControl.Create(Application);
  InsideRect.Width := Rect.CalculatedWidth - 10;
  InsideRect.Height := Rect.CalculatedHeight - 10;
  InsideRect.Color := Silver;
  Image := TCastleImageControl.Create(Application);
  Image.URL := ApplicationData('Female-Zombie-300px.png');
  Image.Anchor(vpTop, -10);
  LabelStats := TCastleLabel.Create(Application);
  LabelStats.Color := Black;
  LabelStats.Html := true;
  { anything, just to show off the HTML :) }
  LabelStats.Caption := 'Statistics:' + NL +
    'Life: <font color="#ff0000">12%</font>' + NL +
    'Stamina: <font color="#ffff00">34%</font>' + NL +
    'Mana: <font color="#0000ff">56%</font>';
  LabelStats.Anchor(vpBottom, 100);
  ButtonRun := TCastleButton.Create(Application);
  ButtonRun.Caption := 'Run';
  ButtonRun.Anchor(hpLeft, 10);
  ButtonRun.Anchor(vpBottom, 10);
  ButtonRun.PaddingHorizontal := 40;
  ButtonFight := TCastleButton.Create(Application);
  ButtonFight.Caption := 'Fight';
  ButtonFight.Anchor(hpRight, -10);
  ButtonFight.Anchor(vpBottom, 10);
  ButtonFight.PaddingHorizontal := 40;

As you probably noticed, we do not have a visual UI designer yet. It's on the roadmap to make a visual designer integrated with Lazarus, so you could design the inside of TCastleControl and even TCastleWindow in the same comfortable way as you design a Lazarus form with standard Lazarus LCL components. Support the engine development to make this happen sooner!

4. User-interface scaling (Container.UIScaling)

You can use the UIScaling to automatically adjust all the UI controls sizes. You activate it like this:

Window.Container.UIReferenceWidth := 1024;
Window.Container.UIReferenceHeight := 768;
Window.Container.UIScaling := usEncloseReferenceSize;

This is incredibly important if you want your game to work on various window sizes. Which is especially important on mobile devices, where the sizes of the screen (in pixels) vary wildly.

This means that the whole user interface will be scaled, by the same ratio as if we would try to fit a 1024 x 768 area inside the user's window. The proportions will not be distorted, but things will get smaller or larger as necessary to accommodate to the larger window size. If you use anchors correctly, things will accommodate nicely to the various aspect ratios too.

The fact that things are scaled is largely hidden from you. You get and set the Left, Bottom, Anchor, CalculatedWidth, CalculatedHeight, CalculatedRect, FontSize and many other properties in the unscaled pixels. Which basically means that you can hardcode them, and they will still look right everywhere. Only a few properties uncover the final (real or scaled) control size. In particular the ScreenRect method (very useful for custom drawing) returns the control rectangle in the real device pixels (and with the anchors and parent transformations already applied). More about this in the chapter about custom-drawn UI controls.

5. Query sizes

You can check the resulting size of the control with CalculatedWidth and CalculatedHeight.

Beware: Many controls, like TCastleButton, expose also properties called Width and Height, but they are only to set an explicit size of the control (if you have disabled auto-sizing using TCastleButton.AutoSize or TCastleButton.AutoSizeWidth or such). They will not be updated when the control auto-sizing mechanism calculates the actual size. So do not use Width or Height properties to query the size of a button. Always use the CalculatedWidth or CalculatedHeight properties instead.

You can also use the CalculatedRect property, it contains the control position and size. But note that it's valid only after the control, and all it's parents, is part of a window that already received a Resize event. So you may need to wait for the Resize event to actually use this value.

6. Adjust theme

To adjust the look of some controls, you can adjust the theme. All of the standard 2D controls are drawn using theme images. This way the look of your game is defined by a set of images, that can be easily customized.

Use the Theme global variable (instance of TCastleTheme). For example, image type tiButtonNormal is the normal (not pressed or disabled) button look.

You can change it to one of your own images. Like this:

Theme.Images[tiButtonNormal] := LoadImage(ApplicationData('custom_button_normal.png'));
Theme.OwnsImages[tiButtonNormal] := true;
Theme.Corners[tiButtonNormal] := Vector4Integer(1, 1, 1, 1);

Note that we also adjust the Corners. The image will be drawn stretched, using the Draw3x3 to intelligently stretch taking the corners into account.

You can see the default images used in the engine sources, in src/ui/opengl/gui-images/ subdirectory. Feel free to base your images on them.

If you prefer to embed the image inside your application executable, you can do it using the image-to-pascal tool (compile it from the engine sources in tools/image-to-pascal/). You can then assign new image like this:

Theme.Images[tiButtonNormal] := CustomButtonNormal;
Theme.OwnsImages[tiButtonNormal] := false;
Theme.Corners[tiButtonNormal] := Vector4Integer(1, 1, 1, 1);

Note that we set OwnsImages to false in this case. The instance of CustomButtonNormal will be automatically freed in the finalization section of the unit generated by the image-to-pascal.

7. Taking control of the 2D viewport and scene manager

Multiple viewports, interactive scene, shadow volumes and cube-map reflections Multiple viewports with a DOOM level in view3dscene

By default, the TCastleWindow and TCastleControl already contain one item on their Controls list: the default SceneManager instance. For non-trivial games, you may prefer to start with a truly empty window (or control), and create your scene managers and viewports yourself. To do this, simply use the TCastleWindowCustom or TCastleControlCustom instead.

The example below creates a scene manager, showing the world from the player perspective, and then adds a viewport that observes the same world from another perspective. The key to understand this is that the scene manager serves by default two purposes:

  1. It's a central keeper of the information about your game world (in the SceneManager.Items).
  2. It is also a default viewport, that instantiates a camera to view this world. This works because TCastleSceneManager is a descendant of the TCastleAbstractViewport. You can deactivate this feature of scene manager by setting the SceneManager.DefaultViewport to false, then the scene manager is used only to store the information about your world, and you must use at least one TCastleViewport to actually see anything 3D (or anything 2D rendered using TCastleScene).

On the other hand, TCastleViewport always serves only one purpose: it's a viewport, and it always refers to some TCastleSceneManager instance to know what to show inside.

Often is makes sense to keep SceneManager.DefaultViewport as true, if you have something like a primary viewport of the user. And then use TCastleViewport only for secondary viewports. Sometimes, if's better to set SceneManager.DefaultViewport to false, and use only a number of TCastleViewport instances to show it. It really depends on what is easier for you conceptually, in a given game.

Note that, since the scene manager and viewports are 2D controls, you can place them as children of other UI controls. The example below demonstrates this technique, inserting TCastleViewport inside a TCastleRectangleControl.

Scene manager with custom viewport
uses SysUtils, CastleColors, CastleSceneCore, CastleScene, CastleFilesUtils,
  CastleWindow, CastleSceneManager, CastleControls, CastleUIControls,
  CastleCameras, CastleVectors;
  Window: TCastleWindowCustom;
  SceneManager: TCastleSceneManager;
  Scene: TCastleScene;
  ViewportRect: TCastleRectangleControl;
  Viewport: TCastleViewport;
  Window := TCastleWindowCustom.Create(Application);
  Window.Container.UIReferenceWidth := 1024;
  Window.Container.UIReferenceHeight := 768;
  Window.Container.UIScaling := usEncloseReferenceSize;
  { Add a black background underneath. You must always draw on
    the whole window area, otherwise it's contents are undefined.
    In this program, we don't have a full-size viewport filling the whole
    screen, so we use TCastleSimpleBackground to clear the screen. }
  Scene := TCastleScene.Create(Application);
  Scene.Spatial := [ssRendering, ssDynamicCollisions];
  Scene.ProcessEvents := true;
  SceneManager := TCastleSceneManager.Create(Application);
  SceneManager.FullSize := false;
  SceneManager.Left := 10;
  SceneManager.Bottom := 10;
  SceneManager.Width := 800;
  SceneManager.Height := 748;
  SceneManager.MainScene := Scene;
  (SceneManager.RequiredCamera as TUniversalCamera).NavigationType := ntWalk;
  (SceneManager.RequiredCamera as TUniversalCamera).Walk.MoveSpeed := 10;
  { otherwise, inputs are only passed
    when mouse cursor is over the SceneManager. }
  Window.Container.ForceCaptureInput := SceneManager;
  ViewportRect := TCastleRectangleControl.Create(Application);
  ViewportRect.FullSize := false;
  ViewportRect.Left := 820;
  ViewportRect.Bottom := 10;
  ViewportRect.Width := 256;
  ViewportRect.Height := 256;
  ViewportRect.Color := Silver;
  Viewport := TCastleViewport.Create(Application);
  Viewport.FullSize := false;
  Viewport.Left := 10;
  Viewport.Bottom := 10;
  Viewport.Width := 236;
  Viewport.Height := 236;
  Viewport.SceneManager := SceneManager;
  Viewport.Transparent := true;
  (Viewport.RequiredCamera as TUniversalCamera).NavigationType := ntNone;
  (Viewport.RequiredCamera as TUniversalCamera).SetView(
    Vector3Single(5, 92.00, 0.99),
    Vector3Single(0, -1, 0),
    Vector3Single(0, 0, 1));

7.1. Insert animation (full scene manager) into a button

2D animated scene inside a button

As the viewport may contain a TCastleScene with animation, and any viewport (including a scene manager) is just a 2D user-interface control, you can mix user-interface with animations freely. For example, you can design an animation in Spine, load it to T2DScene, insert it to T2DSceneManager, which you can then insert inside a TCastleButton. Thus you can have a button with any crazy animation inside:)

uses SysUtils, CastleVectors, CastleCameras,
  CastleColors, CastleSceneCore, CastleScene, CastleFilesUtils,
  CastleUIControls, CastleWindow, Castle2DSceneManager, CastleControls;
  Window: TCastleWindow;
  Button: TCastleButton;
  MyLabel: TCastleLabel;
  SceneManager: T2DSceneManager;
  Scene: T2DScene;
  Window := TCastleWindow.Create(Application);
  Button := TCastleButton.Create(Application);
  Button.AutoSize := false;
  Button.Width := 400;
  Button.Height := 400;
  MyLabel := TCastleLabel.Create(Application);
  MyLabel.Caption := 'Click here for more dragons!';
  MyLabel.Anchor(vpTop, -10);
  MyLabel.Color := Black;
  Scene := T2DScene.Create(Application);
  Scene.Spatial := [ssRendering, ssDynamicCollisions];
  Scene.ProcessEvents := true;
  Scene.PlayAnimation('flying', paForceLooping);
  SceneManager := T2DSceneManager.Create(Application);
  SceneManager.FullSize := false;
  SceneManager.Width := 390;
  SceneManager.Height := 350;
  SceneManager.Anchor(vpBottom, 10);
  SceneManager.MainScene := Scene;
  { below adjusted to the scene size and position }
  SceneManager.ProjectionAutoSize := false;
  SceneManager.ProjectionWidth := 3000;
  SceneManager.ProjectionOriginCenter := true;
  (SceneManager.RequiredCamera as TUniversalCamera).SetView(
    Vector3Single(0, 500, T2DSceneManager.DefaultCameraZ),
    Vector3Single(0, 0, -1),
    Vector3Single(0, 1, 0));

8. Wrapping it up (in a custom TUIControl descendant)

Dialog box composed from simple UI elements

When you make a non-trivial composition of UI controls, it's a good idea to wrap them in a parent UI control class.

For this, you can derive a new descendant of your top-most UI class. The top-most UI class can be

  1. something specific, like the TCastleRectangleControl if your whole UI is inside a simple rectangle,
  2. or it can be our universal "UI control with size" TUIControlSizeable,
  3. or it can be our abstract TUIControl (in this last case you will need to carefully override it's TUIControl.Rect method).

In the constructor of your new class, you initialize and add all the child controls. You can even register private methods to handle the events of private controls inside, e.g. you can internally handle button clicks inside your new class.

This way you get a new class, like TZombieDialog, that is a full-featured UI control. It hides the complexity of the UI inside, and it exposes only as much as necessary to the outside world. It has a fully functional Update method to react to time passing, it can handle inputs and so on.

The new UI control can be inserted directly to the Window.Controls, or it can be used as a child of other UI controls, to create even more complex stuff. It can be aligned within parent using the normal Anchor methods.

Example below implements the TZombieDialog class, which is a reworked version of the previous UI example, that now wraps the dialog UI inside a neat reusable class.

uses SysUtils, Classes, CastleControls, CastleUtils, CastleFilesUtils,
  CastleColors, CastleUIControls;
  TZombieDialog = class(TCastleRectangleControl)
    InsideRect: TCastleRectangleControl;
    Image: TCastleImageControl;
    LabelStats: TCastleLabel;
    ButtonRun, ButtonFight: TCastleButton;
    constructor Create(AOwner: TComponent); override;
constructor TZombieDialog.Create(AOwner: TComponent);
  Width := 400;
  Height := 500;
  Color := HexToColor('5f3939');
  InsideRect := TCastleRectangleControl.Create(Self);
  InsideRect.Width := CalculatedWidth - 10;
  InsideRect.Height := CalculatedHeight - 10;
  InsideRect.Color := Silver;
  Image := TCastleImageControl.Create(Self);
  // ... see previous example for the rest of Image initialization
  LabelStats := TCastleLabel.Create(Self);
  // ... see previous example for the rest of LabelStats initialization
  ButtonRun := TCastleButton.Create(Self);
  // ... see previous example for the rest of ButtonRun initialization
  ButtonFight := TCastleButton.Create(Self);
  // ... see previous example for the rest of ButtonFight initialization
  SimpleBackground: TCastleSimpleBackground;
  Dialog: TZombieDialog;
procedure ApplicationInitialize;
  Window.Container.UIReferenceWidth := 1024;
  Window.Container.UIReferenceHeight := 768;
  Window.Container.UIScaling := usEncloseReferenceSize;
  SimpleBackground := TCastleSimpleBackground.Create(Application);
  SimpleBackground.Color := Black;
  Dialog := TZombieDialog.Create(Application);

9. User-interface state (TUIState)

Multiple viewports and basic game UI
UI dialog, in a state over the game UI

To go one step further, consider organizing larger games into "states". The idea is that the game is always within some state, and the state is also reflected by some user-interface. We have a ready class TIUState in our engine that helps you take care of that.

In the typical usecase, you create many descendants of the class TIUState. Each descendant represents a different state, like TStateMainMenu, TStatePlay, TStatePause and so on. Usually you create a single instance for each of these classes, at the beginning of your game (e.g. in Application.OnInitialize handler).

Each such class contains the user-interface appropriate in the given state. As TIUState is itself a special TUIControl descendant, it can act as a parent (always filling the whole window) for other UI controls. You can add children controls:

  • In the state constructor.

  • Or you can add them in every Start call, overriding it. In this case, you should remove the controls in the Stop method. Or you can set the controls' owner to a special FreeAtStop component, to make them freed and removed automatically at the next Stop call.

  • For advanced uses, if you will use the state stack, you can also add / remove children in the Resume and Pause calls.

During the game you use TIUState class methods and properties to change the current state. Most importantly, you can simply change to a new state by setting "TUIState.Current := NewState;". This will call Stop on the old state, and Start on the new state (these are methods that you can override to do something useful).

For advanced uses, you can also have a "state stack". This is perfectly useful when one user-interface is displayed on top of another, for example when the TStatePause shows a dimmed state of the game underneath. Be sure to actually pause the game underneath; you can make a "dimmed" look by adding a fullscreen TCastleRectangleControl with a transparent color (that has alpha between 0 and 1, like 0.5). If you don't want the underlying state to also receive the inputs, be sure to set InterceptInput on the top state (TStatePause in this example).

To actually change the state using the "stack" mechanism, use the TUIState.Push and TUIState.Pop methods.

The example game zombie_fighter shows a simple implementation of TStateMainMenu, TStatePlay, TStateAskDialog. The TStateAskDialog is activated when you click the zombie sprite in the TStatePlay, it then shows our TZombieDialog created above.