Custom drawn 2D controls: player HUD

Player HUD we are going to create
fps_game demo player HUD showing inventory
"The Castle" inventory

1. Introduction

While you can compose user interface from existing UI controls, sometimes it's more flexible to create a control that renders what you want using lower-level utilities for 2D drawing.

An example that we will consider in this chapter is designing a HUD (heads-up display) that displays some player information, like current life.

There are two places where you can draw:

  1. You can just place the appropriate drawing code in OnRender event (see TCastleWindow.OnRender, TCastleControl.OnRender). This is simple to use, and works OK for simple applications.

  2. In the long-term, it's usually better to create your own TCastleUserInterface descendant. This way you wrap the rendering (and possibly other processing) inside your own class. You can draw anything you want in the overridden TCastleUserInterface.Render method.

2. Initial code

Here's a simple start of a 2D control class definition and usage. It shows the player health by simply writing it out as text.

uses SysUtils, Classes,
  CastleColors, CastleVectors, CastleWindow, CastleUIControls,
  CastleControls, CastleRectangles;
 
type
  TPlayerInformation = class(TComponent)
  public
    Life, MaxLife: Single;
  end;
 
var
  Window: TCastleWindow;
  PlayerInformation: TPlayerInformation;
 
type
  TPlayerHud = class(TCastleUserInterface)
  public
    procedure Render; override;
  end;
 
procedure TPlayerHud.Render;
begin
  inherited;
  UIFont.Print(20, 20, Yellow, Format('Player life: %f / %f', [
    PlayerInformation.Life,
    PlayerInformation.MaxLife
  ]));
end;
 
var
  PlayerHud: TPlayerHud;
begin
  Window := TCastleWindow.Create(Application);
  Window.Open;
 
  PlayerInformation := TPlayerInformation.Create(Application);
  PlayerInformation.Life := 75;
  PlayerInformation.MaxLife := 100;
 
  PlayerHud := TPlayerHud.Create(Application);
  Window.Controls.InsertFront(PlayerHud);
 
  Application.Run;
end.

3. Drawing stuff

Inside TPlayerHud.Render you can draw using our 2D drawing API.

3.1. Text

To draw a text, you can use ready global font UIFont (in CastleControls unit). This is an instance of TCastleFont. For example, you can show player's health like this:

UIFont.Print(10, 10, Yellow, Format('Player life: %f / %f', [
  PlayerInformation.Life,
  PlayerInformation.MaxLife
]));

You can also create your own instances of TCastleFont to have more fonts. See the manual chapter about "Text and fonts" for more.

Note: It is more advised (easier, more flexible) to use TCastleLabel control than to draw text like above.

3.2. Rectangles, circles, other shapes

To draw a rectangle use the DrawRectangle method. Blending is automatically used if you pass color with alpha < 1.

For example, we can show a nice health bar showing the player's life:

procedure TPlayerHud.Render;
var
  R: TFloatRectangle;
begin
  inherited;
 
  R := FloatRectangle(10, 10, 400, 50);
  { draw background of health bar with a transparent red }
  DrawRectangle(R, Vector4(1, 0, 0, 0.5));
  { calculate smaller R, to only include current life }
  R := R.Grow(-3);
  R.Width := R.Width * PlayerInformation.Life / PlayerInformation.MaxLife;
  { draw the inside of health bar with an opaque red }
  DrawRectangle(R, Vector4(1, 0, 0, 1));
 
  UIFont.Print(20, 20, Yellow, Format('Player life: %f / %f', [
    PlayerInformation.Life,
    PlayerInformation.MaxLife
  ]));
end;

Note: It is more advised (easier, more flexible) to use TCastleRectangleControl control than to draw rectangle like above.

To draw a circle use the DrawCircle. There are also procedures to draw only an outline: DrawRectangleOutline DrawCircleOutline.

Note: It is more advised (easier, more flexible) to use TCastleShape control than to draw shapes like above.

To draw an arbitrary 2D primitive use the DrawPrimitive2D method. Blending is automatically used if you pass color with alpha < 1.

3.3. Images

To draw an image, use the TDrawableImage class. It has methods TDrawableImage.Draw and TDrawableImage.Draw3x3 to draw the image, intelligently stretching it, optionally preserving unstretched corners.

Here's a simple example of TDrawableImage usage to display a hero's face. You can use an image below, if you're old enough to recognize it:) (Source.)

DOOM hero face

uses ..., Classes, CastleFilesUtils, CastleGLImages;
 
type
  TPlayerHud = class(TCastleUserInterface)
  private
    FMyImage: TDrawableImage;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Render; override;
  end;
 
constructor TPlayerHud.Create(AOwner: TComponent);
begin
  inherited;
  FMyImage := TDrawableImage.Create('castle-data:/face.png');
end;
 
destructor TPlayerHud.Destroy;
begin
  FreeAndNil(FMyImage);
  inherited;
end;
 
procedure TPlayerHud.Render;
begin
  inherited;
 
  // ... previous TPlayerHud.Render contents ...
 
  FMyImage.Draw(420, 10);
end;

Note: Note: It is more advised (easier, more flexible) to use TCastleImageControl control than to draw image like above.

3.4. Complete example code showing above features

Here's a complete source code that shows the above features. You can download and compile it right now!

uses SysUtils, Classes,
  CastleColors, CastleVectors, CastleWindow, CastleUIControls,
  CastleControls, CastleRectangles, CastleGLUtils, CastleFilesUtils, CastleGLImages;
 
type
  TPlayerInformation = class(TComponent)
  public
    Life, MaxLife: Single;
  end;
 
var
  Window: TCastleWindow;
  PlayerInformation: TPlayerInformation;
 
type
  TPlayerHud = class(TCastleUserInterface)
  private
    FMyImage: TDrawableImage;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Render; override;
  end;
 
constructor TPlayerHud.Create(AOwner: TComponent);
begin
  inherited;
  FMyImage := TDrawableImage.Create('castle-data:/face.png');
end;
 
destructor TPlayerHud.Destroy;
begin
  FreeAndNil(FMyImage);
  inherited;
end;
 
procedure TPlayerHud.Render;
var
  R: TFloatRectangle;
begin
  inherited;
 
  R := FloatRectangle(10, 10, 400, 50);
  { draw background of health bar with a transparent red }
  DrawRectangle(R, Vector4(1, 0, 0, 0.5));
  { calculate smaller R, to only include current life }
  R := R.Grow(-3);
  R.Width := R.Width * PlayerInformation.Life / PlayerInformation.MaxLife;
  { draw the inside of health bar with an opaque red }
  DrawRectangle(R, Vector4(1, 0, 0, 1));
 
  UIFont.Print(20, 20, Yellow, Format('Player life: %f / %f', [
    PlayerInformation.Life,
    PlayerInformation.MaxLife
  ]));
 
  FMyImage.Draw(420, 10);
end;
 
var
  PlayerHud: TPlayerHud;
begin
  Window := TCastleWindow.Create(Application);
  Window.Open;
 
  PlayerInformation := TPlayerInformation.Create(Application);
  PlayerInformation.Life := 75;
  PlayerInformation.MaxLife := 100;
 
  PlayerHud := TPlayerHud.Create(Application);
  Window.Controls.InsertFront(PlayerHud);
 
  Application.Run;
end.

3.5. Animations from images (movies, sprite sheets)

If you would like to display a series of images, not a static image, you can use TGLVideo2D (show image sequence from many separate images or a video).

See e.g. our game "Muuu" for a demo of using sprite animations.

3.6. Player inventory

The TPlayer class manages the player inventory. Each inventory item may already have a default image associated with it. It is defined in the resource.xml file of the item, see the chapter about using creatures / items and see the chapter about defining creatures / items resource.xml files and see the examples/fps_game/data/item_medkit/ for an example item definition.

The image is available as a TDrawableImage instance ready for drawing. For example, you can iterate over the inventory list and show the items like this:

for I := 0 to Player.Inventory.Count - 1 do
  Player.Inventory[I].Resource.GLImage.Draw(I * 100, 0);

See the examples/fps_game/ for a working example of this.

3.7. Screen fade effects

For simple screen fade effects, you have procedures inside the CastleGLUtils unit called GLFadeRectangleDark and GLFadeRectangleLight. These allow you to draw a rectangle representing fade out (when player is in pain).

For example you can visualize pain and dead states like this:

if Player.Dead then
  GLFadeRectangleDark(ContainerRect, Red, 1.0)
else
  GLFadeRectangleDark(ContainerRect, Player.FadeOutColor, Player.FadeOutIntensity);

Note that Player.FadeOutIntensity will be 0 when there is no pain, which cooperates nicely with GLFadeRectangleDark definition that will do nothing when 4th parameter is 0. That is why we carelessly always call GLFadeRectangleDark — when player is not dead, and is not in pain (Player.FadeOutIntensity = 0) then nothing will actually happen.

Note: There is also a full-featured UI control that draws an effect with blending (possibly modulated by an image): TCastleFlashEffect.

4. Coordinates and window (container) sizes

To adjust your code to window size, note that our projection has (0,0) in lower-left corner (as is standard for 2D OpenGL). You can look at the size, in pixels, of the current OpenGL container (window, control) in TCastleUserInterface.ContainerWidth x TCastleUserInterface.ContainerHeight or (as a rectangle) as TCastleUserInterface.ContainerRect. The container size is also available as container properties, like TCastleWindow.Width x TCastleWindow.Height or (as a rectangle) TCastleWindow.Rect.

5. Take into account UI scaling and anchors

So far, we have simply carelessly drawn our contents over the window.

Note that it is OK to ignore (some) of these issues, if you design a UI control specifically for your game, and you know that it's only going to be used in a specific way.

To have more full-featured UI control, we could solve these issues "one by one", but as you can see there are quite a few features that are missing. The easiest way to handle all the features listed above is to get inside the Render method the values of RenderRect and UIScale. Just scale your drawn contents to always fit within the RenderRect rectangle. And scale all user size properties by UIScale before applying to pixels.

Like this:

procedure TMyImageControl.Render;
begin
  inherited;
  FMyImage.Draw(RenderRect);
end;
 
var
  MyControl: TMyImageControl;
begin
  MyControl := TMyImageControl.Create(Application);
  MyControl.Left := 100;
  MyControl.Bottom := 200;
  MyControl.Width := 300;
  MyControl.Height := 400;
  Window.Controls.InsertFront(MyControl);
end;

6. Examples

See examples/fps_game for a working and fully-documented demo of such TPlayerHud implementation. See "The Castle" sources (unit GamePlay) for an example implementation that shows more impressive player's life indicator and inventory and other things on the screen.