Writing code to modify scenes and transformations

1. Introduction

In this chapter we’ll show how to do some useful operations on viewports and scenes using Pascal code. We assume you have already read the overview of the viewport and scenes and Tutorial: Designing a 3D world (that shows how to set up things using the editor).

By editing Pascal code you can do much more than what is possible by just designing in the Castle Game Engine editor.

  • All the classes that you can create using the Castle Game Engine editor can be also created using code. Creating these classes from code allows to do it at any moment in the application. For example you can spawn a new TCastleScene when user presses a key.

    We call these classes components, as they descend from standard Pascal TComponent class.

  • Likewise, it is possible to adjust all of the classes properties, whenever you want. For example, you can update the Translation to move some object.

  • Moreover, there are many engine features that are only available using Pascal code, as exposing them in the editor is not easy (though we work on making more and more features available in the editor).

2. Example code

Cars, surrounded by a wall build in code

The complete result of this tutorial is in the Castle Game Engine. Just open the project examples/viewport_and_scenes/cars_demo and consult it as necessary.

3. Where to edit code

In simple applications, most of the code modifications will be done in the Pascal file that defines your view.

If you started from the "Empty" template, this view is called Main by default, and the Pascal unit is code/gameviewmain.pas.

If you started from the "3D FPS game" or "2D game" templates, then you have 2 views created by default. The actual game is in view called Play, and the Pascal unit is code/gameviewplay.pas.

You can just double-click on that Pascal file in Castle Game Engine editor. It will open your code editor, like Lazarus or Delphi or Visual Studio Code (see installation for instructions how to configure this).

The Pascal unit defines a class called like TViewMain that descends from TCastleView. It overrides some virtual TCastleView methods, and these are the usual places where you will want to add your own code:

  • TViewMain.Start overrides TCastleView.Start. It is run once when we enter the view. It is a good place to initialize something.

  • TViewMain.Update overrides TCastleView.Update. It is run very often, multiple times per second. It is a good place to do something you have to do constantly. Use this for example to

    • move some object,

    • check whether player is in some state,

    • check whether user is pressing some key to keep doing something, as long as user holds the key down.

  • TViewMain.Press overrides TCastleView.Press. It is run when user presses a key or a mouse button.

See Designing user interface and handling events chapter that also talks about the view code.

4. Creating initial project

If you want to try all the examples on this page yourself, we recommend that you create a simple test project now. Follow a subset of the Tutorial: Designing a 3D world instructions to:

  • Create a new project using the "Empty" template.

  • Add an instance of TCastleViewport on the design. Call it MainViewport.

  • Add the sample 3D models of the car and road: copy them from CGE examples/viewport_and_scenes/cars_demo/data into your project.

  • Add an instance of TCastleScene on the design. Call it RoadScene, and load there castle-data:/road.gltf.

5. Refer to the designed components

Often, your code will need to refer to some components that you have added in the editor. The component Name is used to uniquely get an instance of this component from code. In this example, we want to access MainViewport and RoadScene in Pascal. To do this:

  1. Declare their corresponding fields. Do this in the published section of TViewMain class, near the comment { Components designed using CGE editor…​. }. There should be LabelFps already defined (it is part of the "Empty" template), you will add 2 new lines below it:

    RoadScene: TCastleScene;
    MainViewport: TCastleViewport;

    The end result is that TViewMain class starts like this:

    type
      { Main view, where most of the application logic takes place. }
      TViewMain = class(TCastleView)
      published
        { Components designed using CGE editor.
          These fields will be automatically initialized at Start. }
        LabelFps: TCastleLabel;
        RoadScene: TCastleScene;
        MainViewport: TCastleViewport;
  2. Remember to add the necessary units to your uses clause to have the appropriate identifiers defined. If you follow the reference links, like this one: TCastleScene, then you will see in which unit is each identifier defined. TCastleScene class is part of the CastleScene unit. TCastleViewport is part of the CastleViewport unit.

That’s it. Now anywhere in your view code you can access RoadScene or MainViewport.

Compile and run the application now. Use menu item "Run → Compile And Run (F9)" from the Castle Game Engine editor.

If you are familiar with Lazarus or Delphi or other IDE, you can also compile and run from there. Use "Code → Open Project in Code Editor" to open your code editor. In Lazarus or Delphi you can again press F9 to compile and run (possibly inside a debugger).

6. Change the existence of a scene

Now that we have a reference to RoadScene, let’s make a simple modification: let’s make to road disappear when user presses the key R on the keyboard. And appear again, when user presses R again.

To do this, we will react to pressing the R key and toggle the Exists property of RoadScene.

  1. Find the TViewMain.Press implementation.

  2. In the implementation, add these lines:

    if Event.IsKey(keyR) then
    begin
      RoadScene.Exists := not RoadScene.Exists;
      Exit(true);
    end;
  3. Add the CastleKeysMouse unit to the uses clause.

Testing Event.IsKey(keyR) is the way to test whether user pressed a key that generates letter R.

When user pressed R, we toggle the Boolean value of RoadScene.Exists. So it will change to false if it is true right now, or change to true if it is false right now.

We return using the Exit(true) call to tell the parent user interface control that "pressing this key has been handled". This doesn’t really matter in case of this simple application, as nothing else is interested in handling the input. But it matters for more complicated setups, when multiple controls may be interested in handling the same key.

Compile and test the code. The road should appear / disappear as you press R on the keyboard now.

Note

Another way to show and hide the scene would be to toggle the TCastleTransform.Visible property. Setting this to false makes scene invisible, but it still exists in the world. In particular it still collides.

You can also make a scene non-collidable but still visible. Use TCastleTransform.Collides property for this. This is a useful trick to make non-collidable walls in 3D games, behind which you can hide secrets.

Note

All these properties (TCastleTransform.Exists, TCastleTransform.Visible, TCastleTransform.Collides) can also be adjusted in the editor. Here, we adjust the Exists in code, because we want to react to user pressing a key.

Note

Yet another way to make the scene non-existing would be to remove it from Viewport.Items.

  • Doing Viewport.Items.Remove(RoadScene) has a similar effect as RoadScene.Exists := false

  • Doing Viewport.Items.Add(RoadScene) has a similar effect as RoadScene.Exists := true

7. Creating TCastleScene

You can create TCastleScene instances using code. You can add the created scene to a viewport and adjust it’s properties like Translation.

  1. Declare a new field CarScene: TCastleScene in the private section of TViewMain.

  2. In the TViewMain.Start add code to initialize it, load car model, and add to the viewport:

    CarScene := TCastleScene.Create(FreeAtStop);
    CarScene.Load('castle-data:/car.gltf');
    CarScene.PreciseCollisions := true;
    MainViewport.Items.Add(CarScene);

Doing this in TViewMain.Start isn’t yet very useful. We could have added the car scene using the editor too. But it will be useful later.

8. Playing animation

Car animations
Mousey animations

To play an animation, call the PlayAnimation method from code.

Just add this to the TViewMain.Start to play wheels_turning animation once the model is loaded:

CarScene.PlayAnimation('wheels_turning', true);
Note

You can test what animations are available on your model e.g. by opening it with Castle Model Viewer (activate the panel Animations to test animations).

PlayAnimation is very powerful. In addition to choosing an animation (by name) and whether it should loop, it has an overloaded version that takes TPlayAnimationParameters instance and allows to:

See the example examples/animations/play_animation in engine sources for a demo of PlayAnimation capabilities.

Note

Code can also set the AutoAnimation and AutoAnimationLoop properties to change the animation. But using PlayAnimation is usually more comfortable.

9. Moving scene in each update

To make the car moving, we can update its position in TViewMain.Update. Change it into this:

procedure TViewMain.Update(const SecondsPassed: Single; var HandleInput: Boolean);
var
  T: TVector3;
begin
  inherited;
  { This virtual method is executed every frame.}
  LabelFps.Caption := 'FPS: ' + Container.Fps.ToString;

  T := CarScene.Translation;
  { Thanks to multiplying by SecondsPassed, it is a time-based operation,
    and will always move 40 units / per second along the +Z axis. }
  T := T + Vector3(0, 0, 40) * Container.Fps.SecondsPassed;
  { Wrap the Z position, to move in a loop }
  if T.Z > 70 then
    T.Z := -50;
  CarScene.Translation := T;
end;
Note
A more flexible approach to implement this is to define a behavior class that moves the parent, and attach a different instance of it to every car. Read about behaviors to learn about this powerful pattern.

10. Multiple instances of the same scene

Many car instances

It’s allowed to add the same instance of the TCastleScene many times to your viewport items. This allows to reuse it’s data, which is great for both performance and the memory usage.

Note
It is possible to achieve the optimization described in this section also using TCastleTransformReference class. Such approach is also possible to do in the editor, without writing any code. See the Tutorial: Designing a 3D world for details.

For example, let’s make 20 cars moving along the road. You will need 20 instances of TCastleTransform, but only a single instance of the TCastleScene.

  1. Declare in the private section of TViewMain an array of transformations:

    CarTransforms: array [1..20] of TCastleTransform;
  2. Initialize it in TViewMain.Start like this:

    for I := Low(CarTransforms) to High(CarTransforms) do
    begin
      CarTransforms[I] := TCastleTransform.Create(Application);
      CarTransforms[I].Translation := Vector3(
         (Random(4) - 2) * 6, 0, RandomFloatRange(-70, 50));
      CarTransforms[I].Add(CarScene);
      Viewport.Items.Add(CarTransforms[I]);
    end;

    Above we added a randomization of the initial car position. The RandomFloatRange function is in the CastleUtils unit. There’s really nothing magic about the randomization parameters, I just adjusted them experimentally to look right.

  3. Remove the line

    MainViewport.Items.Add(CarScene);

    All our cars will be now controlled using the CarTransforms array. The CarScene is used 20 times as a child of CarTransforms[…​] items.

  4. Finally, make all our cars moving. Change the TViewMain.Update to do the same thing as previously, but now in a loop, for every instance of CarTransforms list.

    procedure TViewMain.Update(const SecondsPassed: Single; var HandleInput: Boolean);
    
      procedure UpdateCarTransform(const CarTransform: TCastleTransform);
      var
        T: TVector3;
      begin
        T := CarTransform.Translation;
        { Thanks to multiplying by SecondsPassed, it is a time-based operation,
          and will always move 40 units / per second along the +Z axis. }
        T := T + Vector3(0, 0, 40) * Container.Fps.SecondsPassed;
        { Wrap the Z position, to move in a loop }
        if T.Z > 70 then
          T.Z := -50;
        CarTransform.Translation := T;
      end;
    
    var
      I: Integer;
    begin
      inherited;
      { This virtual method is executed every frame.}
      LabelFps.Caption := 'FPS: ' + Container.Fps.ToString;
    
      for I := Low(CarTransforms) to High(CarTransforms) do
        UpdateCarTransform(CarTransforms[I]);
    end;

Note that all 20 cars are in the same state (they display the same animation). This is the limitation of this technique. If you need the scenes to be in a different state, then you will need different TCastleScene instances. You can efficiently create them e.g. using the TCastleScene.Clone method. In practice, it is simplest to reserve this optimization (sharing the same scene multiple times) only for completely static scenes (where you don’t use PlayAnimation).

11. Building a mesh using code

Cars, with additional mesh build in code

The scene property RootNode holds a scene graph of your scene. It is automatically created when you load the model from file, and automatically modified by animations. You can also modify it by code, during the game. This means that you can freely modify the 3D models as often as you like (at initialization, as a reaction to key press, every frame…​) and you can even build from scratch new 3D objects.

Below we show a sample code building a scene with a mesh. Building mesh like this is a bit pointless (because it would be easier to just define such mesh in Blender road.blend and export it to road.gltf) but it should give you lots of ideas how to extend it, to make procedurally-generated world. For example, you could take a curve defined using Curves tool and build a road for cars using this curve as a guide.

Our scene graph is composed from X3D nodes organized in a tree. A number of classes used below, named like TXxxNode, correspond to various X3D nodes. The nodes we use below are:

This is a function using these nodes to create TCastleScene with a mesh:

function CreateAdditionalMesh: TCastleScene;
var
  Coord: TCoordinateNode;
  TexCoord: TTextureCoordinateNode;
  IndexedFaceSet: TIndexedFaceSetNode;
  BaseTexture: TImageTextureNode;
  Material: TPhysicalMaterialNode;
  Appearance: TAppearanceNode;
  Shape: TShapeNode;
  Transform: TTransformNode;
  RootNode: TX3DRootNode;
begin
  Coord := TCoordinateNode.Create;
  Coord.SetPoint([
    Vector3(-15.205387, -66.775894, -0.092525),
    Vector3(9.317978, -66.775894, -0.092525),
    Vector3(-15.205387, -68.674622, -0.092525),
    Vector3(9.317978, -68.674622, -0.092525),
    Vector3(9.317978, -78.330063, 3.456294),
    Vector3(-15.205387, -78.330063, 3.456294),
    Vector3(9.317978, -80.814240, 7.241702),
    Vector3(-15.205387, -80.814240, 7.241702)
  ]);

  TexCoord := TTextureCoordinateNode.Create;
  TexCoord.SetPoint([
    Vector2(0.0001, 0.9964),
    Vector2(1.0000, 0.9964),
    Vector2(1.0000, 0.8541),
    Vector2(0.0001, 0.8541),
    Vector2(0.0001, 0.7118),
    Vector2(1.0000, 0.7118),
    Vector2(1.0000, 0.5695),
    Vector2(0.0001, 0.5695),
    Vector2(0.0001, 0.5695),
    Vector2(1.0000, 0.5695),
    Vector2(1.0000, 0.4272),
    Vector2(0.0001, 0.4272)
  ]);

  IndexedFaceSet := TIndexedFaceSetNode.Create;
  IndexedFaceSet.Coord := Coord;
  IndexedFaceSet.TexCoord := TexCoord;
  IndexedFaceSet.SetTexCoordIndex([0, 1, 2, 3, -1, 4, 5, 6, 7, -1, 8, 9, 10, 11, -1]);
  IndexedFaceSet.SetCoordIndex([0, 1, 3, 2, -1, 2, 3, 4, 5, -1, 5, 4, 6, 7, -1]);
  IndexedFaceSet.Solid := false; // make it visible from both sides

  BaseTexture := TImageTextureNode.Create;
  BaseTexture.SetUrl(['castle-data:/textures/tunnel_road.jpg']);

  Material := TPhysicalMaterialNode.Create;
  Material.BaseTexture := BaseTexture;
  Material.BaseColor := Vector3(1, 1, 0); // yellow

  Appearance := TAppearanceNode.Create;
  Appearance.Material := Material;

  Shape := TShapeNode.Create;
  Shape.Geometry := IndexedFaceSet;
  Shape.Appearance := Appearance;

  Transform := TTransformNode.Create;
  Transform.Translation := Vector3(0, 0, 0);
  Transform.Rotation := Vector4(1, 0, 0, -Pi / 2);
  Transform.AddChildren(Shape);

  RootNode := TX3DRootNode.Create;
  RootNode.AddChildren(Transform);

  Result := TCastleScene.Create(FreeAtStop);
  Result.Load(RootNode, true);
end;

Test it like this:

  1. Add X3DNodes and CastleBoxes units to the uses clause.

  2. Add the CreateAdditionalMesh function as a nested routine to TViewMain.Start.

  3. Call the CreateAdditionalMesh function and add the new scene to the viewport, by adding to TViewMain.Start this:

    MainViewport.Items.Add(CreateBoxesScene);

To improve this documentation just edit this page and create a pull request to cge-www repository.