What's the proper way to keep ‌consistency‌ of variables?

I have a Fighter instance, which has a float Range property. It indicates the range of fighter’s weapon, and also the target detecting range.

Then there are two child nodes that takes this parameter: Weapon : Node who creates bullets and assign Rangeto the bullet; and TargetDetector : Area2D that search for targets. I want it to be modified so I can see the circle range of TargetDetector Area2D.

What’s the proper way to make Weapon.Range, Fighter.Range and TargetDetector.Range
all equal? I have to assign them manually, which is odd to me…

I tried the approach using getter & setter, sets child properties in setter. The problem gets to the initialization order: if the property is assigned before exported child nodes, they will be unable to get assigned and a null reference exception would be thrown. If I ignore the exception, then values would not get synced.

So…. What’s recommended?

Keep Range only in Fighter and set it to Weapon and TargetDetector in _Ready(). This avoids initialization order issues and keeps the values synced.

1 Like

Hi, maybe i dont get the whole issue, but just using Range as variable is not enough?

When I assign values in the editor it won’t get synced instantly to child nodes, until I run the game or create a new instance of it. for [Tool] _Ready runs only once as well, still not syncing them.

Simply for recording a Range variable it is enough, but I want other code to make a bunch of properties always (I want it always…) the same in multiple binded nodes, regardless of time they get synced.

Do it in a setter.

3 Likes

Oh let me talk about it more precisely.

So this is Fighter:

[Export]
public float Range {
    get => ...;
    set {
        _range = value;
        UpdateRadiusForTargetDetector();
    }
}

[Export]
public Area2D TargetDetector { get; set; }


void UpdateRadiusForTargetDetector()
{
    (TargetDetector.GetChild<CollisionShape2D>(0).Shape as CircleShape2D circleShape).Radius = Radius;
}

The problem is, on the initialization stage, it assign proeprties like

instance.Range = ...
instance.TargetDetector = ...

So the first line, since TargetDetector is not assigned, it will throw exception.
Since it throw exception, TargetDetector will finally get a value of null.

Further, if you’d like to have a null-check:

if(TargetDetector != null) UpdateRadiusForTargetDetector();

Notice that, the value is not assigned at the initialization, because TargetDetector is not assigned yet. So the setter does nothing.

Thus I have to do it again within _Ready():

public override void _Ready()
{
    // I'd like it reporting error if TargetDetector is not assigned.
    UpdateRadiusForTargetDetector();
}

Is this… Ummmmmm.. by design? Good to go?

Well assign the initial values in constructors.

1 Like

I’m confused, did you mean I need to assign TargetDetector in constructor? But it’s an Export variable recorded in the .tscn file, how do I know the exact object in constructor?

You can re-assign it to itself after you do instance.TargetDetector = ... to trigger the setter.

Oh you may have misunderstood, the code instance.TargetDetector = ... is the engine code that I suppose from where I encountered null-reference exceptions.
The code seems to be excuted when I double-click Fighter’s .tscn file and it open up in editor, then the exception comes in the output. It’s not defined by me.

So the only valid re-assign position is _OnReady(), where the engine code finish execution and come to my own code.

Then it comes to what I posted above…

What is “engine code”?

About how godot engine load .tscn and assign values into the real C# object.

Well just re-trigger the setter in _Ready() then. I don’t see where’s the problem.

Problem: It’s cumbersome… Forgive me for expecting a lazy life…

If it’s the only/official/recommended way to achieve the goal, I will take it..

What is “the goal” exactly?

To synchornize multiple properties so that when I modify the root/source property, others gets to the right value immediately.

In this post, Fighter.Range is the source. TargetDetector.Range and Weapon.Range, as they both are child node of Fighter, should be always equal to Fighter.Range. By the word always I mean it must be effective in the godot editor, when I change Fighter’s Range property in inspector, I shall see the TargetDetector : Area2D change its collsion shape size in the 2D view.

If node is not ready then the children won’t exist in the tree so their properties can’t be set. The way to do is via Fighter.Range setter. You can call IsNodeReady() in the setter and exit early if it returns false. Otherwise assign the value to dependent nodes. For this to work in the editor the script must be a tool script. If the children properties have setters, their scripts must be tools as well in order for everything to run properly in the editor.

You can force the setter execution in _Ready() if needed by doing Fighter.Range = Fighter.Range

1 Like

It sounds like you need to instantiate the TargetDetector and add it to the scene tree early, like in a constructor, and that TargetDetector.Range should be a reference that gets set to Fighter.Range, or just some reference to the Fighter to pull variables from it.

imo this sounds like a broader architectural problem, but Ive never used cs in godot tho so idk.

If its suitable, you could just make a @tool node+gdscript that updates other node’s properties on _process() to keep your fighter from being a tool but getting the updates you want. that’s a quick and dirty way to do it assuming the value doesn’t change at runtime and you exclude the tool from builds.

If you want to share the same data between different objects you should use Resources.

Have all objects reference your resources-range

1 Like