Published state fields are now automatically initialized, no need in most cases for DesignedComponent calls

Posted on

Van Gogh , Bedroom in Arles , night - by ruslans3d from https://sketchfab.com/3d-models/van-gogh-bedroom-in-arles-night-7c28126099404406bd0f5c150d809394

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.

Notable Replies

  1. I gave it a first try by outcommenting the first TCastleImageTransform in TStatePlay.Start but got errors because the transform cannot be found?

    Clipboard01

    procedure TStatePlay.Start;
    begin
      inherited;
    
    //  WestBeachTransform := DesignedComponent('WestbeachTransform') as TCastleImageTransform;
      PassagebeachTransform := DesignedComponent('PassagebeachTransform') as TCastleImageTransform;
      EastbeachTransform := DesignedComponent('EastbeachTransform') as TCastleImageTransform;  
    

    Relevant TStatePlay is:

    type
        TStatePlay = class(TUIState)
    
        private
        PlayerMouse: TGamemouse;
        Player: TAvatar;
        Location: TLocation;
        PlayingSound1, PlayingSound2: TCastlePlayingSound;
    
       PlayerTransform: TCastleTransform;
    
     
      published
          Label1, Label2, Label3, Label4, NarratorLabel, NPCTalkLabel: TCastleLabel;
    
          procedure CreatePlayerMouse;
          procedure CreateProps;
          procedure CreateLocation;
          procedure CreatePlayer;
          procedure CreateNPC;
    
             procedure Westbeach;
          procedure Passagebeach;
          procedure Eastbeach;
    
     public
          procedure Stop; override;
          constructor Create(AOwner: TComponent); override;
          procedure Start; override;
          procedure Update(const SecondsPassed: Single; var HandleInput: Boolean); override;
          function Press(const Event: TInputPressRelease): Boolean; override;          
     end;
    
    constructor TStatePlay.Create(AOwner: TComponent);
    begin
    
      inherited;
    
      DesignUrl := 'castle-data:/gamestateplay.castle-user-interface';
    end;      
    
  2. @Carring In the published section you show I don’t see a declaration of WestBeachTransform.

    See the post description – only the fields in the published section are initialized. If you remove the line

    WestBeachTransform := DesignedComponent('WestbeachTransform') as TCastleImageTransform;
    

    then you need to make sure the field WestBeachTransform is in published section.

  3. 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

    Assert(WestbeachTransform <> nil);
    WritelnLog('Got WestbeachTransform ' + WestbeachTransform.Name);
    

    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.

Continue the discussion at Castle Game Engine Forum

8 more replies

Participants