4. Quick 2D game (getting to know window events)

Simple game where you move an image on the screen - you will learn how to do it very soon!
The simplest basketball game ever created
2D game with animations done in Spine
Simple "River Ride" clone done in 1-hour gamejam

Warning This tutorial page uses features available only in the unstable engine version on GitHub. Do not read this if you use the stable engine version (downloaded as zip or tar.gz from our pages), or be prepared to make some modifications.

In particular, in the stable engine version, the TGLImage class is a little more difficult to use. It needs to be created / destroyed in OnGLContextOpen / OnGLContextClose. Details are explained here.

Before we dive into 3D, we can take a quick stab at basic stuff you can do with our new context. Let's draw some images and handle basic inputs.

Many engine examples demonstrate this. You can take a look e.g. at our "River Ride" clone done in a 1-hour game-jam ! :)

1. Loading image (TGLImage class)

Create an instance of TGLImage, and load an image there. TGLImage allows to load and display the image on screen.

  1. If you use Lazarus form with TCastleControl: Create and destroy the image in the form's OnCreate and OnDestroy events, like this:

    • Select the form in Lazarus (click on it in the form designer or object inspector — be sure to select the form, not the TCastleControl instance).
    • Then double click on appropriate events to create code for OnCreate and OnDestroy. Put there the following code:
    // Also: add to your uses clause: CastleGLImages, CastleFilesUtils
     
    // Also: add to your form private section a declaration of: "Image: TGLImage"
     
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Image := TGLImage.Create(ApplicationData('my_image.png'));
    end;
     
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      FreeAndNil(Image);
    end;

    If effect, your whole unit code should look like this:

    unit laz_unit1;
    {$mode objfpc}{$H+}
    interface
     
    uses Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs,
      CastleControl, CastleGLImages, CastleFilesUtils;
     
    type
      TForm1 = class(TForm)
        CastleControl1: TCastleControl;
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      private
        Image: TGLImage;
      public
        { public declarations }
      end;
     
    var
      Form1: TForm1;
     
    implementation
    {$R *.lfm}
     
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      Image := TGLImage.Create(ApplicationData('my_image.png'));
    end;
     
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      FreeAndNil(Image);
    end;
     
    end.
  2. If you use TCastleWindow: In the simplest case, just create and destroy the image like this:

    uses SysUtils, CastleWindow, CastleGLImages, CastleFilesUtils;
    var
      Window: TCastleWindow;
      Image: TGLImage;
    begin
      Image := TGLImage.Create(ApplicationData('my_image.png'));
      try
        Window := TCastleWindow.Create(Application);
        Window.Open;
        Application.Run;
      finally FreeAndNil(Image) end;
    end.

2. Drawing image (OnRender event)

Next we want to draw this image. To do this, we want to call TGLImage.Draw method within the OnRender callback of our window.

  1. If you use Lazarus form with TCastleControl: Select the TCastleControl instance, and double click to create code for an event OnRender. Put there the following code:

    // Also: add to your form private section a declaration of: "X, Y: Single;"
     
    procedure TForm1.CastleControl1Render(Sender: TObject);
    begin
      Image.Draw(X, Y);
    end;
  2. If you use TCastleWindow: Change your program like this:

    uses SysUtils, CastleWindow, CastleGLImages, CastleFilesUtils;
    var
      Window: TCastleWindow;
      Image: TGLImage;
      X: Single = 0.0;
      Y: Single = 0.0;
     
    procedure WindowRender(Container: TUIContainer);
    begin
      Image.Draw(X, Y);
    end;
     
    begin
      Image := TGLImage.Create(ApplicationData('my_image.png'));
      try
        Window := TCastleWindow.Create(Application);
        Window.OnRender := @WindowRender;
        Window.Open;
        Application.Run;
      finally FreeAndNil(Image) end;
    end.

As you can guess, we can now move the image by simply changing the X, Y variables. Note that we defined X, Y as floating-point values (Single type), not just integers, because floating-point values are more comfortable to animate (you can easily change them at any speed). When necessary for rendering, they will internally be rounded to whole pixels anyway.

3. Moving image (OnUpdate event)

The event OnUpdate is continously called by the engine. You should use it to update the state of your world as time passes.

Use the Fps.UpdateSecondsPassed to know how much time has passed since the last frame. You should scale all your movement by it, to adjust to any computer speed. For example, to move by 100 pixels per second, we will increase our position by CastleControl1.Fps.UpdateSecondsPassed * 100.0.

  1. If you use Lazarus form with TCastleControl: double click to create an event OnUpdate on TCastleControl, and put there the following code:

    procedure TForm1.CastleControl1Update(Sender: TObject);
    begin
      Y := Y + CastleControl1.Fps.UpdateSecondsPassed * 100.0;
    end;
  2. If you use TCastleWindow: Assign a Window.OnUpdate callback (analogous to Window.OnRender above):

    procedure WindowUpdate(Container: TUIContainer);
    begin
      Y := Y + Container.Fps.UpdateSecondsPassed * 100.0;
    end;
     
    // ... at initialization, right after assigninig Window.OnRender, add:
      Window.OnUpdate := @WindowUpdate;

4. Reacting to user input (OnPress event)

The react to one-time key or mouse press, use the OnPress event. You can also check which keys are pressed inside the OnUpdate event, to update movement constantly. Examples below show both ways.

  1. If you use Lazarus form with TCastleControl: double click to create an event OnPress on TCastleControl. Change the OnPress and OnUpdate like below.

    procedure TForm1.CastleControl1Press(Sender: TObject; const Event: TInputPressRelease);
    begin
      if Event.IsKey(K_Space) then
        Y -= 200.0;
    end;
     
    // new extended OnUpdate handler
    procedure TForm1.CastleControl1Update(Sender: TObject);
    var
      SecondsPassed: Single;
    begin
      SecondsPassed := CastleControl1.Fps.UpdateSecondsPassed;
      Y := Y + SecondsPassed * 100.0;
      if CastleControl1.Pressed[K_Left] then
        X := X - SecondsPassed * 200.0;
      if CastleControl1.Pressed[K_Right] then
        X := X + SecondsPassed * 200.0;
    end;
  2. If you use TCastleWindow: Assign a Window.OnPress callback (analogous to Window.OnRender above). Change the OnPress and OnUpdate like below.

    // Also add to the uses clause unit CastleKeysMouse
     
    procedure WindowPress(Container: TUIContainer; const Event: TInputPressRelease);
    begin
      if Event.IsKey(K_Space) then
        Y -= 200.0;
    end;
     
    // new extended OnUpdate handler
    procedure WindowUpdate(Container: TUIContainer);
    var
      SecondsPassed: Single;
    begin
      SecondsPassed := Container.Fps.UpdateSecondsPassed;
      Y := Y + SecondsPassed * 100.0;
      if Container.Pressed[K_Left] then
        X := X - SecondsPassed * 200.0;
      if Container.Pressed[K_Right] then
        X := X + SecondsPassed * 200.0;
    end;
     
    // ... at initialization, right after assigninig Window.OnRender, do this:
      Window.OnUpdate := @WindowUpdate;
      Window.OnPress := @WindowPress;

5. Further reading

If you want to go more into the direction of 2D games, here are some starting points:

  • See the tutorial about displaying 2D controls - player HUD. It has a nice overview of 2D drawing capabilities.

    It also shows a more flexible way to handle drawing and inputs, by creating new descendants of TUIControl (instead of simply attaching to window callbacks).

  • 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).

  • If you want to use smooth and efficient animations, instead of using a series of images, you can load a 2D model (and animation) from an X3D or Spine or other format supported by our engine. To do this, create a T2DSceneManager, and inside it add T2DScene instance. T2DScene descends from our powerful TCastleScene, you can load a 2D or 3D model there, you can transform it using T3DTransform and do many other fancy stuff with it.

    See the Web3d2015 Castle Game Engine tutorial (the slides are here, and the examples (sample data and code) are here). It's 2nd part shows nicely this.

    See also the example castle_game_engine/examples/2d_dragon_spine_android_game/.

To make inputs user-configurable, you could wrap them in TInputShortcut instance. This will store whether the input is a key press or a mouse click, and you can check and change it at runtime. More information is in tutorial about key / mouse shortcuts. This will allow you to replace the check Event.IsKey(K_Space) with something much more powerful:)