Quick 2D game (basic 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

Before we dive into full-featured "scene managers" and 3D, let's take a quick look at the simple things you can do with our window. Let's draw some images and handle inputs.

For a demo of a game using simplest 2D image drawing, take a look 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:

  • See the manual about drawing your own 2D controls. It has a nice overview of 2D drawing capabilities. You can also use the standard 2D controls with a lot of ready functionality.

    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 want to use smooth and efficient animations, 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.

    Just follow the rest of this manual, knowing that everything applies also to 2D, not just 3D:)

    See also the Web3d2015 Castle Game Engine tutorial: the slides, and the examples (code and sample data). The 2nd part of this tutorial shows the usage of T2DScene.

    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 manual about key / mouse shortcuts. This will allow you to replace the check Event.IsKey(K_Space) with something much more powerful:)