Converting string name to existing scene

I would like to use a string name to replace a scene name in a procedure so I don’t have to use if…then for every similar statement.

original:

If Player.Transform.WorldBoundingBox.RectangleXY.Collides(westbeach_downplane.WorldBoundingBox.RectangleXY) then

idea:

Location.Name := 'westbeach';

if Player.Transform.WorldBoundingBox.RectangleXY.Collides(Location.Name + '_downplane' + .WorldBoundingBox.RectangleXY) then // this obviously not works because .Worldboundingbox is a scene method, not a string).

Where Location.Name is a string, depending on the location.
Each location has a number of planes that set the borders of the location for limiting movement of a character sprite.
(westbeach, eastbeach, northbeach etc.)

So is this possible?

Yes, it is possible. I believe the simplest (in logic, may be more complicated in code) way to is by using a TObjectDictionary like

uses Generics.Collections
...
var
  CurrentLocation: String;
  ScenesDictionary: specialize TObjectDictionary<string, TCastleScene>;
procedure InitializeScenes;
begin
   ScenesDictionary := specialize TObjectDictionary<string, TCastleScene>;
   ScenesDictionary.Add('westbeach_downplane', westbeach_downplane);
   ... // add others
   CurrentLocation := 'westbeach_downplane';
end;
...
if Player.Transform.WorldBoundingBox.RectangleXY.Collides(ScenesDictionary[CurrentLocation].WorldBoundingBox.RectangleXY) then ...
...
(when finishing everything)
FreeAndNil(ScenesDictionary);
...

You can read a bit more about generic lists and dictionaries in Modern Object Pascal Introduction for Programmers | Castle Game Engine

However, please note that while this will work, this is not the best way to. A more efficient way would be to handle everything related to the location through such sort of containers. E.g. like this:

type
  TLocation = class(TObject)
  private
     // Private stuff needed to handle internally
  public
    // Public variables; including Scene and a lot of other information related to the scene
    LocationName: String;
    GlobalX, GlobalY: Integer;
    ... // and anything else related to this location grouped together
    Scene: TCastleScene;
  end;
  TLocationsDictionary = specialize TObjectDictionary<string, TLocation>;

And then you’ll work with Player.Transform.WorldBoundingBox.RectangleXY.Collides(Location[CurrentLocation] .WorldBoundingBox.RectangleXY) and have easy and unified access to everything related to the location in one call.

Thanks, this looks the most direct approach.

I have now this but I get errors?

type TLocation = class(TObject) // (TComponent)
  public
    Name: String;
    LongDescription, ShortDescription: String;
  end;
  TLocationsDictionary = specialize TObjectDictionary<string, TLocation>; 


procedure TAvatar.Border_Collision;
begin
  with StatePlay do
  begin
    Location.Name:= Location.Name + '_topplane'; // the stringname is defined in a different procedure

 //   if Player.Transform.WorldBoundingBox.RectangleXY.Collides(westbeach_topplane.WorldBoundingBox.RectangleXY) then...
    if Player.Transform.WorldBoundingBox.RectangleXY.Collides(Location[Location.Name].WorldBoundingBox.RectangleXY) then...

 end;
end;       

Yes, because your Location doesn’t have a WorldBoundingBox. (It was my typo above :D). To have it you either need to have some TCastleScene inside TLocation and then ask it as Locations[Location.Name].Scene.WorldBoundingBox

Or alternatively (it’s possible, though I’d not recommend doing it this way) you can have TLocation = class(TCastleScene) - then your TLocation can just be rendered on the Screen. However, it may cause some unexpected consequences.

Note 1 - you don’t need to ask Locations[Location.Name] if you already have Location class you can just ask Location.Scene).

Note 2 - don’t do Location.Name:= Location.Name + '_topplane' - by this you modify Location.Name - i.e. imagine initially you’ve had Location.Name = 'westbeach'. Now you modify the Name and get Location.Name = 'westbeach_topplane', then you modify it again (e.g. when switching locations) and you get unexpected Location.Name = 'westbeach_topplane_topplane'

You’d be much better to use Locations[Location.Name + '_topplane'].Scene.Xxxxxx - this way Location.Name will always stay the same.

Thanks. I added a scene but it still is not recognised inside this code line?

type TLocation = class(TObject) 
  public
    Name: String;
    Scene: TCastleScene;
 end;
  TLocationsDictionary = specialize TObjectDictionary<string, TLocation>;

Check Messages window for errors. It most likely has some compilation issues. You might need to press F9 to start compiling for Messages window to update properly (don’t worry about unfinished line, it fails somewhere above, e.g. it can say something like “Unknown identifier TCastleScene” and you should include it somewhere in the uses section.

UPD: Ah, it may say “Cyclic reference detected” or something similar complaining about Generics.Collections. Unfortunately it’s the bug of that specific unit and sometimes you get Code Tools (the utility responsible for the hint in your screenshot) failing to parse your code properly. Just write next part of the code (.Scene) manually, it should compile without issues - this bug affects only hints.

It says Identifier Locations not found.

Then I studied the manual on Dictionaries and tried this (with this result).

I am confused.

Ok, one step at a time.

First error notifies you that indeed you don’t have the container Locations. You need to 1) declare it and 2) fill it with your locations information.

You should declare it somewhere globally. E.g. just as a top level variable in interface of the Unit that deals with locations.

Then you should create it - only once, not every time it is requested.

And finally you should fill it in with proper data about your locations. For this feature to work in your favor - you should have there all your locations loaded and ready for action.

The second error complains it cannot find location named 'westbeach_topplane' in Locations.

So it should be something like this (again, it’s a pseudocode, don’t rely it’ll work out-of-the-box, you’ll need to adjust it to fit for your purpose :))

interface
type
  TLocation = class...
  ...
  end;
  TLocationsDictionary = specialize TObjectDictionary<string, TLocation>;

var
  Locations: TLocationsDictionary;

procedure LoadLocations; // this is a "forward declaration", you should call it at the beginning of the game, e.g. in GameInitialize

implementation

procedure LoadLocations;
var
  Location: TLocation;
begin
  Locations := TLocationsDictionary.Create([doOwnsValues];
  // Here I hardcoded the locations information, it's convenient when beginning the development
  // Later it'd be better to load those from some game data (like Text, JSON or XML file) where it's easier to edit
  Location := TLocation.Create;
  Location.Name = 'westbeach';
  Location.Scene := westbeach_topplane; // Maybe directly TCastleScene.Create('castle-data:/locations/westbeach.X3D');
  Locations.Add(Location.Name, Location);
  Location := TLocation.Create;
  Location.Name = 'eastbeach';
  Location.Scene := eastbeach_topplane;
  Locations.Add(Location.Name, Location);
end;

...(here you can work with Locations)...

finalization
FreeAndNil(Locations);
End.

Note that here you will access Location by it’s regular name. So, you’ll need to use Locations['westbeach'] without any additional '_topplane'.

Many thanks!
I did as you showed above.
The Location properties are recognised but now I get Ilegal expression errors?

type TLocation = class(TObject)
  public
    Name: String;
    Scene: TCastleScene;
  end;
  TLocationsDictionary = specialize TObjectDictionary<string, TLocation>;  

You do

Location.Name = 'westbeach';

instead of using := to assign.

Thanks, I overlooked that.

Now I still can’t acess the planes in the above example.
I created the plane in the design editor and need to access it from the collision procedure.

type
     TStatePlay = class(TUIState)

    published
    WestbeachImageTransform: TCastleImageTransform;
    westbeach_topplane,westbeach_downplane: TCastlePlane;  

Clipboard01

Clipboard01

And it needs a string but that has no WorldBoundingBox.