This is another important “quality of life” improvement for developers:
You no longer need to explicitly initialize the components you want to access using DesignedComponent
method. You just need to move your fields to the published
section to make them initialized automatically.
Detailed explanation of the difference
All (or most) of the calls like below can be now removed:
LabelFps := DesignedComponent('LabelFps') as TCastleLabel; |
Previously, we advised to organize your state like this:
type TStateMain = class(TUIState) private { Components designed using CGE editor, loaded from gamestatemain.castle-user-interface. } LabelFps: TCastleLabel; public constructor Create(AOwner: TComponent); override; procedure Start; override; end; constructor TStateMain.Create(AOwner: TComponent); begin inherited; DesignUrl := 'castle-data:/gamestatemain.castle-user-interface'; end; procedure TStateMain.Start; begin inherited; { Find components, by name, that we need to access from code } LabelFps := DesignedComponent('LabelFps') as TCastleLabel; end; |
Now, we advise a simpler approach:
type TStateMain = class(TUIState) published { Components designed using CGE editor. These fields will be automatically initialized at Start. } LabelFps: TCastleLabel; public constructor Create(AOwner: TComponent); override; end; constructor TStateMain.Create(AOwner: TComponent); begin inherited; DesignUrl := 'castle-data:/gamestatemain.castle-user-interface'; end; |
Of course you may still find it useful to define Start
method, to initialize various things about your state. But there’s no need for it if it was only doing DesignedComponent
calls.
What’s going on under the hood
The published fields of the TUIState
descendants are now automatically initialized when the design is loaded. Right before Start
, the published fields (that have names matching some object in the design) are now automatically set to the corresponding objects. At Stop
(when design is unloaded) these fields are set to nil.
As a bonus (advantage over previous solution) this avoids having dangling references after Stop
. While previously you could set all your references to nil
manually in Stop
method… likely nobody did it, as it was a tiresome and usually pointless job. Now they are nil
after Stop automatically, so accessing them will result in clearer errors (and can be safeguarded by X <> nil
reliably).
I should also mention one disadvantage from the previous approach: if you make a typo in component name, e.g. declare BabelFps
instead of LabelFps
, then the mistakenly-named field will just remain uninitialized. Nothing will make an automatic exception like “BabelFps not initialized!”. Of course any code doing BabelFps.Caption := 'aaa';
will crash and the debugger should clearly show that BabelFps
is nil
. And you can write something like Assert(BabelFps <> nil);
in Start
to get explicit exception in case of mistake.
This is very consistent with how Delphi VCL and Lazarus LCL initialize their form fields.
Our conventions — where to put the published section?
I admit I had a little discussion with myself about “where to put the published
section”?
Following our usual conventions for writing components, the published
section should go as last. We usually write private
, then public
, then published
. So I wanted to have section in the increasing order of “being exposed”:
type TStateMain = class(TUIState) // MOST INTERNAL private MyInternalStuff: Integer; // EXPOSED TO OUTSIDE WORLD public constructor Create(AOwner: TComponent); override; // EXPOSED TO OUTSIDE WORLD ALSO THROUGH RTTI published { Components designed using CGE editor. These fields will be automatically initialized at Start. } LabelFps: TCastleLabel; end; |
But eventually I came to the conclusion that it is a bit unnatural in this case. Basically, Delphi VCL and Lazarus LCL are right to put it at the beginning. Because in the usual case, you don’t think about this published section as “the most exposed identifiers for outside code”. You think about it as “internal components I need to access to implement my design”.
And it’s kind of an “unfortunate but sensible limitation” that it means that these things have to be also exposed to everything from the outside. This fact makes sense if you realize that the automatic initialization requires RTTI (RunTime Type Information, known also as reflection in various other languages). Things that RTTI has access to are naturally available to the outside world, through RTTI. So it would not be consistent for compiler to “hide” the fields in the published
section while still making these identifiers available through RTTI.
Yet, when creating Delphi VCL form, or Lazarus LCL form, or CGE state, you usually don’t really want to “expose” these fields to the outside world. You want to access them right within your form / state. Thus, having them at the beginning of the state makes sense. And is consistent with Delphi VCL / Lazarus LCL as a bonus.
Still I decided to explicitly spell the published
section name everywhere. This allows me to easily say in documentation “put your field in the published
section”. I don’t need to say “initial section” or “automatic section” and users don’t need to understand how it works and how {$M+}
in Pascal works and whether TUIState
was compiled with {$M+}
. So, in the end, I propose to write:
type TStateMain = class(TUIState) published { Components designed using CGE editor. These fields will be automatically initialized at Start. } LabelFps: TCastleLabel; private MyInternalStuff: Integer; public constructor Create(AOwner: TComponent); override; end; |
Have fun with this! I have already updated our templates, our manual like here, and some (but not all!) examples to follow the new convention.
I gave it a first try by outcommenting the first TCastleImageTransform in TStatePlay.Start but got errors because the transform cannot be found?
Relevant TStatePlay is:
@Carring In the
published
section you show I don’t see a declaration ofWestBeachTransform
.See the post description – only the fields in the
published
section are initialized. If you remove the linethen you need to make sure the field
WestBeachTransform
is inpublished
section.I still get the error?
Maybe it’s some unrelated issue? What you show looks good.
WestbeachTransform
will be initialized, if your design (castle-data:/gamestateplay.castle-user-interface
) will contain a component with the same name.You can verify it by doing things like
in Start method.
If you cannot solve it, then maybe you have some unrelated problem. As usual, please submit a full testcase to reproduce the problem.
Now the WritelnLog identifier cannot be found.
Though ‘Classes’ is in my uses list.