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. Publishing class properties

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

5.1.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.1.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.2. 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;
  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.3. 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.4. 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".


To improve this documentation just edit the source of this page in AsciiDoctor (simple wiki-like syntax) and create a pull request to Castle Game Engine WWW (cge-www) repository.