Custom drawn 2D controls: player HUD
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:
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.
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.)
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.