Hello there!

I'm a Unity dev working for a small indie game studio. After Unity's recent announcement, we're considering switching engines. Since we mainly make 2D games, Godot might be a good choice.

The only thing that makes us hesitate is the fact that in Godot a lot seems to be solved via magic strings. GetNode<CollisionShape2D>("CollisionShape2D") for example or var map = load("res://Scenes/Maps/MapaDePrueba.tscn").instance(). Why are strings used here? This produces errors that can only be discovered at runtime which leads to very brittle code.
What if the resource path changes? What if desigers change the node name or the structure of a node tree?

So my question is: How refactor-friendly is Godot really? Is it possible that larger projects quickly become unmanageable?

I would prefer an honest answer to a biased one, because a lot depends on the decision to change the engine. Thank you very much!

    In GDScript instead of writing get_node("MyMovementComponent") you can just write $MyMovementComponent. If people do this a lot then convention is usually to do something like:

    @onready var myMovementComponent: FancyMovementComponent = $MyMovementComponent
    
    func do_something():
        myMovementComponent.whatever()

    That way you don't really use strings.

    Also you can of course always define the strings as constants, meaning you write them only once.

    MeisterDerMagie var map = load("res://Scenes/Maps/MapaDePrueba.tscn").instance(). Why are strings used here?

    How else would you refer to a resource stored on the disk other than using a path string? If you use preload() instead of load() the loaded resource object becomes a constant that is evaluated at compile time. Try it out, the editor will catch all bad paths as soon as you type them.

    As for node paths, you can adjust the system to be completely indifferent to node names. But, like it or not, you'll still have to deal with the tree structure, as everything that exists in a Godot scene must be part of that structure.

    I guess changing your thinking form ECS-like system to node based system will take a bit of getting used to.

      xyz How else would you refer to a resource stored on the disk other than using a path string?

      I also feel uneasy when using disk paths to load resources. It should not be this way. Every resource should have an internal identifier disconnected from its path in the disk. In a way that if the path is changed the reference will still remain.

      It is very common to make assets disk relocation for reorganization purposes. And not the editor, nor the compiler, will complain about lost references until runtime.

      Hard-coded explicit disk paths in the code should be identified as a bad practice. IMHO

      • xyz replied to this.

        It is true that loading resources by path is very common in tutorials and even in the documentation. I try to avoid it as hell, for the reasons you mentioned.

        I use exports. You can export any kind of Resources. Also export packed scenes.

        Then in the editor, you can click and drag the Resource or the Scene in the exported var. When connections are made this way, and you reorganize the files (using the Godot Editor File Explorer) the connections will be updated as well.

        d2clon Well, someone needs to specify the path somewhere. If you'd like the engine to abstract this away for you, fair point. In a serious project you wouldn't use hardcoded paths scattered across the code like they show in quick demos. You'd write a small management system that works directly with ResourceLoader singleton, or uses references/exports as you mention.

        Exports seem to be a good solution. In Unity I'm able to assign references (exports) automatically by script at editor time. Is this possible in Godot aswell?
        Example: I have a script that needs a reference to a Sprite2D which sits in one of its children. It's easy to forget assigning this reference in the inspector using drag and drop. Is it possible to have the script fetch this reference automatically using something like (pseudo c# code):

        [Export] Sprite2D spriteNode;
        private override void OnAddScript()
        {
        spriteNode = GetNodeInChildren<Sprite2D>();
        }

          Yes, you typically do this in node's overridden _Ready() callback which executes exactly once when node (and all of its children) are added to the scene tree. In GDSscript there's a neat syntactical sugar for this in form of @onready annotation, used as show by @Toxe above. Doing this outside of runtime in the editor wouldn't make much sense, except for instanced scenes in which case you can use use [Tool] attribute to make the script run in the editor.

          MeisterDerMagie Example: I have a script that needs a reference to a Sprite2D which sits in one of its children

          Yeah, this one is easy and very common. If the Node you want to reference is part of the Scene then you can use Scene Unique Nodes.

          xyz As for node paths, you can adjust the system to be completely indifferent to node names. But, like it or not, you'll still have to deal with the tree structure, as everything that exists in a Godot scene must be part of that structure.

          Unless they write their own custom main loop!

          • xyz replied to this.

            Megalomaniak I'd love to see someone actually build a game with a custom loop. I guess in that case nodes would indeed completely go out of the window and the only way to do stuff would be via calls to Godot's server singletons (like DisplayServer etc).

            Hmm... this way maybe the whole Unity could be recreated inside Godot 😃
            Looks like as of recent, a team of eager volunteers for this endeavor could be easily assembled, from the large pool of disgruntled Unity rejects 😉

            MeisterDerMagie The only thing that makes us hesitate is the fact that in Godot a lot seems to be solved via magic strings.

            That was a major shortcoming a few years ago; now (especially in 4.x) there are enums and such. I suggest you do some quick stress-tests to evaluate your performance/usability concerns.

            My main complaints are the GUI system, 3D/physics performance, and instability in every sense of the word. If the instability persists in 4.2 I'm going to evaluate other engines again, and if it continues into 4.3 I'm switching.

            Toxe I am not the expert on this but you can define tool scripts that run inside the editor. Maybe you can achieve this with a tool script.

            I'm getting pretty comfortable with tool scripts and editor plugins. You don't need them for what @d2clon is talking about. Just put @export var asdf:Resource in a script attached to a node, click on the node, you'll see an Asdf property at the top of your inspector panel, and you can simply drag any .res/.tres file onto it.

            I don't actually use this much if at all; it has some downsides. I generally store the pathname and load() it as needed.

              synthnostate My main complaints are the GUI system, 3D/physics performance, and instability in every sense of the word. If the instability persists in 4.2 I'm going to evaluate other engines again, and if it continues into 4.3 I'm switching.

              If you have a product to ship in the foreseeable future, it's probably best to stick to 3.5

              MeisterDerMagie What if the resource path changes?

              Why would it change?

              synthnostate If the instability persists in 4.2 I'm going to evaluate other engines again, and if it continues into 4.3 I'm switching.

              The instability is likely to persist longer. The current concept of rapid development does not allow you to expect thorough "work on bugs". The near future is likely to be hard for the engine.

              I suppose I am not doing anything anyone would consider fancy or complicated because I haven't really noticed any stability issues that people seem to talk about since I am using 4.1.1 other than renaming files/scenes which can be a bit wonky.

              synthnostate My main complaints are the GUI system, 3D/physics performance, and instability in every sense of the word.

              What exactly do you mean with instability? Instability of the editor application or of the game build?

                One thing I don't think Godot's documentation makes obvious enough for newbies:

                Strings are often not just Strings! Often times you will come across examples of functions being called via String, when really what the function takes is a StringName or a NodePath

                StringName is used commonly in places like Input where you want to compare strings.

                var did_jump = Input.is_action_pressed("jump")

                This code is actually using the String "jump" to construct a StringName. Each StringName points to the same location, so two StringNames can be compared for equality far faster than normal Strings can. However, creating a StringName from a String is itself kinda slow. A good practice is to cache your StringNames so you only construct them once. Here's one way:

                @export var jump_str := StringName("jump")
                #later...
                var did_jump = Input.is_action_pressed(jump_str)

                This way all the engine is essentially doing is checking an int comparison!

                NodePath is used by functions like get_node.

                Calling get_node with a String will construct a NodePath with the String. To make things quicker and less prone to error, one thing you can do is exporting the NodePath.

                @export var path_to_some_node:NodePath

                This will appear in the inspector as a field you can drag and drop another node into, and if that Node's name or location changes, the NodePath will update automatically.

                Resources in the filesystem don't have their own unique kind of String, but you can still get similar automatic behavior using the export alternatives @export_file and @export_dir. When the resources are moved within Godot, the exported Strings will be updated accordingly. If something goes wrong, say for instance the files get moved in Git but the references weren't updated, Godot can try to automatically repair the broken paths.

                  One final thing though, as to the question

                  "How refactor-friendly is Godot really?"

                  Less friendly than Unity. GDScript doesn't have great refactoring support. C# and C++ are much better in that regard. Certainly with Strings, sometimes references can break, even if you're careful, and if you have a large project it can be difficult to refactor all the uses of a particular string. Personally I like to overcome this by keeping my strings in one place and then referencing those variables. For example, my Config class contains StringName variables for every InputAction I have named, and I can access those from anywhere.

                  award This code is actually using the String "jump" to construct a StringName. Each StringName points to the same location, so two StringNames can be compared for equality far faster than normal Strings can. However, creating a StringName from a String is itself kinda slow. A good practice is to cache your StringNames so you only construct them once. Here's one way:

                  @export var jump_str := StringName("jump")
                  #later...
                  var did_jump = Input.is_action_pressed(jump_str)

                  This way all the engine is essentially doing is checking an int comparison!

                  Yup, compilers and interpreters use this method heavily, they usually call them "interned strings".

                  There's a stringname literal syntax (at least in 4.x), you just put a & in front of the string. Should be just as efficient.

                  var did_jump = Input.is_action_pressed(&"jump")