Jump to content
Willicious

Problems with Delphi class structure / visibility

Recommended Posts

15 hours ago, Willicious said:

It's the idea of instances-of-classes that keeps catching me out. If a class is defined somewhere, it should be callable as that class. Why do I have to redefine/redeclare it in a different unit? Why not this instead:

Unit 1
    Class1

          PropertyA

 

Unit 2

    uses

         Class1

 

if Class1.PropertyA etc...
 

 

The one thing no one has pointed out:

 

Unit1

  TClass1

     PropertyA

 

Unit2

  Uses

    Unit1 {the unit name, not the class inside the unit name}

 

Another useful link is https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Programs_and_Units_(Delphi)

 

  • Like 1

Share this post


Link to post
15 hours ago, Willicious said:

In this code, the call for LemGame.CurrentIteration generates an undeclared identifier error. This is in spite of LemGame being one of the uses in this unit, and CurrentIteration being public and callable from other units (elsewhere, it's called as Game.CurrentIteration, but that doesn't work here either).

You don't show your uses clause(s), but the only explanation for what you describe is that you have not added the unit that LemGame is declared in to your uses clause. That, or you have not actually declared a LemGame variable anywhere, or it is otherwise out of scope here.

Share this post


Link to post
15 hours ago, Willicious said:

Yes, but surely we can use pseudo-code or non-compilable examples to illustrate the concepts?

The problem with that is pseudo-code is an extreme abstraction. It's not helpful when trying to determine the cause of compiler errors. Your pseudo-code conflated units with classes -- I think perhaps an OOP primer would be useful for you. You might start by getting Cantu's Object Pascal Handbook or some other free basic introduction to coding with Delphi. https://lp.embarcadero.com/Object-Pascal-Handbook-2021

  • Like 1

Share this post


Link to post

It happened again today when I was briefly attempting to use a property of (LemTheme unit / TTheme class) in (GamePreviewScreen Unit).

 

The problem is that LemTheme / TTheme cannot be called as a separate instance because the original instance itself loads a particular theme for use in many different parts of the game (but unfortunately not the Preview screen). If I create a new instance of TTheme in the Preview screen logic, it will be an empty instance. The actual instance where all the stuff gets loaded is in LemTheme, in the actual TTheme class itself.

 

This was essentially the problem I was having yesterday: I was attempting to use a new instance of TReplay, but all the actual replay data was being created and stored in a different instance (the original one, in the LemReplay unit).

 

To use one of the analogies given here... I need to use some of the furniture in house 2 whilst I'm in house 1, but to do so is impossible without copying the furniture. As I understand it, it's bad practice to write code again that's already written somewhere else (i.e. I shouldn't have to load the Replay or the Theme again in the current Unit/Class if it's already been loaded in another Unit/Class). Instead, I should be able to access that data, but... sometimes, it just doesn't let me do so, and I'm not entirely sure why.

 

Maybe I can make a quick video sometime demonstrating the problem. Copying and pasting code can lead to confusion, sidetracking and over-focus on context rather than getting to the actual root of the problem (which, in my case, is that I'm clearly not understanding the basic principles of OOP).

Edited by Willicious

Share this post


Link to post

OK, here's a video demonstrating the issue I'm having. It comes up time and again, and sometimes I can find a fix by mimicking code elsewhere in the project. But, I'd much rather understand exactly what's going on for those times when there's nothing to mimic, and I need to fix it myself.

 

Apologies again for the tone of the OP in this topic, I have been very frustrated with it recently and I would appreciate whatever help you can offer. I do want to try and understand as much as I can, and I realise that Delphi itself isn't the problem!

Share this post


Link to post

I watched the first third of your video. You don't have an instance of your class TNeoTheme to actually retrieve the property from.


A theme sounds like something you only need a single instance of in your whole application (I could be wrong on that, but for now let's say I'm right).

 

Since there's only going to be one of them a global TNeoTheme instance variable will be acceptable for now. Let's declare it in the LemNeoTheme.pas unit

unit LemNeoTheme;

interface

type
  TNeoTheme = class
  private
    FLemNames : string;
  public
    property LemNames : string read FLemNames write FLemNames;
  end;

var
  MyTheme : TNeoTheme; { this variable MyTheme will represent the single instance of the class TLemNeoTheme }
                       { it does not however create that instance here, just declares the variable }

implementation

{ code, code, code, more code }

{ now at the very bottom of the unit, just before the final end. statement }

initialization
  MyTheme := TNeoTheme.Create; { this is where the memory for the instance of TLemNeoTheme is allocated }
                               { and assigned to the MyTheme variable. It will occur when the program starts }

finalization
  MyTheme.Free; { this is where the memory for the instance is released. It will occur when the program shuts down }
  MyTheme := nil;

end.

Now over in TGamePreviewScreen.GetTextLineInfoArray you access the LemNames property via the MyTheme instance variable

  Result[2].Line := Result[2].Line + IntToStr(zombies + lemmings)
                                   + MyTheme.LemNames;

Please be aware that we're only scratching the surface with the code above. It's probably appropriate for your TNeoTheme class but most other class instances will not be global variables like this and will require other techniques.

 

Actually surely there is already a TNeoTheme instance somewhere in this code already. Do a Find in Files search for "TNeoTheme.Create". Ignore the actual constructor implementation but you'll likely find something like:

  AnotherThemeInstanceVariable := TNeoTheme.Create;

That variable AnotherThemeInstanceVariable is probably the one you should be using rather than the MyTheme variable I declared and created above.

 

I hope you'll take this in the spirit that it's intended in but a quick look at the project on that video makes me think you've picked a pretty big mountain to climb for your first outing. Maybe go find a few gentle hills to train on first before attempting something as large and complex in scale as game development.

Edited by Lachlan Gemmell
  • Like 3

Share this post


Link to post

I watched the rest of the video. A few comments.

 

Firstly if you find yourself typing unit names (e.g. LemNeoTheme) outside of a uses clause, 9 times out of 10 you're doing it wrong.

 

Secondly if you find yourself accessing a property through a class name (e.g. TNeoTheme), 8 times out of 10 you're doing it wrong.

 

Think of a unit and the classes inside it as the paper instructions on how to build a table (a real physical table you sit at). The class procedures and functions are the steps you will need to follow to build that table and the data variables inside that class are just a list of the tools you'll need. 

 

Lets call this class TFantasticWoodenTableBuilder and it has public property HammerColour (which is a string).

 

Don't think of that TFantasticWoodenTableBuilder class (or the unit that contains it) as being "alive" in any sense. It is as inanimate as an actual set of paper table building instructions. It can't actually physically do or verbally tell you anything.

 

Next a real person Frank comes along and reads those instructions. Frank commits them to memory, goes away and starts building a table. 

 

Frank is now an instance of TFantasticWoodenTableBuilder and his HammerColour happens to be red.

 

John reads the instructions and decides to build a wooden table with them. His hammer is blue.

 

We now have two instances of TFantasticWoodenTableBuilder, Frank and John each with their own separate properties (though both called HammerColour) that stores their own respective hammer colours.

 

If you want to know the colour of somebody's hammer you have to ask them (via an instance variable). There's no point asking the class (TFantasticWoodenTableBuilder), it's just a piece of paper, it's not going to talk back to you.

 

The Frank and John instance variables are declared like this

var
  Frank : TFantasticWoodenTableBuilder;
  John : TFantaticWoodenTableBuilder;

This is how they are created (as in memory allocated to them)

  John := TFantasticWoodenTableBuilder.Create;
  Frank := TFantasticWoodenTableBuilder.Create;

This is how you get or set their hammer colours

if John.HammerColour <> 'blue' then
  ShowMessage('John has pinched somebody elses hammer');

Frank.HammerColour := 'green';

When you're done with them you free these instances so they release their memory

Frank.Free;
Frank := nil; { to everyone else, lets not make this line a thing please :-) }

John.Free;
John := nil;

So the above code declared two instances John and Frank, created those instances, interrogated their respective hammer colours, and lastly freed the memory associated with those instance variables.

 

Very important, if you're in another unit and you want to know what Frank's hammer colour is, you need to be able to reference the Frank instance through the Frank variable. Now that Frank variable is probably not declared in the same unit as the TFantasticWoodenTableClass. It's probably declared in some other unit. You have to add that other unit to your uses clause, not the unit that has the TFantasticWoodenTableClass in it.

 

How do you know which other unit? Well you have to go searching for it just like you did at 7:04 in your video. How do you know what to search for? Detective work. Welcome to programming.

 

At the 7:30 mark of the video you're getting close to understanding the problem but the actual solution is going to evade you until you learn more about object oriented programming. I haven't watched it but this video might be a good start.

Edited by Lachlan Gemmell
  • Like 3
  • Thanks 1

Share this post


Link to post

I recommend that you visualise the whole situation. That is the most important thing to do. This will help you to understand the problem.
Since you can't do it in your head, and neither can I, you have to sketch it out. Paper is inappropriate for this task. The best I know is the DIA program. It's not finished and it's already dead. It does its job more than well.
I'm confident that you will move forward with this technique.
I personally use it to visualize problems:

  • DIA: DB bindings, Classes: dependencies (links), their structure - as needed
  • Word, text editor
  • Excel
  • FreeMind
  • Like 1

Share this post


Link to post
19 hours ago, Lachlan Gemmell said:

Actually surely there is already a TNeoTheme instance somewhere in this code already. Do a Find in Files search for "TNeoTheme.Create". Ignore the actual constructor implementation but you'll likely find something like:


  AnotherThemeInstanceVariable := TNeoTheme.Create;

 

Yes, you're right! There is one called simply fTheme, which is declared and created in LemRendering. This would make sense, since the renderer does all of the drawing/animation work, and for that it needs the sprite types and theme colours.

 

Problem is, I'm having the same issue trying to access LemRendering from GamePreviewScreen. To be more specific, the class in LemRendering is called TRenderer, and here's where the instance of TNeoTheme is created:

 

constructor TRenderer.Create;
begin
  inherited Create;
  fTheme := TNeoTheme.Create;

 

LemRendering is in the uses clause at the very top of GamePreviewScreen, not the implementation/uses clause.

 

Thanks for your detailed replies btw, very helpful 🙂👍 I'm closer to understanding the principle, but there are still times like I described in the video where I do declare an instance, but it's empty - like if one of the table builders in your example hadn't read the instructions.

 

And, in the example here, I've managed to find an existing instance of TNeoTheme which is an actual "table builder", but I now need to know how to direct them to GamePreviewScreen to do some stuff over there. Or... don't I? Is this one of those times when declaring a new instance in GamePreviewScreen would be creating a new "table builder who has read the instructions"?

Edited by Willicious

Share this post


Link to post
3 hours ago, Willicious said:

Problem is, I'm having the same issue trying to access LemRendering from GamePreviewScreen. To be more specific, the class in LemRendering is called TRenderer, and here's where the instance of TNeoTheme is created

So you'll need to find an instance of the TRenderer to access it's internal theme instance (hopefully via a public Theme property on the renderer).

 

Use the same technique. Do a find in files for TRenderer.Create and find where the renderer is created. You may find it's contained by another class, in which case you repeat the same process for that class to find it's instance(s).

 

To get access to the theme LemNames you may end up doing something like

  SomeGlobalInstanceOfSomeClass.SomePropertyForTheRendererInstance.Theme.LemNames := 'Cats';

Note that none of the parts of that made up line are unit names or class names. They're all instances, a global instance in the case of the first part, then properties within a class representing an instance, and finally the LemNames string property of the TNeoTheme class.

 

To get this fictional line of code to compile I need to add to my uses clause whichever unit contains the declaration for the first instance, in this case the SomeGlobalInstanceOfSomeClass global variable.

Edited by Lachlan Gemmell
  • Thanks 1

Share this post


Link to post
18 hours ago, Lachlan Gemmell said:

So you'll need to find an instance of the TRenderer to access it's internal theme instance (hopefully via a public Theme property on the renderer).

 

Use the same technique. Do a find in files for TRenderer.Create and find where the renderer is created. You may find it's contained by another class, in which case you repeat the same process for that class to find it's instance(s).

 

To get access to the theme LemNames you may end up doing something like


  SomeGlobalInstanceOfSomeClass.SomePropertyForTheRendererInstance.Theme.LemNames := 'Cats';

 

Right on the money! GameParams is the global instance, that provides the address from which I can then access Renderer, Theme, and LemNames! And, it works!!!

 

It's reassuring to know that sometimes the only way to know something is to know what to search for. And, thanks to your explanation, I actually understand what's going on here 👍

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×