Creating and selecting buttons on runtime

That’s weird. Can you screenshot the error? It seems simple enough inside and shouldn’t cause any errors or exceptions.

Yes, that’s a correct way to handle the case, unless you use some other tricks to know “stop reading this part”. Just with a bit different code:

SomeVariableForCount := StrToInt(S); // You cannot assign TStringList.Count directly. There is assignable TStringList.Capacity but I wouldn't count on it, so it's better to declare a separate local variable or even do "for J := 0 to StrToInt(S) - 1"
for J := 0 to SomeVariableForCount - 1 do // you can't use variable "I" here, because NPCInfo[I] will then point to unexpected record or raise range check error, create a new local variable to.
  NPCinfo[I].Locations.Add(Text.Readln);

Note that if you just want to read / write the TStringList contents, then you can use TStringList.LoadFromFile, TStringList.SaveToFile.

Moreover, you can use CastleDownload unit that adds some extensions to standard Pascal TStrings (and so, also TStringList), and then you can use LoadFromURL and SaveFromURL methods on TStringList. Then will handle CGE URLs, like

uses CastleDownload;
...
var
  MyList: TStringList;
begin
  MyList := TStringList.Create;
  MyList.LoadFromURL('castle-data:/my_text_file.txt');
...

Thanks.
I use a text file as a kind of database file to store all variable contents of the different NPC characters.
It worked okay but expanding it with TStringList things made things more complicated.
So using TStringList.SaveToFile to store certain additional information to a separate text file may be better.

I have now this code. It should read the contents (location names) of a TStringList and make buttons for it.

procedure TStatePlay.Button_GoToLocationClick(Sender: TObject);
 var
  S: String;
  I: Integer;

begin
  PlayerMenu.Exists:= true;
  Location.NPC[2].Knowledge.Locations := TStringList.Create;
  Location.NPC[2].Knowledge.Locations.LoadFromURL('castle-data:/characters/thari langdon/thari_knowledge.txt');
  For I := 0 to Location.NPC[2].Knowledge.Locations.Count - 1 do
   begin
      S := Location.NPC[2].Knowledge.Locations[I];
      Location.NPC[2].Knowledge.Locations.Add(S);
      NewButton := TCastleButton.Create(Application);
      NewButton.Tag:= I;
      NewButton.Caption:= S;
      NewButton.OnClick:= @Button_NewButtonClick;
     PlayerMenu.InsertControl(I, NewButton); 
  end;   
end;

But how can I read the caption of the clicked button and perform action?
This does not work:

procedure TStatePlay.Button_NewButtonClick(Sender: TObject);
  begin
   if (Sender as TCastleButton).Caption = 'westbeach' then message := 'But I am already there!' else message := 'Allright, I will go there.';
 end;

This should work. What do you expect to happen and what happens? Note, that when you assign message it isn’t displayed anywhere. You should assign it to some visible UI, like a label. Or at least dump it into WriteLnLog.

Also it’s a bit more reliable to compare TCastleButton.Name not Caption as Caption is something human-readable, and you can change it during the game development (forcing you to apply changes to multiple places in your code, which is usually a bad idea).

Yes, I forgot to add the procedure that takes the “message” and put it in a label.
Embarrassing. :face_with_open_eyes_and_hand_over_mouth:

Ok, it works now and I replaced the caption with the button name.
There is one thing I would like to add and that’s the button name (the location name) to the message.
I added NewButton.Name but it always is the last added button name in the TStringList.
So how can I make the message display the right button name (or caption)?

procedure TStatePlay.Button_NewButtonClick(Sender: TObject);
 begin
   if (Sender as TCastleButton).Name = 'westbeach' then message := 'But I am already there!' else message := 'Allright, I will go to ' + NewButton.Name;
   NPCTalk;
 end;

When you display something human-readable you should use Caption. When you have something unique for internal use - Name. In this situation:

NewButton.Name := 'westbeach';
NewButton.Caption := 'West Beach';

Next, NewButton is never assigned in this procedure. I have a feeling that it should even be a local variable to not be acceptable from other procedures, because it can cause errors like this. In the current case you should do something like that:

procedure TStatePlay.Button_NewButtonClick(Sender: TObject);
 begin
   if (Sender as TCastleButton).Name = CurrentLocation then message := 'But I am already there!' else message := 'Alright, I will go to ' + (Sender as TCastleButton).Caption;
   GoToLocation((Sender as TCastleButton).Name);
   NPCTalk;
 end;

If you reuse the button many times, it might be a good idea to add a “shortcut” like:

procedure TStatePlay.Button_NewButtonClick(Sender: TObject);
var
  ClickedButton: TCastleButton;
begin
  ClickedButton := Sender as TCastleButton;
   if ClickedButton.Name = CurrentLocation then message := 'But I am already there!' else message := 'Alright, I will go to ' + ClickedButton.Caption;
   GoToLocation(ClickedButton.Name);
   NPCTalk;
 end;

Ah yes, this is what I needed. Thanks!

Yes, I use a string variable (DestinationLocation) as a property of the selected NPC so every NPC can have different destination locations. And it is stored in the text file database. :slight_smile:

Thanks, for now this is all I need. I hope to show the result in my next update movie soon (Creating a non linear adventure step by step).

1 Like

Experimenting with this I got an error (already exists) because every time this procedure is called the previous newbutton names still exists and so NewButton := TCastleButton.Create(Application) gives an error.
So how do I delete/destroy the buttons or prevent them from creating them again?
(every call to this procedure should start with no buttons until they are created by name)

procedure TStatePlay.Button_GoToLocationClick(Sender: TObject);
 var
  S: String;
  I: Integer;
  NewButton: TCastleButton;
begin
   For I := 0 to Location.NPC[NPC_Selected].Knowledge.Locations.Count - 2 do
  begin
  S := Location.NPC[NPC_Selected].Knowledge.Locations[I];
  Location.NPC[NPC_Selected].Knowledge.Locations.Add(S);
   NewButton := TCastleButton.Create(Application);
   NewButton.Tag:= I;
   NewButton.Name:= S;
   NewButton.Caption:= S;
   NewButton.OnClick:= @Button_NewButtonClick;
   PlayerMenu.InsertControl(I, NewButton);
   end;    
end;

When you remove the control from PlayerMenu (e.g. by calling ClearControls) it doesn’t get freed automatically. You’ll need to free all controls when removing those.

I’ve made a simple class helper for that in my games: code/states/gameuiutils.pas · grandmaster · EugeneLoza / Vinculike · GitLab

Also in your code you do a not-a-good-thing-to-do:

NewButton := TCastleButton.Create(Application);

I.e. you set “ownership” of the NewButton to Application. This means the button will get freed automatically only when the Application gets freed, i.e. when the game is closed. Here you should set ownership to its direct parent:

NewButton := TCastleButton.Create(PlayerMenu);

which will automatically free the button, when PlayerMenu gets freed (e.g. when you load a new State) or

NewButton := TCastleButton.Create(FreeAtStop);

Which explicitly asks to free the button when this TUiState is stopped, e.g. when switching to another state.

If the solution above works, then you won’t need to apply class helper solution. The last one is only useful when you fill-in the PlayerMenu multiple times in the current state.

Thanks.

I used your class helper and put the code in the procedure.
But now I get a ‘list out of bounds[0]’ error?

procedure TStatePlay.Button_GoToLocationClick(Sender: TObject);
 var
  S: String;
  I: Integer;
  C: TCastleUserInterface;
  NewButton: TCastleButton;
begin
  PlayerMenu.Exists:= true;

  while PlayerMenu.ControlsCount > 0 do
  begin
    C := Controls[0];
    RemoveControl(C);
    C.Free;
  end;

Oh,
I had to change this. :slight_smile:

C := PlayerMenu.Controls[0];

Right.
Now the game crashes after handling the next procedure:

procedure TStatePlay.Button_NewButtonClick(Sender: TObject);
 var ClickedButton: TCastleButton;
 begin
   ClickedButton := Sender as TCastleButton;
   if (Sender as TCastleButton).Name = Location.NPC[NPC_Selected].CurrentLocation then message := 'But I am already there!' else
   begin
     message := 'Allright, I will go to ' + ClickedButton.Caption + '.';
     Location.NPC[NPC_Selected].Action.GoToLocation:= true;
     Location.NPC[NPC_Selected].Action.ResponseDelay:= 150;
     LoadWalkAnimations;
     CommandMenu.Exists := false;
     PlayerMenu.Exists:= false;

   end;
     NPCTalk;
 end;    

Clipboard01

The message string is displayed (with NPCTalk) but then the game crashes with the above error?

Oh. Found it.

Location.NPC[NPC_Selected].Action.GoToLocation:= true;
This referred to another procedure that had still Playermenu buttons in it (but were removed by the remove control before).
Now I get another crash in following “dialog” buttons but I think it is because of the same issue.
I come back to it later, I’ll try to solve it first. :wink:

Got it all working now. :slight_smile:
I made a separate menu for the locations list so it did not cause errors with existing buttons in the player menu.

Now I have another question, as my fantasy and game wishes are more complex than my programming skills :wink:

After selecting a destination location the NPC should walk to it.
If the destination location is directly to the left or right of the current location I have a guess how I could achieve this.
So for instance moving from ‘northbeach’ to “southbeach”
I could make a Stringlist that is read and when ‘southbeach’ is found the NPC knows it has to walk down. It first enters ‘passagebeach’ which has ExitSouthLocation ‘village’. This is not ‘southbeach’ so it keeps walking down until it reaches ‘southbeach’

ExitEastLocation, ExitWestLocation, ExitNorthLocation, ExitSouthLocation: string;

if currentlocation = destinationlocation then StopMove else GoSouth;

But how can I make it find a location in a matrix of locations?

So for instance moving from ‘northbeach’ to “cliffs” is not in a direction list, so how can I make the NPC move there?
There a probably better methods to find the location. This is just my idea so any help with this would be great.

Hi Carring,

I think you need to go over the grid with a for loop. You first look for the start location and note the coordinates (northbeach: col=1/row=0) and then the same with the end location (cliffs: col2=2/row2=4). Now you can determine the delta. dcol=col2-col and drow=row2-row. If the delta values ​​are positive then to the south or east and otherwise to the north or west.

Thanks for your suggestion.
I will try, I have no experience with using delta values, but numbering all locations in columns and rows seems a good idea.
Maybe a coded example would help.
:wink:

something like this:
you need an array, fill the array with your locations and then loop over the array and look for start and end location.

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private

    LocationMatrix: array[0..4,0..5] of string;

public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
var
  StartLocation, EndLocation: string;
  x,y: integer;
  xStart,yStart,xEnd,yEnd,dx,dy: integer;
begin
  StartLocation:='northbeach';
  EndLocation:='cliffs';
  xStart:=0;
  yStart:=0;
  xEnd:=0;
  yEnd:=0;
  dx:=0;
  dy:=0;
  for x:=0 to high(LocationMatrix) do
  begin
    for y:=0 to high(LocationMatrix[x]) do
    begin
      if LocationMatrix[x,y]=StartLocation then
      begin
        xStart:=x;
        yStart:=y;
        break;
      end;
    end;
  end;
  for x:=0 to high(LocationMatrix) do
  begin
    for y:=0 to high(LocationMatrix[x]) do
    begin
      if LocationMatrix[x,y]=EndLocation then
      begin
        xEnd:=x;
        yEnd:=y;
        break;
      end;
    end;
  end;
  dx:=xEnd-xStart;
  dy:=yEnd-yStart;
  if (dx>0)and(dy>0) then ShowMessage(dx.ToString+' Step(s) to the south and '+dy.ToString+' Step(s) to the east');
  if (dx<0)and(dy<0) then ShowMessage(dx.ToString+' Step(s) to the north and '+dy.ToString+' Step(s) to the west');
  if (dx>0)and(dy<0) then ShowMessage(dx.ToString+' Step(s) to the south and '+dy.ToString+' Step(s) to the west');
  if (dx<0)and(dy>0) then ShowMessage(dx.ToString+' Step(s) to the north and '+dy.ToString+' Step(s) to the east');
  if (dx=0)and(dy>0) then ShowMessage(dy.ToString+' Step(s) to the east');
  if (dx=0)and(dy<0) then ShowMessage(dy.ToString+' Step(s) to the west');
  if (dx>0)and(dy=0) then ShowMessage(dx.ToString+' Step(s) to the south');
  if (dx<0)and(dy=0) then ShowMessage(dx.ToString+' Step(s) to the north');
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  LocationMatrix[0,1]:='northbeach';
  LocationMatrix[0,2]:='devils mountain';
  LocationMatrix[1,0]:='westbeach';
  LocationMatrix[1,1]:='passagebeach';
  LocationMatrix[1,2]:='forrest';
  LocationMatrix[1,3]:='eastbeach';
  LocationMatrix[1,4]:='ocean';
  LocationMatrix[1,5]:='paradise island';
  LocationMatrix[2,1]:='mansion';
  LocationMatrix[3,1]:='village';
  LocationMatrix[4,1]:='southbeach';
  LocationMatrix[4,2]:='cliffs';
end;

end.

@eugeneloza I need your help again :slight_smile:

I get an error if I call the Create button procedure again, because it already existed from first creation.
So the buttons or names still exist. When I have clicked the selected NewButton all buttons should be destroyed and a new list created on every call.

Hmmm… I believe TCastleButton.Create(Application); may be the culprit here. Try TCastleButton.Create(FreeAtStop);

If this helps then: You set “ownership” of the button to Application. Usually it’s not a big deal. But it means that the button won’t get “freed” if the current TUiState ends (the trick you do “above” helps if you want to “repopulate” the buttons list, but it’s not called when the state just stops (e.g. changing locations if you implemented those as states)). Setting “ownership” to FreeAtStop will make sure this button will get freed when this TUiState stops.