![]() |
![]() |
![]() |
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 and inventory. In the chapter about "Player" we shown how to easily use the standard TPlayer class to automatically store and update your player's life and inventory (see TPlayer and ancestors properties, like TAliveWithInventory.Inventory and TAlive.Life). But this information is not displayed automatically in any way, since various games have wildly different needs. Let's draw the information about player ourselves.
There are two places where you can draw:
You can just place the appropriate drawing code in
OnRender
event
(see TCastleWindowBase.OnRender,
TCastleControlBase.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.
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, CastleColors, CastleVectors, CastleWindow, CastleUIControls, CastleControls, CastlePlayer, CastleRectangles, CastleGLUtils; var Window: TCastleWindow; Player: TPlayer; type TMyPlayerHUD = class(TCastleUserInterface) public procedure Render; override; end; procedure TMyPlayerHUD.Render; begin inherited; UIFont.Print(20, 20, Yellow, Format('Player life: %f / %f', [Player.Life, Player.MaxLife])); end; var PlayerHUD: TMyPlayerHUD; begin Window := TCastleWindow.Create(Application); Window.Open; Player := TPlayer.Create(Window.SceneManager); Player.Life := 75; // just to make things interesting Window.SceneManager.Items.Add(Player); Window.SceneManager.Player := Player; { When starting your game, create TMyPlayerHUD instance and add it to Window.Controls } PlayerHUD := TMyPlayerHUD.Create(Application); Window.Controls.InsertFront(PlayerHUD); Application.Run; end.
Inside TMyPlayerHUD.Render
you can draw 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]));
You can also create your own instances of TCastleFont to have more fonts. See the manual chapter about "Text and fonts" for more.
Note: Drawing a text this way means that you manually do something similar to the TCastleLabel control.
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 TMyPlayerHUD.Render; var R: TRectangle; begin inherited; R := Rectangle(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 := Round(R.Width * Player.Life / Player.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', [Player.Life, Player.MaxLife])); end;
Note: Drawing a rectangle this way means that you manually do something similar to the TCastleRectangleControl control.
To draw a circle use the DrawCircle. There are also procedures to draw only an outline: DrawRectangleOutline DrawCircleOutline.
Note: Drawing shapes this way means that you manually do something similar to the TCastleShape control.
To draw an arbitrary 2D primitive use the DrawPrimitive2D method. Blending is automatically used if you pass color with alpha < 1.
To draw an image, use the TDrawableImage class. It has methods Draw and 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 TMyPlayerHUD = class(TCastleUserInterface) private FMyImage: TDrawableImage; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure Render; override; end; constructor TMyPlayerHUD.Create(AOwner: TComponent); begin inherited; FMyImage := TDrawableImage.Create('castle-data:/face.png'); end; destructor TMyPlayerHUD.Destroy; begin FreeAndNil(FMyImage); inherited; end; procedure TMyPlayerHUD.Render; begin inherited; // ... previous TMyPlayerHUD.Render contents ... FMyImage.Draw(420, 10); end;
Note: Drawing images this way means that you manually do something similar to the TCastleImageControl control.
Here's a complete source code that shows the above features. You can download and compile it right now!
uses SysUtils, CastleColors, CastleVectors, CastleWindow, CastleUIControls, CastleControls, CastlePlayer, CastleRectangles, CastleGLUtils, Classes, CastleFilesUtils, CastleGLImages; var Window: TCastleWindow; Player: TPlayer; type TMyPlayerHUD = class(TCastleUserInterface) private FMyImage: TDrawableImage; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure Render; override; end; constructor TMyPlayerHUD.Create(AOwner: TComponent); begin inherited; FMyImage := TDrawableImage.Create('castle-data:/face.png'); end; destructor TMyPlayerHUD.Destroy; begin FreeAndNil(FMyImage); inherited; end; procedure TMyPlayerHUD.Render; var R: TRectangle; begin inherited; R := Rectangle(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 := Round(R.Width * Player.Life / Player.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', [Player.Life, Player.MaxLife])); FMyImage.Draw(420, 10); end; var PlayerHUD: TMyPlayerHUD; begin Window := TCastleWindow.Create(Application); Window.Open; Player := TPlayer.Create(Window.SceneManager); Player.Life := 75; // just to make things interesting Window.SceneManager.Items.Add(Player); Window.SceneManager.Player := Player; { When starting your game, create TMyPlayerHUD instance and add it to Window.Controls } PlayerHUD := TMyPlayerHUD.Create(Application); Window.Controls.InsertFront(PlayerHUD); Application.Run; end.
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) or TSprite (show image sequence from a sprite sheet — one large image containing many animation frames).
See e.g. our game "Muuu" for a demo of using sprite animations.
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.
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).
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 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.
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 ContainerWidth x ContainerHeight or (as a rectangle) as ContainerRect. The container size is also available as container properties, like TCastleWindow.Width x TCastleWindow.Height or (as a rectangle) TCastleWindow.Rect.
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;
See examples/fps_game
for a working and fully-documented
demo of such TMyPlayerHUD
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.
Copyright Michalis Kamburelis and other Castle Game Engine developers. We use cookies
for analytics.
Like every other frickin' website on the Internet.
See our privacy policy.
Thank you to Paweł Wojciechowicz from Cat-astrophe Games for various graphics.
This documentation is also open-source and you can even redistribute it on open-source terms.