12. Display 2D controls: player HUD

You will probably want to draw some 2D controls on your screen, even if your game is 3D. For example you will want to display some player information, like current life or inventory, as a HUD (heads-up display). Note that you can use the standard TPlayer class to automatically store and update your player's life and inventory (see TPlayer and ancestors properties, like T3DAliveWithInventory.Inventory and T3DAlive.Life). But this information is not displayed automatically in any way, since various games have wildly different needs.

In the simple cases, to display something in 2D, just place the appropriate drawing code in OnRender event (see TCastleWindowCustom.OnRender, TCastleControlCustom.OnRender).

In the not-so-simple cases, when your game grows larger and you want to easily manage the stuff you display, it is best to define a new TUIControl descendant, where you can draw anything you want in the overridden TUIControl.Render method. A simple example:

uses ..., CastleUIControls;
 
type
  TGame2DControls = class(TUIControl)
  public
    procedure Render; override;
  end;
 
procedure TGame2DControls.Render;
begin
  { ... }
end;
 
{ When starting your game, create TGame2DControls instance
  and add it to Window.Controls }
var
  Controls2D: TGame2DControls;
...
  Controls2D := TGame2DControls.Create(Application);
  Window.Controls.Add(Controls2D);

Inside TGame2DControls.Render you have the full knowledge about the world. For example, you can access the player of your 3D game using Window.SceneManager.Player (see tutorial about Player for FPS game). You can draw them using our 2D drawing API:

  • 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', [Player.Life, Player.MaxLife]));

    Of course you can also create your own instances of TCastleFont to have more fonts. See the tutorial chapter about text and fonts for more.

    Note that in simple cases, you don't need to render the text this way. You can instead use ready control TCastleLabel (see the tutorial about text). But drawing font yourself is often more flexible.

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

    Note that in simple cases, you don't need to render the image this way. You can instead use ready control TCastleImageControl. But drawing image yourself is often more flexible.

    Note that TGLImage is an OpenGL resource — which means that usually you create it in GLContextOpen and destroy in GLContextClose methods of your TUIControl descendant. The example below shows this technique.

    In non-trivial cases, sometimes this requirement of TGLImage is too cumbersome, and coding correct GLContextOpen and GLContextClose methods is uncomfortable. In this case, you can simply use TGLImageManaged that can be created and destroyed at any point within your application (and it's instance can "survive" the recreation of OpenGL context).

    Here's a promised example of optimal TGLImage creation and destruction:

    uses ..., CastleGLImages, CastleUIControls;
     
    type
      TGame2DControls = class(TUIControl)
      private
        FMyImage: TGLImage;
      public
        procedure Render; override;
        procedure GLContextOpen; override;
        procedure GLContextClose; override;
      end;
     
    procedure TGame2DControls.GLContextOpen;
    begin
      inherited;
      FMyImage := TGLImage.Create(ApplicationData('map_tile.png'));
    end;
     
    procedure TGame2DControls.GLContextClose;
    begin
      FreeAndNil(FMyImage);
      inherited;
    end;
     
    procedure TGame2DControls.Render;
    begin
      inherited;
      FMyImage.Draw(100, 200);
    end;
  • Note about UI scaling and TGLImage. In engine version 5.3.0, we add to UI system parents, anchoring, and automatic UI scaling.

    If you would like your own 2D controls to honor this system, your Render method will need to take them into account, and you should also override the Rect method. Use helpers like ScreenRect or UIScale to translate/scale your control correctly.

    function TGame2DControls.Rect: TRectangle;
    begin
      Result := Rectangle(Left, Bottom, FMyImage.Width, FMyImage.Height);
      Result := Result.ScaleAround0(UIScale);
    end;
     
    procedure TGame2DControls.Render;
    begin
      inherited;
      FMyImage.Draw(ScreenRect);
    end;
  • DrawRectangle allows to easily draw 2D rectangle filled with color. Blending is automatically used if you pass color with alpha < 1.

  • Every inventory item has already loaded image (defined in resource.xml), as TCastleImage (image stored in normal memory, see CastleImages unit) and TGLImage (image stored in GPU memory, easy to render, see CastleGLImages unit). For example, you can iterate over inventory list and show them like this:

    for I := 0 to Player.Inventory.Count - 1 do
      Player.Inventory[I].Resource.GLImage.Draw(I * 100, 0);
  • 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 TUIControl.ContainerWidth x TUIControl.ContainerHeight or (as a rectangle) as TUIControl.ContainerRect. The container size is also available as container properties, like TCastleWindow.Width x TCastleWindow.Height or (as a rectangle) TCastleWindow.Rect.

  • For simple screen fade effects, you have procedures inside CastleGLUtils unit like GLFadeRectangle. This allows you to draw a rectangle representing fade out (when player is in pain). And TPlayer instance already has properties Player.FadeOutColor, Player.FadeOutIntensity representing when player is in pain (and the pain color). Player.Dead says when player is dead (this is simply when Life <= 0).

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

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

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

  • Use the Theme global variable (instance of TCastleTheme) to draw using a predefined set of images. For example, image type tiActiveFrame is a general-purpose frame that you can use to mark a specific region on the screen. You draw it like this:

    Theme.Draw(Rectangle(10, 10, 100, 100), tiActiveFrame);

    You can change all the theme images. You can change them to one of the predefined images in CastleControlsImages unit. Like this:

    Theme.Images[tiActiveFrame] := FrameYellow;
    Theme.Corners[tiActiveFrame] := Vector4Integer(1, 1, 1, 1);

    Or you can change them to one of your own images. Like this:

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

    All our 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 changed by artists.

    If the set of predefined images in Theme is too limiting, then use TGLImage directly.

  • CastleGLUtils, and some other units, provide other drawing helpers.

    As a last resort, if you know OpenGL, you can just use direct OpenGL commands to render. This requires OpenGL knowledge and some extra care — you cannot change some state carelessly (see TUIControl.Render), and you must be careful if you want your code to be portable to OpenGLES (Android,iOS).

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

You can use any 2D engine controls like this, just add them to Window.Controls. See CastleControls unit for some standard buttons and panels and images. But for a specific game you will probably want a specialized UI, done like the example above.