#Best practice for accessing another scene trees nodes

34 messages · Page 1 of 1 (latest)

last thistle
#

Hi everyone,
I am new to Godot and although I appreciate many things of its ways I couldn't figure out a sustainable practice of accessing another scene trees nodes. A quick example: A game is made of a main scene tree, a player scene tree and an enemy scene tree. The main scene contains instances of player and enemy as well as a tilemap and maybe a GUI. Following the common approach of the separation of concerns I split all the functionality of my various game objects into different sub nodes. Therefore I have an Armament node (basically a script called "Armament" attached to an ordinary "Node") that contains everything an object needs to calculate its attack range in tile units. Both the player and the enemy are meant to leave all the range calculation logic to their respective Armament node. Lets say, to calculate the distance in tile units the respective Armament node of player and enemy needs a reference to the tilemap node. Because it is not possible to drag and drop the tilemap node from the main scene directly into the export field of another scenes sub node (in this case the Armament node), one is forced to use their respective root node by making it provide an export field and dragging and dropping the tilemap node there. The Armament node only needs a reference to its root node, from where it could access the Tilemap reference. But...
What if the root nodes of the different Armament instances aren't of the same type? For example, in this case the player derives from CharacterBody2D and the enemy derives from StaticBody2D. With Godots lack of interfaces there is no way for one single Armament script to have an export field being capable of referencing a variety of different root node types - at least if you are like me not willing to work with type casts or using the built in functions to iterate through scene trees because of the high maintenance costs and susceptibility to errors. So what would be the best practice in this case?

rocky ridge
#

Godot has duck typing. So where in a language like java you could have both implement an interface with a method calculate_path() and then check for that interface and call the method in godot you can just call the function on the node, nodes who do not possess that function will simply not execute anything.

#

The way it looks to me you have 2 options, you either pass in your tilemap as a param when calling calculate_attack_range(map : TileMap). Or you could save a reference to the tilemap in a global structure so you always have a way of getting it. I don't think the practice of getting nodes via relative searching like get_parent() is that much of a code smell because if they fail you want to be notified (tried to call calculate_range but tilemap is null would be valuable information, there's no way to calculate anything without a tilemap and i would like the system to notify me when that happens). So you could use get_node to set your tilemaps instead of dropping them into a var via the editor.

last thistle
# rocky ridge Godot has duck typing. So where in a language like java you could have both impl...

Hi Sibyl, thanks a lot for your explanation! I have never heard of duck typing, but this should pretty much solve the problem... if I used GDscript! It seems that there is no adaptation to the C# code I usually write. So I would either have to use GDscript for the affected classes or stay with C# and abandon the whole Godot package of nodes, drag and drop, etc. at least for individual solutions like this. And this is because using C# interfaces (which I usually prefer) leads to a failed build due to Godot not accepting nodes dragged and dropped onto the inspector field of a declared interface type. Ultimately this would result in using the Godot editor only for the obvious tasks and switching to pure C# code with constructor calls and so on for the slightly more specific functionality. Am I right?

last thistle
tacit raptor
#

(although in most cases you should be able to identify a single tilemap used for these calculations in the whole tree without passing it around, I think)

rocky ridge
#

I'm not familiar with godot + C# so I'm not sure about editor export fields accepting nodes of a certain interface and accounting for that properly. But there are some facilities to protect you from unsafe casts @ https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_features.html

rocky ridge
rocky ridge
# last thistle Hi Sibyl, thanks a lot for your explanation! I have never heard of duck typing, ...

if you want the class to work in your editor you can use a global class i think https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/c_sharp_global_classes.html

rapid heart
last thistle
# rocky ridge I'm not familiar with godot + C# so I'm not sure about editor export fields acce...

Thank you. Although I am able to express myself in clean, well thought code I am not so much experienced with the technological fundamentals underneath the written programming language. Maybe its because of this I do not really recognise any solution in the described features, which seem to show the same thing in different manners: a simple type cast. I want my errors to be shown during compile time and as far as I understand there is no way to cover the pitfalls of type casts. One way or another: If one casts wrong, the program crashes, no matter if I use them explicitly or implicitly by generifying the return type and therefore just pushing the actual type cast further into the engine... Please tell me if I didn't understand correctly!

last thistle
last thistle
last thistle
last thistle
rocky ridge
#

Say you have an Armament class that needs a reference to a TileMap to perform a calculation. You could have this Armament request a node (enemy or player in your case) which implements an interface such as:

public interface IHaveTileMap {
  public TileMap GetMap() {};
}

Now you can check for this contract when requesting the node and execute GetMap() to obtain your reference. However if player for example doesn't obtain a tilemap (maybe you mistyped the nodepath) the method will still return a null and you're left with a null reference in your Armament calculation. This is when your application would exit, and there's no way to statically check for that. Getting a concrete reference happens at runtime.

#

It works the same way in godot, I can have a script that has a exported variable of type TileMap, but I can't ensure that whatever mechanism i use to get this reference actually results in a working TileMap being delivered. Inside my script I can call any function on this TileMap as if it exists but getting the actual reference is something that happens at runtime. Some other piece of code could theoretically delete the tilemap at some point or modify the tree structure so it's no longer in the correct position and break it.

#

I think most of your issues would be fixed if you moved your tilemap reference to something of a "Level" node. Then all armaments could get their reference from there. And It makes sense for a level to have 1 or multiple tilemaps, each character having their own would mean you have n tilemaps for n characters - which seems wrong (but might be correct in the context of your game). In your example I'm assuming both player and enemy are referencing the same tilemap.

#

Alternatively you could make a global class that has a property that exposes a tilemap (like an interface). And have Player+Enemy both be instances of this class - this means you merge all their functionality just so you could reference them both as 1 type. And at that point why not just directly reference the TileMap?

#

In short, there are multiple ways of getting a reference to a node that is less fragile than an absolute path to the node. But you can never ensure a get_node() will result in a valid node at runtime as the scene tree is constantly changing (we might be adding explosion effects, removing enemies etc). Your options are either via code get_node or exporting a var and drag&dropping the tilemap inside the editor property. But neither are guarenteed to protect you from any runtime mishaps

last thistle
# rocky ridge In short, there are multiple ways of getting a reference to a node that is less ...

Hmm... 🤔 So basically, what you are saying is that once one got a reference to the tilemap node one could never be sure if it points actually to that tilemap. It could be null or be overwritten with something else (another tilemap) during runtime or whatever. I understand that. What I don't understand is why this problem would relativise the additional problems of manually reworking every string I pass in get_node() or the risk of casting to the wrong type. Both are issues you might stumble upon during runtime - in worst case after you rolled out your game. So while accepting the problem you described one could at least try to work around the others. Shouldn't we focus on solutions that hold the potential of compile time validation? Duck Typing in general seems to be an alternative to the concept of interfaces and therefore guarantees to catch a node with full avoidance of the risks and problems I explained. Of course, one would have to use them wisely, always surround them with has_method(), etc. And of course, it doesn't guarantee the right object. But from what I understand this seems to be the most secure way for this explicit case.

last thistle
# rocky ridge Say you have an Armament class that needs a reference to a TileMap to perform a ...

Well, in java I always solved that problem by passing references via constructors and assigning them to constant fields during the respective object construction. This way no one would ever have the opportunity to change something that is not meant to. I was actually quiet shocked when I realised that no game engine seems to care about that concept and handles object instantiation the way they do. I would be thankful for every explanation one could give me on this one. Because I do miss my interfaces, constants and my tight, secure object control a lot in Godot and Unity...

rocky ridge
#

And just as an extra aside, there's no need to do it in code. You can expose your TileMap via a export var and assign it in the editor

rapid heart
last thistle
# rocky ridge I assume you mean final, java doesnt have const afaik. But in your example i cou...

In Java, constants are indicated by the final keyword. Its the same, they are just called constants. And yes: by default you'd have that problem in Java, too. Your constructor could receive null and assign that to its declared constant. Therefore people use annotation frameworks like Lombok or built in IDE solutions, which provide Nullable annotations for your fields, parameters and local variables. This way one could write constructors and declaring its parameters as @NonNullconstants. This way the compiler can tell you if you receive any null values during compile time. More modern languages like C# even come with built in concepts to handle the null problem. Therefore it seems like a missed opportunity to me that engines like Godot or Unity, which support C# natively, in fact completely ignore the existence and possibilities of constants. I'd love an engine that gave me control over object instantiation. 😍

rocky ridge
#

these annotation just generate a null check for you, saves code but does not solve the problem

last thistle
# rocky ridge

I know and I did that. The problem is that in my main scene tree I cannot access the respective sub nodes of my instantiated player scene which need the Tilemap. So I dragged it into my root node, that is the player or enemy. Considering the separation of concerns those root nodes won't handle the range calculation themselves, but have a child node named Armament which does that. Now the Armament script deriving from Node only needs to reference its root. But everything falls apart when those roots are of different types. 🙁 In other languages I'd make player and enemy implement an interface that provides the type declared for the root field in the Armament script. But Godot doesn't support C# interfaces. Anyway: I will write this script in GDscript using Duck Typing - I won't come closer to the interface concept.

rocky ridge
last thistle
# rocky ridge these annotation just generate a null check for you, saves code but does not sol...

Well, your annotations can cause warnings and errors in your IDE, holding you from building the project. Thats also the reason people use Javas @Overrideannotation - because it throws an error during compile time when one suddenly chooses to not inherit from a certain class anymore while still having a method initially intended to alter a parents behaviour instead of adding original functionality.