Creating and selecting buttons on runtime

On runtime I created some buttons that are part of a menu (TCastleVerticalGroup).
and insert them in controls.
But how can I access their individual contents (depending on the caption text or controlindex / button number) in the NewButtonClickmenu?


procedure TStatePlay.Button_GoToLocationClick(Sender: TObject);
 begin
   PlayerMenu.Exists:= true;
   NewButton := TCastleButton.Create(Application);
   NewButton.Caption:= 'westbeach';
   PlayerMenu.InsertControl(1, NewButton);

   NewButton := TCastleButton.Create(Application);
   NewButton.Caption:= 'passagebeach';
   PlayerMenu.InsertControl(2, NewButton);

   NewButton := TCastleButton.Create(Application);
   NewButton.Caption:= 'eastbeach';
   PlayerMenu.InsertControl(3, NewButton);
   PlayerMenu.InsertFront(NewButton);

 end;

 procedure TStatePlay.Button_NewButtonClick(Sender: TObject);
 begin
    How can I access the clicked PlayerMenu index number?
  
 end;
Sender as TCastleButton;

is the button that was clicked. E.g. like this:

NewButton := TCastleButton.Create(Application);
NewButton.Name := 'ButtonGoWest';
NewButton.Caption:= 'westbeach';
NewButton.OnClick := @Button_NewButtonClick; // DON'T FORGET THIS
PlayerMenu.InsertControl(1, NewButton);
...
procedure TStatePlay.Button_NewButtonClick(Sender: TObject);
begin
  If (Sender as TCastleButton).Name = 'ButtonGoWest' then
    GoWest;
end;

However, depending on the situation (if the amount of buttons is fixed and they do some defined thing without much context needed then creating a separate OnClick events for different actions may be better. E.g.:

NewButton := TCastleButton.Create(Application);
NewButton.Caption:= 'westbeach';
NewButton.OnClick := @ClickWest;
PlayerMenu.InsertControl(1, NewButton);
NewButton := TCastleButton.Create(Application);
NewButton.Caption:= 'eastbeach';
NewButton.OnClick := @ClickEast;
PlayerMenu.InsertControl(1, NewButton);
...
procedure TStatePlay.ClickWest(Sender: TObject);
begin
  GoWest;
end;
procedure TStatePlay.ClickEast(Sender: TObject);
begin
  GoEast;
end;

It’s the TComponent, right?

NewButton.Tag := {index}
1 Like

I just had to find that snipped from my code

Button.Tag := PtrInt(MapItem);
...
MapItem := TMapItem((Sender as TComponent).Tag);  

And then naively wonder why my game crashes randomly :smiley:

Note about your code snippet:

   PlayerMenu.InsertControl(3, NewButton);
   PlayerMenu.InsertFront(NewButton);

This inserts the NewButton twice to PlayerMenu (which is not allowed, you cannot insert the same UI 2x into the hierarchy, and you will get a warning about it). Delete one of the above lines.

As for the topic question, others have exhausted the answers :slight_smile: To summarize, the approach can be:

  1. Use different OnClick handlers for each button.

  2. Use the same OnClick for every button, but assign different Tag to each button, and then in click handler look at (Sender as TCastleButton).Tag to read this tag.

  3. Look at really anything else you want through the Sender. You can even look at Name, like (Sender as TCastleButton).Name.

  4. There are more possibilities actually – you could create your own TCastleButton descendant, that can “carry” any additional information (fields and methods) you want.

To stick to the simplest advise: If the buttons do different things, go with AD 1. If the buttons do the same thing, I’d go with AD 2.

1 Like

Thanks all!
I go for the AD2 because the only outcome of every click will be a sprite going to the button caption name location.
The idea is that every location that is ‘visited’ before by the sprite will be added to a stringlist. So every new visited location will create a new button in the location list.
So the next thing is: how do I add to the stringlist and read back the contents to update a button list?

The easiest way would be TStringList:

var
  S: String;
  I: Integer;
  Locations: TStringList;
begin
  Locations := TStringList.Create;
  Locations.Add('firstlocation');
  Locations.Add('secondlocation');
  Locations.Add('thirdlocation');
  for S in Locations do
    WriteLnLog(S); // will output fist, second and third location names
  Locations.Delete(1); // will delete 'secondlocation'
  for I := 0 to Locations.Count - 1 do
    WriteLnLog(Locations[I]); // will output first and third locations names.
  Locations.Free;
end;

because TComponent.Tag was int32 not PtrInt on 64-bit archs (too lazy to check myself) ?

There are more possibilities actually – you could create your own TCastleButton descendant

Didn’t suggest it because - while it the most correct approach from theoretical OOP - it would require some non-trivial for newcomers scafoflding to make those subclasses into GCE Editor GUI.

Though for a program which user interface is designed by code not by visual tools that could be the cleanest approach

Blockquote TComponent.Tag was int32

It’s PtrInt and PtrInt = Int64; so should be good. And the crashes are not from buttons :slight_smile: I just mentioned that I’m doing something stupidly unsafe.

Blockquote user interface is designed by code

Wait… that button is generated by code. Why the heck did I need to use Tag hack for it when I can effortlessly make a descendant??? Thank you for opening my eyes :smiley: (Yeah, I know how to rebuild Editor, but try to avoid custom components for as long as possible because it creates some issues, e.g. setting up paths correctly which I’m often too lazy to)

Indeed, with FPC and latest Delphi TComponent.Tag size deliberately matches the pointer size.

You can register your custom button class to be available even visually. See Editor | Manual | Castle Game Engine , Custom Components | Manual | Castle Game Engine . So the custom class is a working approach for both buttons created by code and designed visually. But in general, indeed I would not advise AD 4 (creating custom class) as a first choice – it’s probably more work than necessary for something trivial, when the TComponent.Tag seems “simple enough”.

Yeah, sure. I’m using it often :smiley: Just “if it’s possible” then try to avoid custom components, because they create some additional hassle: need to be registered, registered in Manifest, rebuild Editor every time (and it’s not always trivial, e.g. I failed to set up properly environment variables in XFCE, so I need to quit Editor and run it from terminal to have CGE at $path - again additional hassle :D), etc.

I get an identifier not found “WriteLnLog”?

image

You need to include uses CastleLog for this function to work. It just writes a line into the game log (usually in AppData on Windows, into Terminal output on Linux) where it can be found and used for debugging.

1 Like

Also see manual: Logging | Manual | Castle Game Engine

Also note that if you don’t know where something is, you can just search the API reference. The first result for searching WritelnLog correctly shows it is part of CastleLog unit: Search Results

I did use the search option but did not see any uses unit?

Fist it says : CastleLog.WriteLnLog - the first one is the “namespace” (in Pascal it means name of the Unit you have to put into the uses section). Also you can additionally click on the found link and on top of the page there you can see which unit provides the appropriate class or function:

1 Like

Now I want to write the contents to a text file and read them back.

Locations := TStringList.Create;
   Locations.Add('westbeach');
   Locations.Add('passagebeach');
   Locations.Add('eastbeach');   

for S in Locations do
          WriteLn (InitialDBFile, S);
          Locations.Free;

This works, but reading them back?

Procedure LoadNPC_Database;
var
  S, S1: String;
  I: Integer;
begin
   Text := TTextReader.Create(DBfile);
  try
  S := Text.ReadLn;    
 
    NPCinfo[I].ID:= StrToInt(S);
    S := Text.Readln;
    NPCinfo[I].Personalia.FirstName:= S;
    S := Text.Readln;
    NPCinfo[I].Personalia.LastName:= S;
   
    for S1 in NPCinfo[I].Locations do
    S := Text.Readln;
    NPCinfo[I].Locations.Free;

```
It does not read the 3 location strings apparently as my game crashes while loading the text file.

First: You do not seem to have I initialized. This alone is enough to have a crash. Also make sure how many elements NPCInfo contains (and why you are reading only one of them).

Next: You call for S1 in NPCinfo[I].Locations do on, most likely, non-initialized instance of Locations. And even if it would be initialized, you won’t be able to for inside of it, because it’s empty. I’m not exactly sure what is the state and purpose of all the classes and variables here, but something like this should hopefully get you started:

  try
    I := 0;
    S := Text.ReadLn;    
    NPCinfo[I].ID:= StrToInt(S);
    S := Text.Readln;
    NPCinfo[I].Personalia.FirstName:= S;
    S := Text.Readln;
    NPCinfo[I].Personalia.LastName:= S;
   
    NPCinfo[I].Locations := TStringList.Create; // Create the instance
    while not Text.Eof do // Read every other line until the end of the file
      NPCinfo[I].Locations.Add(Text.Readln); // Add every line to the Locations class

    NPCinfo[I].Locations.Free; // this will destroy everything we've just read, but for testing purposes...

Thanks!
The while not Text.Eof caused an error (" ") but maybe I should also save the location.count of strings in the textfile and then read it back on loading the textfile

NPCinfo[I].Locations := TStringList.Create;

S := Text.Readln;
NPCinfo[I].Locations.Count := StrToInt(S);
for I := 0 to Locations.Count - 1 do
NPCinfo[I].Locations.Add(Text.Readln);

something like this or is there a better way to read the number of strings back and then continue with other variables that are not part of the stringlist?