Why does godot use magic strings instead of constants/enums/auto-generated properties?

Godot Version

4.2

Question

Hello, I’m a web developer used to working with the .NET Core framework and I occasionally try out one game engine or another out of curiosity.

I’m currently going through godot’s documentation before testing it, and I don’t understand why godot - and most game engines - use magic strings where they don’t belong.

First example:

var direction = 0
if Input.is_action_pressed("ui_left"):
	direction = -1
if Input.is_action_pressed("ui_right"):
	direction = 1

rotation += angular_speed * direction * delta

I guess in GDScript auto-completion works for “ui_left” or “ui_right”, but is this also the case in Visual Studio (or VS Code)?
And above all, why a magic string?

Second example, in internalization:

level.text = tr("LEVEL_5_NAME")

Here again, why a magic string and not a class property (something similar to .resx in the .NET framework)?

I’ve often asked myself this question without finding the answer… thanks in advance!

1 Like

i found this, maybe the reason why string for the parameter? String — Godot Engine (stable) documentation in English.

Strings are reference-counted and use a copy-on-write approach (every modification to a string returns a new String ), so passing them around is cheap in resources.

you might want the devs to answer this too

The names of Input actions are user-configurable. This flexibility can’t be achieved with constants or enums.

2 Likes

Actually, you are right.

What about .NET’s .resx system, which automatically generates public properties when you add, update or delete key-value pairs, so that you can, if necessary, access them the hard way if you want a specific culture:

var enText = ResourcesClass.ResourceManager.GetString(
    nameof(ResourcesClass.YourText),
    new CultureInfo("en-US"));
// You dont use "en-US" as a string but any form of constant obviously

Or in the current users language:
var text = ResourcesClass.YourText;

This way, the string keys are used as properties instead of the magic string.

Again, I haven’t tried this yet, but I’m very skeptical about the usefulness of autocompletion when using godot in VS or VSC with these strings.

You could generate one if you feel like it. Quick version using an EditorScript that generates a script with the input map:

@tool
extends EditorScript


func _run() -> void:
	var inputs = []

	for prop in ProjectSettings.get_property_list():
		var prop_name:String = prop.get("name", "")
		if prop_name.begins_with('input/'):
			prop_name = prop_name.replace('input/', '')
			prop_name = prop_name.substr(0, prop_name.find("."))
			if not inputs.has(prop_name):
				inputs.append(prop_name)

	var inputs_string = "\n".join(inputs.map(func(input): return 'static var {name}:StringName = &"{name}"'.format({"name": input})))

	var script_content = \
"""
class_name InputActions extends RefCounted

{inputs}

""".format({"inputs": inputs_string})

	print(script_content)

	var script = GDScript.new()
	script.source_code = script_content
	ResourceSaver.save(script, 'res://input_actions.gd')

It generates a script like:

class_name InputActions extends RefCounted

static var ui_accept:StringName = &"ui_accept"
static var ui_select:StringName = &"ui_select"
static var ui_cancel:StringName = &"ui_cancel"
# more input actions here

I have not tested this but you could probably create a plugin that connects to ProjectSettings.settings_changed signal and re-generates the script.

2 Likes

This doesn’t matter for performance as long as ReferenceEquals is used to check equality; it’s as fast as checking an enum.

However, InputMap appears to allocate a new StringName object whenever it’s used. That means even if it uses ReferenceEquals underneath to do the check as fast as possible, it’s still allocating new stuff on the heap that has to be garbage collected. That will impact performance by increasing micro-pauses in the CPU as the game runs.

Here’s some code where I try caching the StringName object from the InputMap, so I can compare the references directly.

global using Godot;
global using System;
global using System.Collections.Generic;
global using System.Linq;

public partial class Universe : Node3D
{
    public const string DOIT = "doit";
    public StringName CachedDoIt { get; private set; }

    public override void _Ready()
    {
        CachedDoIt = InputMap.GetActions().First(a => a == DOIT);
        GD.Print($"input map ref saved: {CachedDoIt}");
    }

    public override void _Input(InputEvent @event)
    {
        if (Input.IsActionJustPressed(CachedDoIt))
        {
            StringName fromInputMap = InputMap.GetActions().First(a => a == DOIT);
            bool areTheSameObject = ReferenceEquals(CachedDoIt, fromInputMap);

            GD.Print($"Is the StringName from InputMap the same as the one I cached? {areTheSameObject}"); // false
            GD.Print($"Do they evaluate as equal? {fromInputMap == CachedDoIt}"); // true

            // .Net makes sure the references are the same so equality checks are instant
            GD.Print($"Is the .Net const the same object as a magic string? {ReferenceEquals(DOIT, "doit")}"); // true
        }
    }
}

Output:
image

1 Like

Personally, I cache them like this:

public static class InputAction
{
    public static StringName Thrust { get; private set; } = "thrust";
    public static StringName Left { get; private set; } = "left";
    public static StringName Right { get; private set; } = "right";

    public static float GetXInput()
    {
        return Input.GetActionStrength(Right) - Input.GetActionStrength(Left);
    }
}

And here’s how I use them:

    public override void _Input(InputEvent input)
    {
        if (Input.IsActionJustPressed(InputAction.Thrust))
        {
            boosterParticles.Emitting = applyThrust = true;
            engineAudio.Play();
        }
    }
2 Likes

It does auto complete in vs code.

Honestly, it was just probably not a priority. I do see fewer magic strings in v4 compared to code examples from v3, so it’s moving in the right direction.

But you’re 100% right - all magic strings should ideally be eliminated. I can definitely see maaaany other features being a far-higher priority, so maybe it’s just a matter of time allocation / a hero volunteer rising to the cause.

As far as micro-stutters, the solution is now and always will be to simply not use C# for making video games :stuck_out_tongue_winking_eye: You can learn a more suitable language using the time you’ll spend anyway on optimizing around garbage collector.