godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.34k stars 21.25k forks source link

Value Initialization in both C# and GDScript only work when using the [Tool] Attribute on classes that derive from any Godot Engine Type (e.g Node, Resource) #80864

Open TruelyMostWanted opened 1 year ago

TruelyMostWanted commented 1 year ago

Godot version

4.2-dev3

System information

Godot v4.2.dev3.mono - Windows 10.0.19045 - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 3080 (NVIDIA; 31.0.15.3623) - AMD Ryzen 7 3800X 8-Core Processor (16 Threads)

Issue description

I'm not sure if this "by design", a bug or still open for discussion, but is the following true about value initialization?

@export var vec : Vector3;

func _init(): vec = Vector3(1.0,2.0,3.0);


In C# -> TestNode.cs
```cs
[Tool]
public partial class TestNode : Node
{
    public Vector3 Vec;

    public TestNode()
    {
        Vec = new Vector3(1.0f, 2.0f, 3.0f);
    }
}

Steps to reproduce

Take any project, and copy these steps:

(1) Open up any Godot version from the 4.X releases or dev-snapshots

(2) Create a .cs or .gd file and copy the following code GDScript -> testnode.gd

@tool
extends Node

@export var vec : Vector3;

func _init():
    vec = Vector3(1.0,2.0,3.0);

In C# -> TestNode.cs

[Tool]
public partial class TestNode : Node
{
    public Vector3 Vec;

    public TestNode()
    {
        Vec = new Vector3(1.0f, 2.0f, 3.0f);
    }
}

(3) Add a simple "Node" and attach the script to it. You'll see the values are there.

(4) Remove the [Tool] or (at)tool, Save the script.

(5) Remove the script from the "Node" object

(6) Re-add the script to the Node. The value wont update

Minimal reproduction project

(see steps above)

raulsntos commented 1 year ago

Scripts that aren't marked as [Tool] are not instantiated in the editor, only a placeholder gets created so it can store the properties' values edited through the inspector.

This behavior is by design, and I think it's somewhat explained in the documentation (see the Running code in the editor documentation page), but it could be made more prominent because it seems a lot of users miss this.

Since the type is not instantiated, the constructor won't be invoked.

In 3.x, C# used to invoke the constructor in order to retrieve the default values for properties but this caused issues since constructing the type had undesired side-effects and users complained about it (see https://github.com/godotengine/godot/issues/40970). AFAIK GDScript never had this behavior.

TruelyMostWanted commented 1 year ago

Looking at it by saying that this logic is code that runs in the editor makes sense. But yeah true. It definitly needs to be cleared up somewhere.

people struggle with this as early as 2018 / Godot 3.X, see #22633 (still open til today) And the common misconception with the "undesired side-effects" is interesting. Therefore a certain understanding of the life cycle of Godot Objects and Logic should be prominent.

Let me explain how my misconception was created:

And then i went here and created that issue with that long title above.

Also a Use-Case that came to mind:

This way you can save like 7 out of 9 clicks

raulsntos commented 1 year ago

So i thought: If in GDScript people are able to perform value initialization via _init(), then i need to do the same thing in C# using a default constructor on the object.

This is correct. The GDScript _init method is the constructor. C# does not have a _Init method, you should use the constructor.

When using Colliders or Areas for example , you can give them a CollisionShape3D/2D below and they have a Shape Property [...] and once you create that object, it seems to have some default values like the Cube Shape being automatically 1x1x1. [...] Why does mine need to be a editor class to perform that logic?

The built-in types behave as Tool scripts, their code is always executed in the editor.

In C#, you can also define the default value of your properties by using inline initialization which doesn't require the script to be [Tool]. There are some limitations to this, so I recommend to create the default value in a method and call it in the inline initialization.

public partial class MyResource : Resource
{
    public MyEnum { A, B, C }

    [Export] public int MyNumber { get; set; } = 42;
    [Export] public string MyString { get; set; } = "Hello, World!";
    [Export] public MyEnum[] MyEnumArray { get; set; } = GetMyEnumArrayDefault();

    private static MyEnum[] GetMyEnumArrayDefault()
    {
        var values = Enum.GetValues<MyEnum>();
        return values.Select(value => value).ToArray();
    }
}
TruelyMostWanted commented 1 year ago

Okay the fact that the Godot types and classes have the [Tool] attribute is the final puzzle piece i was missing! Because it really did not feel like editor code at all! Thanks for the clear up!

Regarding ways of how to initialize values, everyone has a different preference. I personally like the seperation for non-static members: declaring variables/properties at the top and then assigning their values where the object gets created (constructor). On the static (readonly) or const side of things, i'm forced to instantly declare and define variables. So i do it there.

So it really comes down to what one wants

public partial class MyResource : Resource 
{
    public int MyValue = 42;
}
public partial class MyResource : Resource 
{
    public int MyValue;

    public MyResource()
    {
        MyValue = 42
    }
}