unit MyButtonUnit;
interface
uses CastleControls;
type
TMyButton = class(TCastleButton)
end;
implementation
uses CastleComponentSerialize;
initialization
RegisterSerializableComponent(TMyButton, 'My Button');
end.
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.
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
:
Descend from TCastleUserInterface
to define new user interface controls.
Descend from TCastleTransform
to define new things you can put in your 3D and 2D game world.
Descend from TCastleBehavior
to define new things that enhance the behavior of TCastleTransform
.
Descend from TCastleComponent
otherwise.
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.
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.
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;
You can also place in the published
section a property that references another class. There are two common cases:
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.
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 FMyLinkedImage;
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.
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;
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
case PropertyName of
'URL', 'Color', 'CenterPersistent': // list here new basic properties
Result := [psBasic];
else
Result := inherited PropertySections(PropertyName);
end;
end;
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.
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.
Warning
|
This section describes new feature on new-cameras branch, not yet merged with CGE master branch. |
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
TSerializationProcessVectorsHelper.ReadWriteVector
(this is a helper method — if you use the CastleVectors
unit, you can use it as if it was a method of TSerializationProcess
class)
TSerializationProcessColorsHelper.ReadWriteColor
(this is a helper method — if you use the CastleColors
unit, you can use it as if it was a method of TSerializationProcess
class)
There are 2 use-cases:
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;
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;
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.
Copyright Michalis Kamburelis and Castle Game Engine Contributors.
This webpage is also open-source and we welcome pull requests to improve it.
We use cookies for analytics. See our privacy policy.