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.
Warning: Drawing 2D things manually, as documented on this page, is possible but not advised. In our experience, manual drawing often results in more work while at the same time the resulting solution is less efficient and has less features (compared to composing your UI from components). The page below lists various ways to draw things manually but also points to higher-level components that achieve the same, and often are easier to use and more powerful. So, instead of this page, better read about higher-level UI controls and use them.
An example that we will consider in this chapter is designing a HUD (heads-up display) that displays some player information, like current life.
Place your own drawing code in an overridden Render
method of TCastleUserInterface
descendants. It is simplest to use your view class (which is also a TCastleUserInterface
descendant) for this, just add a Render
method to your view. But you can also create your own TCastleUserInterface
descendants.
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.
Inside TPlayerHud.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', [ 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.
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.
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: It is more advised (easier, more flexible) to use
TCastleImageControl
control than to draw image like above.
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.
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.
Note: We advise to use instead sprite sheets for animations, as they are more efficient and easier to use.
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), and we advise to use it instead:
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
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
.
So far, we have simply carelessly drawn our contents over the window.
TCastleUserInterface.Translation
).
Nor did we take into account parent control position.TCastleUserInterface
.TCastleUserInterface.Anchor
.TCastleContainer.UIScaling
.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;