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 TCastleWindowBase or TCastleControlBase, 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 TCastleUserInterface 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 TCastleWindowBase.Controls (if you use TCastleWindowBase) or TCastleControlBase.Controls (if you use Lazarus forms with TCastleControlBase). 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 TCastleUserInterface can be arranged in hierarchy: one instance of TCastleUserInterface 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 TCastleWindowBase or TCastleControlBase 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 TCastleUserInterface descendant. Note that a scene manager by default acts also as a viewport into the 3D world, and it's also a descendant of TCastleUserInterface. 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 TCastleUserInterface in our engine.

While TCastleUserInterface 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 TCastleUserInterface 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.
  • TCastleUserInterface itself is also very useful - 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 TCastleUserInterface 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) TCastleUserInterface. Note that TCastleUserInterface 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, CastleColors;
 
var
  Window: TCastleWindowBase;
  MyLabel: TCastleLabel;
  MyButton: TCastleButton;
begin
  Window := TCastleWindowBase.Create(Application);
  Window.Open;
 
  MyButton := TCastleButton.Create(Application);
  MyButton.Caption := 'Click me!';
  MyButton.Anchor(hpMiddle);
  MyButton.Anchor(vpBottom, 10);
  Window.Controls.InsertFront(MyButton);
 
  MyLabel := TCastleLabel.Create(Application);
  MyLabel.Caption := 'Click on the button!';
  MyLabel.Color := White;
  MyLabel.Anchor(hpMiddle);
  { position label such that it's over the button }
  MyLabel.Anchor(vpBottom, 10 + MyButton.EffectiveHeight + 10);
  Window.Controls.InsertFront(MyLabel);
 
  Application.Run;
end.

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;
 
var
  Window: TCastleWindowBase;
  MyRect: TCastleRectangleControl;
  HideRectButton: TCastleButton;
 
type
  TEventHandler = class
    class procedure ButtonClick(Sender: TObject);
  end;
 
class procedure TEventHandler.ButtonClick(Sender: TObject);
begin
  MyRect.Exists := not MyRect.Exists;
  HideRectButton.Pressed := not MyRect.Exists;
end;
 
begin
  Window := TCastleWindowBase.Create(Application);
  Window.Open;
 
  HideRectButton := TCastleButton.Create(Application);
  HideRectButton.Caption := 'Hide rectangle';
  HideRectButton.Toggle := true;
  HideRectButton.Anchor(hpMiddle);
  HideRectButton.Anchor(vpBottom, 10);
  { use a trick to avoid creating a useless instance
    of the TEventHandler class. }
  HideRectButton.OnClick := @TEventHandler(nil).ButtonClick;
  Window.Controls.InsertFront(HideRectButton);
 
  MyRect := TCastleRectangleControl.Create(Application);
  MyRect.Color := Yellow;
  MyRect.Width := 200;
  MyRect.Height := 200;
  MyRect.Anchor(hpMiddle);
  MyRect.Anchor(vpMiddle);
  Window.Controls.InsertFront(MyRect);
 
  Application.Run;
end.

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 TCastleWindowBase.OnPress or TCastleControlBase.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 TCastleUserInterface 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 TCastleUserInterface 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 TCastleUserInterface.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, set the ClipChildren to true.

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/ code that contains the final application we will make in this manual! It uses the ApplicationInitialize procedure.

uses SysUtils, CastleControls, CastleUtils, CastleFilesUtils,
  CastleColors, CastleUIControls;
 
var
  Rect: TCastleRectangleControl;
  InsideRect: TCastleRectangleControl;
  Image: TCastleImageControl;
  LabelStats: TCastleLabel;
  ButtonRun, ButtonFight: TCastleButton;
 
procedure ApplicationInitialize;
begin
  Rect := TCastleRectangleControl.Create(Application);
  Rect.Width := 400;
  Rect.Height := 500;
  Rect.Color := HexToColor('5f3939'); // equivalent: Vector4(95/255, 57/255, 57/255, 1.0);
  Rect.Anchor(hpMiddle);
  Rect.Anchor(vpMiddle);
  Window.Controls.InsertFront(Rect);
 
  InsideRect := TCastleRectangleControl.Create(Application);
  InsideRect.Width := Rect.EffectiveWidth - 10;
  InsideRect.Height := Rect.EffectiveHeight - 10;
  InsideRect.Color := Silver;
  InsideRect.Anchor(hpMiddle);
  InsideRect.Anchor(vpMiddle);
  Rect.InsertFront(InsideRect);
 
  Image := TCastleImageControl.Create(Application);
  Image.URL := 'castle-data:/Female-Zombie-300px.png';
  Image.Anchor(hpMiddle);
  Image.Anchor(vpTop, -10);
  InsideRect.InsertFront(Image);
 
  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(hpMiddle);
  LabelStats.Anchor(vpBottom, 100);
  InsideRect.InsertFront(LabelStats);
 
  ButtonRun := TCastleButton.Create(Application);
  ButtonRun.Caption := 'Run';
  ButtonRun.Anchor(hpLeft, 10);
  ButtonRun.Anchor(vpBottom, 10);
  ButtonRun.PaddingHorizontal := 40;
  InsideRect.InsertFront(ButtonRun);
 
  ButtonFight := TCastleButton.Create(Application);
  ButtonFight.Caption := 'Fight';
  ButtonFight.Anchor(hpRight, -10);
  ButtonFight.Anchor(vpBottom, 10);
  ButtonFight.PaddingHorizontal := 40;
  InsideRect.InsertFront(ButtonFight);
end;

Note that we have a visual designer for UI. It is described in the separate chapter.

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;

Using UI scaling 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, EffectiveWidth, EffectiveHeight, EffectiveRect, 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 RenderRect 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. Using CastleSettings.xml file to define UIScaling and other properties

Since Castle Game Engine 6.5, you can alternatively activate UI scaling by creating a file called CastleSettings.xml in the data subdirectory of your project. The sample CastleSettings.xml contents look like this:

<?xml version="1.0" encoding="utf-8"?>
<castle_settings>
  <ui_scaling
    mode="EncloseReferenceSize"
    reference_width="1024"
    reference_height="768"
  />
</castle_settings>

Then in your Application.OnInitialize callback just call Window.Container.LoadSettings('castle-data:/CastleSettings.xml');. This will set UIScaling.

It can also set other global (for the whole container) properties, like the default font.

The advantage of using the CastleSettings.xml file is that the editor reads this file too, and can apply the same scaling, default font and so on when you edit the UI.

Read more about the CastleSettings.xml file.

6. Query sizes

You can check the resulting size of the control with EffectiveWidth and EffectiveHeight.

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 EffectiveWidth or EffectiveHeight properties instead.

You can also use the EffectiveRect 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.

7. 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('castle-data:/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.

To adjust the initial "Loading" image (visible when you open the application window) you want to adjust the Theme.Images[tiLoading] image. The process is described in detail here.

8. Taking control of the viewport

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

You should use TCastleWindowBase or TCastleControlBase to create an area where Castle Game Engine can render. And you should create an instance of TCastleViewport and add it to the Controls list (TCastleWindowBase.Controls or TCastleControlBase.Controls).

It is allowed to have multiple TCastleViewport instances in your game, even visible at the same time. They can even show the same world (but from different cameras) if they share the same Viewport.Items value.

The example below creates one viewport, showing the world from the player perspective, and then adds another viewport that observes the same world from another perspective.

Note that, since the viewport is a 2D control, you can place it as child 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, CastleViewport, CastleControls, CastleUIControls,
  CastleCameras, CastleVectors;
var
  Window: TCastleWindowBase;
  MainViewport: TCastleViewport;
  Scene: TCastleScene;
  AdditionalViewport: TCastleViewport;
  AdditionalViewportContainer: TCastleRectangleControl;
begin
  Window := TCastleWindowBase.Create(Application);
  Window.Container.UIReferenceWidth := 1024;
  Window.Container.UIReferenceHeight := 768;
  Window.Container.UIScaling := usEncloseReferenceSize;
  Window.Open;
 
  Scene := TCastleScene.Create(Application);
  Scene.Load('castle-data:/level1.x3d');
  Scene.Spatial := [ssRendering, ssDynamicCollisions];
  Scene.ProcessEvents := true;
 
  MainViewport := TCastleViewport.Create(Application);
  MainViewport.AutoCamera := true;
  MainViewport.Left := 10;
  MainViewport.Bottom := 10;
  MainViewport.Width := 800;
  MainViewport.Height := 748;
  MainViewport.Items.Add(Scene);
  MainViewport.Items.MainScene := Scene;
  MainViewport.NavigationType := ntWalk;
  MainViewport.WalkNavigation.MoveSpeed := 10;
  Window.Controls.InsertFront(MainViewport);
 
  { otherwise, inputs are only passed
    when mouse cursor is over the MainViewport. }
  Window.Container.ForceCaptureInput := MainViewport;
 
  AdditionalViewportContainer := TCastleRectangleControl.Create(Application);
  AdditionalViewportContainer.FullSize := false;
  AdditionalViewportContainer.Left := 820;
  AdditionalViewportContainer.Bottom := 10;
  AdditionalViewportContainer.Width := 256;
  AdditionalViewportContainer.Height := 256;
  AdditionalViewportContainer.Color := Silver;
  Window.Controls.InsertFront(AdditionalViewportContainer);
 
  AdditionalViewport := TCastleViewport.Create(Application);
  AdditionalViewport.FullSize := false;
  AdditionalViewport.Left := 10;
  AdditionalViewport.Bottom := 10;
  AdditionalViewport.Width := 236;
  AdditionalViewport.Height := 236;
  AdditionalViewport.Items := MainViewport.Items;
  AdditionalViewport.Transparent := true;
  AdditionalViewport.Camera.SetView(
    Vector3(5, 92.00, 0.99),
    Vector3(0, -1, 0),
    Vector3(0, 0, 1));
  AdditionalViewportContainer.InsertFront(AdditionalViewport);
 
  Application.Run;
end.

8.1. Insert animation (in a viewport) into a button

2D animated scene inside a button

As the viewport may contain a TCastleScene with animation, and a viewport 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 TCastleScene, insert it to TCastleViewport, 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, CastleViewport,
  CastleUIControls, CastleWindow, CastleControls;
var
  Window: TCastleWindowBase;
  Button: TCastleButton;
  MyLabel: TCastleLabel;
  Viewport: TCastleViewport;
  Scene: TCastleScene;
begin
  Window := TCastleWindowBase.Create(Application);
  Window.Open;
 
  Button := TCastleButton.Create(Application);
  Button.Anchor(hpMiddle);
  Button.Anchor(vpMiddle);
  Button.AutoSize := false;
  Button.Width := 400;
  Button.Height := 400;
  Window.Controls.InsertFront(Button);
 
  MyLabel := TCastleLabel.Create(Application);
  MyLabel.Caption := 'Click here for more dragons!';
  MyLabel.Anchor(hpMiddle);
  MyLabel.Anchor(vpTop, -10);
  MyLabel.Color := Black;
  Button.InsertFront(MyLabel);
 
  Scene := TCastleScene.Create(Application);
  Scene.Setup2D;
  Scene.Load('castle-data:/dragon/dragon.json');
  Scene.Spatial := [ssRendering, ssDynamicCollisions];
  Scene.ProcessEvents := true;
  Scene.PlayAnimation('flying', true);
 
  Viewport := TCastleViewport.Create(Application);
  Viewport.Setup2D;
  Viewport.Transparent := true;
  Viewport.FullSize := false;
  Viewport.Width := 390;
  Viewport.Height := 350;
  Viewport.Anchor(hpMiddle);
  Viewport.Anchor(vpBottom, 10);
  Viewport.Items.Add(Scene);
  Viewport.Items.MainScene := Scene;
  { below adjusted to the scene size and position }
  Viewport.Camera.Orthographic.Width := 3000;
  Viewport.Camera.Orthographic.Origin := Vector2(0.5, 0.5);
  Viewport.Camera.SetView(
    Vector3(0, 500, TCastleViewport.Default2DCameraZ),
    Vector3(0, 0, -1),
    Vector3(0, 1, 0));
  Button.InsertFront(Viewport);
 
  Application.Run;
end.

9. Wrapping it up (in a custom TCastleUserInterface 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 position and size": TCastleUserInterface.

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;
 
type
  TZombieDialog = class(TCastleRectangleControl)
  private
    InsideRect: TCastleRectangleControl;
    Image: TCastleImageControl;
    LabelStats: TCastleLabel;
    ButtonRun, ButtonFight: TCastleButton;
  public
    constructor Create(AOwner: TComponent); override;
  end;
 
constructor TZombieDialog.Create(AOwner: TComponent);
begin
  inherited;
 
  Width := 400;
  Height := 500;
  Color := HexToColor('5f3939');
 
  InsideRect := TCastleRectangleControl.Create(Self);
  InsideRect.Width := EffectiveWidth - 10;
  InsideRect.Height := EffectiveHeight - 10;
  InsideRect.Color := Silver;
  InsideRect.Anchor(hpMiddle);
  InsideRect.Anchor(vpMiddle);
  InsertFront(InsideRect);
 
  Image := TCastleImageControl.Create(Self);
  // ... see previous example for the rest of Image initialization
  InsideRect.InsertFront(Image);
 
  LabelStats := TCastleLabel.Create(Self);
  // ... see previous example for the rest of LabelStats initialization
  InsideRect.InsertFront(LabelStats);
 
  ButtonRun := TCastleButton.Create(Self);
  // ... see previous example for the rest of ButtonRun initialization
  InsideRect.InsertFront(ButtonRun);
 
  ButtonFight := TCastleButton.Create(Self);
  // ... see previous example for the rest of ButtonFight initialization
  InsideRect.InsertFront(ButtonFight);
end;
 
var
  SimpleBackground: TCastleSimpleBackground;
  Dialog: TZombieDialog;
 
procedure ApplicationInitialize;
begin
  Window.Container.UIReferenceWidth := 1024;
  Window.Container.UIReferenceHeight := 768;
  Window.Container.UIScaling := usEncloseReferenceSize;
 
  SimpleBackground := TCastleSimpleBackground.Create(Application);
  SimpleBackground.Color := Black;
  Window.Controls.InsertFront(SimpleBackground);
 
  Dialog := TZombieDialog.Create(Application);
  Dialog.Anchor(hpMiddle);
  Dialog.Anchor(vpMiddle);
  Window.Controls.InsertFront(Dialog);
end;

10. 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 TCastleUserInterface 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.