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;
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.
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;
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.
See also section below Children components on which user can edit properties (using SetSubComponent).
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.
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;
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.
|
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.
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;
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".
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.
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:
As mentioned above: TCastleCheckbox
is composed from TCastleImageControl
and TCastleLabel
. See src/ui/castlecontrols_checkbox.inc.
TCastleBox
is composed from internal TCastleScene
.
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
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 |
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);
.
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.
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);
.
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 Application developer could also remove it from being a visual child, e.g. doing 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 |
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.