Custom Components

1. Introduction

You can enhance the functionality of Castle Game Engine by implementing new classes. These classes can descend from anything you want, in particular from existing engine classes.

Moreover, any class that descends from TComponent can be registered such that it is available within Castle Game Engine Editor and you can visually create and modify instances of this class. This is a powerful mechanism to invent pretty much anything you want (generally useful or tailored to your specific project) and use it just like a regular engine class.

2. Base classes

All custom components have to descend from TComponent, a standard class in Pascal. Usually, you want to descend from something more specific than a general TComponent:

3. Example of custom component and its registration

Once you define your custom component class, register it. To do this, add to the initialization section of some unit (usually, the same unit where you define the component class) a call to RegisterSerializableComponent.

An example:

unit MyButtonUnit;

interface

uses CastleControls;

type
  TMyButton = class(TCastleButton)
  end;

implementation

uses CastleComponentSerialize;

initialization
  RegisterSerializableComponent(TMyButton, 'My Button');
end.

See advanced_editor/custom_component for a larger, complete example that defines and registers TImageGrid component in the GameControls unit.

4. Running editor with custom components

Inside your CastleEngineManifest.xml file, set the attribute editor_units to list all the units that call the RegisterSerializableComponent. It is a comma-separated list, like editor_units="MyButtonUnit, MyMenuUnit".

Then just open the project and the editor will automatically ask whether to build a custom editor, with custom components included. You can also explicitly rebuild the editor using the menu item "Project → Restart Editor (With Custom Components)" in the editor. Make sure that the Lazarus location is correct in the editor "Preferences" window.

The custom component will be automatically available in the proper editor menu to add a new component. For example, if your component descends from the TCastleUserInterface, then it will appear in the menu Edit → Add User Interface and when you right-click any existing UI item and choose Add User Interface submenu.

5. Publishing properties

The properties in the published section of your class are displayed by the editor in the Object Inspector (sidebar on the right) and are automatically read and written (serialized and deserialized) to design files.

So you can just place in the published section properties of simple types (like Integer, Single (float), String) and they will work. Like this:

type
  TMyButton = class(TCastleButton)
  private
    FMyInformation: String;
  published
    property MyInformation: String read FMyInformation write FMyInformation;
  end;

5.1. Default value (for simple property types, like Integer)

Every property can have a default specifier. This should specify the initial property value, that is always set by the constructor. When serializing (writing to file) the given component, we do not write the property value when it is equal to this "default" value — because we know it is not necessary.

Note
Remember that you still have to initialize in constructor the property to a given value. Merely adding default does not actually initialize the property. It it only an information for serialization.

For example, the Rows and Columns below have a default values of (respectively) 10 and 20. If you don’t change their values, then they will not be written to serialized JSON file.

type
  TMyTable = class(TCastleUserInterface)
  strict private
    FRows, FColumns: Integer;
  public
    const
      DefaultRows = 10;
      DefaultColumns = 20;
    constructor Create(AOwner: TComponent); override;
  published
    property Rows: Integer read FRows write FRows default DefaultRows;
    property Columns: Integer read FColumns write FColumns default DefaultColumns;
  end;

constructor TMyTable.Create(AOwner: TComponent);
begin
  inherited;
  FRows := DefaultRows;
  FColumns := DefaultColumns;
end;

The above class also defined constants DefaultRows and DefaultColumns. While it is not required to define such constants, we advise doing it for all properties that have a non-obvious default value. This allows to easily refer to the default value from the code.

An alternative way to specify when should the property be serialized is to use a stored method.

In general, a property without the default specifier or stored method is always serialized to file. This is true for properties of e.g. Integer type. But beware: Some specific property types, like floats and Strings, have "implicit" default value. See below.

5.2. Float (Single, Double, Extended) properties

Properties with the floating-point type (Single, Double, Extended) behave a bit differently when it comes to their default value.

  • Double and Extended (note that Extended is often an alias to Double) cannot have default clause at all.

  • Single type can have a default value in FPC, but not Delphi. As our CGE editor right now is compiled only by FPC/Lazarus, and the editor is where proper default really matters (because editor serializes the designs), this is often acceptable for CGE code. Just put use {$ifdef FPC} around it, like:

type
  TMyBehavior = class(TCastleBehavior)
  strict private
    FAngle: Single;
  public
    const
      DefaultAngle = Pi/2;
    constructor Create(AOwner: TComponent); override;
  published
    property Angle: Single read FAngle write FAngle {$ifdef FPC}default DefaultAngle{$endif};
  end;

constructor TMyBehavior.Create(AOwner: TComponent);
begin
  inherited;
  FAngle := DefaultAngle;
end;

If you don’t specify a default value for Single property, then it behaves as if it has default value 0.0. Use the nodefault specifier if you really want to save the Single property always.

When the above capabities of default specifier are not good enough for you, just use the stored method. This is useful e.g. if:

  • You want to have proper serialization of Single property from Delphi. So that it is not saved to JSON from Delphi, when it has the default value you specified by default XXX for FPC.

  • You want to have proper serialization of Double or Extended (from Delphi or FPC).

  • You want to avoid serializing when the field value is different than default by some epsilon.

For example:

type
  TMyPerspective = class(TCastleComponent)
  strict private
    FFieldOfView: Single;
  public
    const
      DefaultFieldOfView = Pi / 4;
    constructor Create(AOwner: TComponent); override;
  published
    property FieldOfView: Single read FFieldOfView write SetFieldOfView
      stored IsStoredFieldOfView;
  end;

constructor TMyPerspective.Create(AOwner: TComponent);
begin
  inherited;
  FFieldOfView := DefaultFieldOfView;
end;

function TMyPerspective.IsStoredFieldOfView: Boolean;
begin
  Result := not SameValue(FFieldOfView, DefaultFieldOfView);
end;

5.3. Publishing class properties

You can also place in the published section a property that references another class. There are two common cases:

5.3.1. Subcomponents

The instance a subcomponent should be always created by the class that contains it, and the class that contains it should be the owner. The serialization/deserialization will take care of storing the properties of a subcomponent, and can assume that the instance of the subcomponent is always available. To indicate this case, use SetSubComponent(true) and make the property read-only. Like this:

unit MyButtonUnit;

interface

uses Classes,
  CastleControls;

type
  TMyButton = class(TCastleButton)
  private
    FMySubComponent: TCastleImageControl;
  public
    constructor Create(AOwner: TComponent); override;
    property MySubComponent: TCastleImageControl read FMySubComponent;
  end;

implementation

uses CastleComponentSerialize;

constructor TMyButton.Create(AOwner: TComponent);
begin
  inherited;
  FMySubComponent := TCastleImageControl.Create(Self);
  FMySubComponent.SetSubComponent(true);
  InsertFront(FMySubComponent);
end;

initialization
  RegisterSerializableComponent(TMyButton, 'My Button');
end.

5.3.2. Editable reference to another instance

A reference to any other class that can be assigned. Another common situation is if you want to expose a property that references another class, and allow to assign any instance (or nil) to this class. Like this:

unit MyButtonUnit;

interface

uses CastleControls;

type
  TMyButton = class(TCastleButton)
  private
    FMyLinkedImage: TCastleImageControl;
  public
    property MyLinkedImage: TCastleImageControl read FMyLinkedImage write FMyLinkedImage;
  end;

implementation

uses CastleComponentSerialize;

initialization
  RegisterSerializableComponent(TMyButton, 'My Button');
end.
Warning
The example above will compile and basically work, but it has an important problem. Read on for how to solve it.

The instance assigned to such property (MyLinkedImage in the above example) can be freed at any moment (at design-time or at run-time). Your component must be prepared to handle this.

The standard Pascal mechanism for this is to watch when the referenced component is freed using the FreeNotification mechanism. See the Free notification description in our "Modern Object Pascal Introduction for Programmers" book.

In Castle Game Engine we encourage to use TFreeNotificationObserver for such purpose, which is generally simpler than using FreeNotification mechanism. This is the corrected example code using it:

unit MyButtonUnit;

interface

uses Classes,
  CastleClassUtils, CastleControls;

type
  TMyButton = class(TCastleButton)
  private
    FMyLinkedImageObserver: TFreeNotificationObserver;
    FMyLinkedImage: TCastleImageControl;
    procedure SetMyLinkedImage(const Value: TCastleImageControl);
    procedure MyLinkedImageFreeNotification(const Sender: TFreeNotificationObserver);
  public
    constructor Create(AOwner: TComponent); override;
    property MyLinkedImage: TCastleImageControl read FMyLinkedImage write SetMyLinkedImage;
  end;

implementation

uses CastleComponentSerialize;

constructor TMyButton.Create(AOwner: TComponent);
begin
  inherited;
  FMyLinkedImageObserver := TFreeNotificationObserver.Create(Self);
  FMyLinkedImageObserver.OnFreeNotification := {$ifdef FPC}@{$endif} MyLinkedImageFreeNotification;
end;

procedure TMyButton.SetMyLinkedImage(const Value: TCastleImageControl);
begin
  if FMyLinkedImage <> Value then
  begin
    FMyLinkedImage := Value;
    FMyLinkedImageObserver.Observed := Value;
  end;
end;

procedure TMyButton.MyLinkedImageFreeNotification(const Sender: TFreeNotificationObserver);
begin
  // set property to nil when the referenced component is freed
  MyLinkedImage := nil;
end;

initialization
  RegisterSerializableComponent(TMyButton, 'My Button');
end.

5.4. Publishing vectors and colors

For now, in FPC you cannot publish a property of record type. This affects our TVectorXxx and TCastleColorXxx records.

To expose them for the editor, you need to wrap them in a class like TCastleVector3Persistent (that wraps TVector3). To do this, add a code like this:

type
  TMyComponent = class(TComponent)
  strict private
    FCenterPersistent: TCastleVector3Persistent;
    function GetCenterForPersistent: TVector3;
    procedure SetCenterForPersistent(const AValue: TVector3);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    { Center of my component, by default (1, 2, 3). }
    property Center: TVector3 read FCenter write FCenter;
  published
    { @link(Center) that can be visually edited in
      Castle Game Engine Editor, Lazarus and Delphi.
      Normal user code does not need to deal with this,
      instead read or write @link(Center) directly.
      @seealso Center }
    property CenterPersistent: TCastleVector3Persistent read FCenterPersistent;
  end;

constructor TMyComponent.Create(AOwner: TComponent);
begin
  inherited;

  FCenter := Vector3(1, 2, 3); // default value of Center

  FCenterPersistent := TCastleVector3Persistent.Create(nil);
  FCenterPersistent.SetSubComponent(true);
  FCenterPersistent.InternalGetValue := {$ifdef FPC}@{$endif} GetCenterForPersistent;
  FCenterPersistent.InternalSetValue := {$ifdef FPC}@{$endif} SetCenterForPersistent;
  FCenterPersistent.InternalDefaultValue := Center; // current value is default
end;

destructor TMyComponent.Destroy;
begin
  FreeAndNil(FCenterPersistent);
  inherited;
end;

function TMyComponent.GetCenterForPersistent: TVector3;
begin
  Result := Center;
end;

procedure TMyComponent.SetCenterForPersistent(const AValue: TVector3);
begin
  Center := AValue;
end;

5.5. Property sections

For the best presentation, override PropertySections. You will usually place the newly added properties in the Basic section, to make them easy to discover by the users. A sample implementation of PropertySections would be like this:

function TMyComponent.PropertySections(const PropertyName: String): TPropertySections;
begin
  if ArrayContainsString(PropertyName, ['URL', 'Color', 'CenterPersistent']) then // list here new basic properties
    Result := [psBasic]
  else
    Result := inherited PropertySections(PropertyName);
end;
Note
If your component only needs to work with FPC (and not Delphi) then you can also use case with a String value to implement this method nicely.

5.6. Property editors

You can also register property editors. These are classes that descend from existing Pascal classes in PropEdits unit (see Lazarus PropEdits unit code) and can customize how the user can edit given property.

Creating property editors for CGE follows the same rules as defining property editors for Lazarus and Delphi. See e.g. Lazarus documentation about creating custom components. You can also take a look at CGE CastlePropEdits unit for an inspiration what can be done.

As the registration of property editors requires using LCL units, register them only when unit is compiled with symbol CASTLE_DESIGN_MODE. Like this:

unit MyUnit;

interface

...

implementation

uses SysUtils,
  CastleComponentSerialize,
  { Use CastlePropEdits, and thus LCL and castle_components, only when part of the editor. }
  {$ifdef CASTLE_DESIGN_MODE} , PropEdits, CastlePropEdits {$endif};

...

initialization
  RegisterSerializableComponent(TMyComponent, 'My Component');
  {$ifdef CASTLE_DESIGN_MODE}
  RegisterPropertyEditor(TypeInfo(AnsiString), TMyComponent, 'URL', TImageURLPropertyEditor);
  {$endif}
end.

6. Transient instances: avoid displaying and serializing internal classes

In general, your custom component can freely create other classes for internal purposes, and generally do anything it wants with them.

But note that if you add new children of TCastleUserInterface, or new children of TCastleTransform, or new behaviors of TCastleTransform, then these new children will be (by default) visible to the user (in CGE editor) and editable (user will be able to modify their properties, or even free their instances). Sometimes this is undesirable, when you want to create something internal (but it still has to be a child of TCastleUserInterface, TCastleTransform etc.).

The solution to this is to call TCastleComponent.SetTransient on an internal instance. "Transient" is a Pascal term for components that are internal, that should not be accessible at design-time, and should not be serialized/deserialized.

7. (De)Serializing other values (not just published properties)

Sometimes you want to read/write to the design file a value that should not be a published property. Maybe it is a private field, that you don’t want to show user in the Object Inspector, maybe it should be just a temporary local variable. To achieve this, override in your class TCastleComponent.CustomSerialization, and within the overridden implementation call TSerializationProcess methods, like

There are 2 use-cases:

  1. A private piece of information, like design-time only information.

    For example, viewport serializes the current settings of camera and navigation at design-time. We don’t want to show these internal camera/navigation properties in Object Inspector, because at run-time, the design-time settings are ignored (so they are not an API we expose for engine users). Still, we want to preserve them in the design file, so that when you open the design file — the camera and navigation are just as they were previously.

    Usage example looks like this:

    type
      TMyComponent = class(TComponent)
      strict private
        const
          DefaultMyPrivateVector: TVector4 = (X: 1; Y: 1; Z: 1; W: 1);
          DefaultMyPrivateString = 'Something something';
        var
          FMyPrivateVector: TVector4;
          FMyPrivateString: String;
      public
        constructor Create(AOwner: TComponent); override;
        procedure CustomSerialization(const SerializationProcess: TSerializationProcess); override;
      end;
    
    constructor TMyComponent.Create(AOwner: TComponent);
    begin
      inherited;
      { Later ReadWriteVector assumes that constructor sets DefaultMyPrivateVector }
      FMyPrivateVector := DefaultMyPrivateVector;
      FMyPrivateString := DefaultMyPrivateString;
    end;
    
    procedure TMyComponent.CustomSerialization(const SerializationProcess: TSerializationProcess);
    begin
      inherited;
      SerializationProcess.ReadWriteVector('MyPrivateVector', FMyPrivateVector,
        DefaultMyPrivateVector, true);
      SerializationProcess.ReadWriteString('MyPrivateString', FMyPrivateString,
        FMyPrivateString <> DefaultMyPrivateVector);
    end;
  2. Reading a piece of information from design file for backward compatibility, to immediately convert it to something else.

    For example, this is useful to read from design-file an old property, that has been removed from the API, but you still want to support reading the old design files.

    If you can reliably convert the old information into new information, then you can even pass IsStored parameter to ReadWriteXxx as false, as you don’t need to store the deprecated information back into file.

    Usage example looks like this:

    type
      TMyComponent = class(TComponent)
      strict private
      public
        { Note: For simplicity sake, the My4DVector and MyString are just public,
          not published, in this example code. But you could make them published,
          following instructions from earlier sections. }
        My4DVector: TVector4;
        MyString: String;
        procedure CustomSerialization(const SerializationProcess: TSerializationProcess); override;
      end;
    
    procedure TMyComponent.CustomSerialization(const SerializationProcess: TSerializationProcess);
    const
      DefaultMyOldVector: TVector3 = (X: 0; Y: 0; Z: 0);
      DefaultMyOldInteger = 123;
    var
      MyOldVector: TVector3;
    begin
      inherited;
    
      { For example sake, let's assume that in the past, you published
        MyOldVector: TVector3 value.
        But now you want to expose My4DVector: TVector4,
        and when reading old designs -- convert MyOldVector -> My4DVector somehow. }
    
      MyOldVector := DefaultMyOldVector;
      SerializationProcess.ReadWriteVector('MyOldVector', MyOldVector,
        DefaultMyOldVector, false);
    
      if not TVector3.PerfectlyEquals(MyOldVector, DefaultMyOldVector) then
        My4DVector := Vector4(MyOldVector, 1.0);
    
      { For example sake, let's assume that in the past, you published
        MyOldInteger: Integer value.
        But now you want to expose MyString: String
        and when reading old designs -- convert MyOldInteger -> MyString somehow. }
    
      MyOldInteger := DefaultMyOldInteger;
      SerializationProcess.ReadWriteString('MyOldInteger', MyOldInteger, false);
    
      if MyOldInteger <> DefaultMyOldInteger then
        MyString := IntToStr(MyOldInteger);
    end;

8. Add "verbs" to the context menu for given component

Custom component with "Reload URL" verb

You can define a component editor with custom "verbs" to add special operations available on the given component.

To do this, define a class descending from TComponentEditor and override a few methods dealing with "verbs".

type
  { Editor for TMyComponent. }
  TMyComponentEditor = class(TComponentEditor)
  public
    function GetVerbCount: Integer; override;
    function GetVerb(Index: Integer): string; override;
    procedure ExecuteVerb(Index: Integer); override;
  end;

function TMyComponentEditor.GetVerbCount: Integer;
begin
  Result := (inherited GetVerbCount) + 1;
end;

function TMyComponentEditor.GetVerb(Index: Integer): string;
var
  InheritedCount: Integer;
begin
  InheritedCount := inherited GetVerbCount;
  if Index < InheritedCount then
    Result := inherited GetVerb(Index)
  else
  if Index = InheritedCount then
  begin
    Result := 'TODO';
  end else
    Result := '';
end;

procedure TMyComponentEditor.ExecuteVerb(Index: Integer);
var
  InheritedCount: Integer;
begin
  InheritedCount := inherited GetVerbCount;
  if Index < InheritedCount then
    inherited ExecuteVerb(Index)
  else
  if Index = InheritedCount then
  begin
    // TODO
    // GetDesigner.Modified; // call this to mark design as modified and add "Undo" step
  end else
    WritelnWarning('TMyComponentEditor.ExecuteVerb invalid verb index: %d', [Index]);
end;

Next, register this class:

initialization
  RegisterSerializableComponent(TMyComponent, 'My Component');
  {$ifdef CASTLE_DESIGN_MODE}
  RegisterComponentEditor(TMyComponent, TMyComponentEditor);
  {$endif}
end.

See advanced_editor/custom_component for a larger, complete example that defines and registers TImageGrid component with a component editor to perform "Reload URL".

9. Children components

Often it is best to implement your new component as a composition of existing components.

For example: TCastleCheckbox is (internally) just an image (TCastleImageControl) and a label (TCastleLabel).

There are a few ways to define a child component, depending on whether you want to expose the child component to the developer using your component.

9.1. Internal children components (using SetTransient)

This is good if you want to use children components in the implementation of your new component, but you don’t want a regular user to actually see these children e.g. in the editor hierarchy.

Advantages:

  • Your new component looks simple to the user. The fact that e.g. TCastleCheckbox is composed from TCastleImageControl and TCastleLabel is really just an implementation detail.

  • You can depend that user will not free your internal components. In particular, when working in the editor, the user cannot free these internal components in any way, because the user will not be able to see or select them.

To do this:

  • Create the internal component in your component constructor, as owner using your component. Like FInternal := TMyInternalComponent.Create(Self); .

  • Immediately after creation call TCastleComponent.SetTransient, like FInternal.SetTransient;.

  • Don’t worry about the FInternal.Name. You can set it to something non-empty (and it only must be unique within the children of this component) but there’s no need to.

Examples:

9.1.1. Example code

type
  { A label that shows an Integer number. }
  TMyIntegerLabel = class(TCastleUserInterface)
  strict private
    FInternalLabel: TCastleLabel;
    FNumber: Integer;
    procedure SetNumber(const Value: Integer);
  published
    property Number: Integer read FNumber write SetNumber;
  end;

constructor TMyIntegerLabel.Create(AOwner: TComponent);
begin
  inherited;
  FInternalLabel := TCastleLabel.Create(Self);
  FInternalLabel.SetTransient;
  FInternalLabel.Caption := IntToStr(FNumber); // set to '0'
  InsertFront(FInternalLabel);
end;

procedure TMyIntegerLabel.SetNumber(const Value: Integer);
begin
  if FNumber <> Value then
  begin
    FNumber := Value;
    FInternalLabel.Caption := IntToStr(FNumber);
  end;
end;

initialization
  RegisterSerializableComponent(TMyIntegerLabel, 'My Integer Label');
end.
Warning

The components made internal using SetTransient are not absolutely "out of reach" for the developer of an application. Any developer can still access them by iterating through the children list (e.g. TCastleUserInterface or TCastleTransform children). Components can be accessed, modified and even freed this way. Like:

MyIntegerLabel := TMyIntegerLabel.Create(...);
(MyIntegerLabel.Controls[0] as TCastleLabel).Caption := 'foo';
MyIntegerLabel.Controls[0].Free;

That said, it is a convention in Castle Game Engine that developers will not do that, i.e. do not act on components you have just found in the tree and you are not "responsible for them". Thus the author of TMyIntegerLabel can assume that noone will free the FInternalLabel or modify FInternalLabel.Caption or other properties.

9.2. Children components that user can freely edit and remove (using TRegisteredComponent.OnCreate)

A different situation is when you want the new component to propose to user some children components, but the user should see (and be able to freely modify and even free) these children components.

An example of this can be seen each time you create a TCastleViewport in the editor. You can create a viewport using either "Viewport (3D)" or "Viewport (2D)" menu items. They both create a TCastleViewport (not any descendant) class, but they configure some properties on this viewport differently and they propose different children components by default. E.g. "Viewport (3D)" adds to TCastleViewport a background (with sky gradient), a camera, a plane and a light, and positions the camera for nice 3D view.

To make it work:

  • Do not:

    • Do not use SetTransient on the children components. You want the children components to be completely visible to user, and editable in the editor.

    • Do not create these children in the constructor. When deserializing, we always execute the constructor, and we would create new copies of the children — instead of reading the existing children from the design.

  • Define an OnCreate event at the component registration. This will be called by the editor only when user creates the component (and not when the component is just deserialized). Assign to it a class procedure that should act on Sender (parameter) and add to it proper children.

  • Create the children with the same Owner as current component. In the editor, whole design (components editable by user) is owned by one (internal) component. Like FMyChild := TMyChildComponent.Create(Owner); .

  • The children component should have some non-empty name (to be nice for user) and this name should be unique in the owner. Do this using ProposeComponentName, like FMyChild.Name := ProposeComponentName(TMyChildComponent, Owner); .

9.2.1. Example code

type
  { A group of rectangles (initially 3). }
  TMyRectanglesGroup = class(TCastleUserInterface)
  private
    class procedure CreateInitialChildren(Sender: TObject);
  public
    procedure MakeAllYellow;
  end;

procedure TMyRectanglesGroup.MakeAllYellow;
var
  Ui: TCastleUserInterface;
begin
  { Does not assume how many children we have, and of what type. }
  for Ui in Self do
    if Ui is TCastleRectangleControl then
      TCastleRectangleControl(Ui).Color := Yellow;
end;

class procedure TMyRectanglesGroup.CreateInitialChildren(Sender: TObject);
var
  Child: TCastleRectangleControl;
begin
  Child := TCastleRectangleControl.Create(Owner);
  Child.Name := ProposeComponentName(TCastleRectangleControl, Owner);
  Child.Color := Red;
  Child.Anchor(hpLeft, 0);
  (Sender as TMyRectanglesGroup).InsertFront(Child);

  Child := TCastleRectangleControl.Create(Owner);
  Child.Name := ProposeComponentName(TCastleRectangleControl, Owner);
  Child.Color := Blue;
  Child.Anchor(hpLeft, 100);
  (Sender as TMyRectanglesGroup).InsertFront(Child);

  Child := TCastleRectangleControl.Create(Owner);
  Child.Name := ProposeComponentName(TCastleRectangleControl, Owner);
  Child.Color := Green;
  Child.Anchor(hpLeft, 200);
  (Sender as TMyRectanglesGroup).InsertFront(Child);
end;

var
  R: TRegisteredComponent;
initialization
  R := TRegisteredComponent.Create;
  R.ComponentClass := TMyRectanglesGroup;
  R.Caption := ['Rectangles Group (initially 3)'];
  R.OnCreate := {$ifdef FPC}@{$endif} TMyRectanglesGroup.CreateInitialChildren;
  RegisterSerializableComponent(R);
end.

9.3. Children components on which user can edit properties (using SetSubComponent)

You can use a subcomponent to allow user to customize the component properties, but not free the component.

At the core, this just means that you create a children component in the constructor, and you expose its instance using a read-only published property.

Moreover:

  • Use the SetSubComponent(true) to make the component properly handled at serialization / deserialization. The serialization / deserialization will assume that the subcomponent will be always created by the constructor.

  • Set the Name of every subcomponent to correspond to the property name where it is published. Though it is not strictly required for now.

  • The owner of subcomponent should be Self, i.e. it should be automatically freed along with the containing component (and never earlier).

An example of this is TCastleScene.RenderOptions property. It always contains a TCastleRenderOptions instance that says how to render (e.g. whether to use wireframe) the containing TCastleScene. There is simple "one to one" correspondence between TCastleScene and TCastleRenderOptions instances: every TCastleScene has exactly one (and different) TCastleRenderOptions.

A subcomponent can be also added as a visual child somewhere. For example, TCastleScrollView defines a subcomponent TCastleScrollView.ScrollArea. This scroll area is also added in the constructor as TCastleScrollView visual child, using InsertFront(FScrollArea);.

9.3.1. Example code

type
  { A TCastleTransform that always contains a sphere in the center.
    You can configure sphere radius
    and other properties through the @link(MySphere) subcomponent. }
  TMyTransformationWithSphere = class(TCastleTransform)
  strict private
    FMySphere: TCastleSphere;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property MySphere: TCastleSphere read FMySphere;
  end;

constructor TMyTransformationWithSphere.Create(AOwner: TComponent);
begin
  inherited;

  FMySphere := TCastleSphere.Create(Self);
  FMySphere.SetSubComponent(true);
  FMySphere.Name := 'MySphere';
  Add(FMySphere);
end;

initialization
  RegisterSerializableComponent(TMyTransformationWithSphere, 'My Transformation With Sphere');
end.
Warning

The subcomponent can in theory be freed by the developer of an application. In the above example, while MySphere property is read only, the developer can just do MyTransformationWithSphere.MySphere.Free. And the MyTransformationWithSphere.MySphere will become then a dangling pointer.

Application developer could also remove it from being a visual child, e.g. doing MyTransformationWithSphere.Remove(MyTransformationWithSphere.MySphere) and such change will not be properly serialized (because in constructor MySphere is always added as a visual child).

That said, it is a convention in Castle Game Engine that developers will not do that. You should not act on a component you are not "responsible for". And by default (unless otherwise documented) managing memory of the instance referenced somewhere is the sole responsibility of the containing class. By default, you are only welcome to change the exposed instance’s properties.

Thus the author of TMyTransformationWithSphere can assume that nothing will free the FMySphere instance and that it will remain a visual child of containing TMyTransformationWithSphere instance.

10. Using designs to visually set up default state of your component

You can visually design a component (or a hierarchy of components) in the editor, following the Reusing a design using TCastleDesign, TCastleTransformDesign (our equivalent to Unity prefabs) video. This is, in some cases, an alternative to creating a custom component class (described in this manual page). So you can design a new hierarchy of components, visually, and can later reuse them by placing TCastleDesign or TCastleTransformDesign in your larger design files.

The approach with TCastleDesign or TCastleTransformDesign however suffers from a limitation (for now): the hierarchy you instantiate through TCastleDesign or TCastleTransformDesign is "opaque". You cannot customize the components you instantiate this way. While you can "expand" them (using "Edit (Copy Here) Referenced Design" context menu) but this effectively creates a copy of the design — further changes to the original design will not be reflected.

Note
In the long run, we plan to improve it, by allowing to customize the hierachy instantiated through TCastleDesign or TCastleTransformDesign. See roadmap "Allow to override design properties when instantiated using TCastleDesign, TCastleTransformDesign" section.

What you can do right now to overcome this limitation is to combine these 2 approaches, in a way: visually design a component, place it in a design file, and then create a custom component class that will instantiate this design file.

Here’s a demo how it can work:

type
  { A button with 2 labels.
    Visually designed in my_button_with_2_labels.castle-user-interface design file.
    Editing my_button_with_2_labels.castle-user-interface will dictate
    the defaults of this button look.
    Using the properties of this class, you can further customize it.
  }
  TMyButtonWith2Labels = class(TCastleUserInterface)
  strict private
    FDesign: TCastleDesign;
    FInternalLabel1, FInternalLabel2: TCastleLabel;
    function GetCaption1: String;
    procedure SetCaption1(const Value: String);
    function GetCaption2: String;
    procedure SetCaption2(const Value: String);
  published
    property Caption1: String read GetCaption1 write SetCaption1;
    property Caption2: String read GetCaption2 write SetCaption2;
  end;

constructor TMyButtonWith2Labels.Create(AOwner: TComponent);
begin
  inherited;

  FDesign := TCastleDesign.Create(Self);
  FDesign.Url := 'castle-data:/my_button_with_2_labels.castle-user-interface';
  FDesign.FullSize := true; // make FDesign fill the whole TMyButtonWith2Labels area
  FDesign.SetTransient; // see above for explanation what does SetTransient
  InsertFront(FDesign);

  FInternalLabel1 := FDesign.FindDesignedComponent('InternalLabel1') as TCastleLabel;
  FInternalLabel2 := FDesign.FindDesignedComponent('InternalLabel2') as TCastleLabel;
end;

function TMyButtonWith2Labels.GetCaption1: String;
begin
  Result := FInternalLabel1.Caption;
end;

procedure TMyButtonWith2Labels.SetCaption1(const Value: String);
begin
  FInternalLabel1.Caption := Value;
end;

function TMyButtonWith2Labels.GetCaption2: String;
begin
  Result := FInternalLabel2.Caption;
end;

procedure TMyButtonWith2Labels.SetCaption2(const Value: String);
begin
  FInternalLabel2.Caption := Value;
end;

initialization
  RegisterSerializableComponent(TMyButtonWith2Labels, 'My Button with 2 labels');
end.

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