Change UI from background thread

Godot Version

4.2

Question

I created a singleton that holds a map<enum, Action>

broadcast(enum) {
    map[enum]()
}

//from anywhere in my code i do:
singleton.broadcast(enum)

I then use this elsewhere in my codebase using:

Ready {
    singleton.subscribe(enumEvent, someFunctionName)
}

void method someFunctionName() {
    label.Text = abc
}

I am getting the error:

This function in this node (/root/Main/Label) can only be accessed from either the main thread or a thread group. Use call_deferred() instead

How can i update the UI from the background thread?

Is there a way to do it in the function someFunctionName?

CallDeferred is definitely the way to go. This is the Thread Safe API call for executing methods in Godot. Although the examples are with gdscript it is applicable in C# as well, but you can read more about it here: Thread-safe APIs — Godot Engine (stable) documentation in English

In case you want a different thread to be running and you managing it, you could just go the ordinary C# way about it by creating a Thread/Task.

My first time using C#, what exactly would be the syntax?

My current code is relying heavily on the singleton.subscrube(enum, someFunctionName)

Ideally i would love a solution like this:

void someFunctionName() {
    callDeffered() => {
        Label.Text = "abc"
    }
}

Is it possible to use CallDeffered like that?

Is there a way to use it with a C# Action?

CallDeferred can only take a Variant as an argument.

What do you mean by this?

if i really needed a system action, i would call a function and pass the action as an argument, then invoke the action from within that function.

Something like this?

   public void test(Enum enum, System.Action action) {
        action.Invoke();
    }

How would i couple this CallDeferred?

Something like this?

CallDeferred("test", arg1, arg2);

Issue with this i cant pass an object as a parameter in arg1. I then cast that object to the respective type later on.

Recently I had to implement async calls in my game as well, so I will give you a snippet of what I did and I think it would also work for you.

So you make your async method first:

public async void LoginAsync(string emailAddress, string password, Action<LoginResult> LoggedInAction)
{
    var result = await SomeLoginMethod(emaiAddress, password);
    LoggedInAction(result);
}

And then you call it like that:

public void StartClient()
{
    Callable.From(() => Client.LoginAsync(EmailField?.Text, PasswordField?.Text, OnLogin)).CallDeferred();
    JoinServer(ServerAddress, ServerPort, ConnectionCompression);
}

Here is the OnLogin method:

 private void OnLogin(LoginResult result)
 {
     Rpc(1, nameof(SendPlayerDataToServer), result.PlayerId, result.Username);
 }

And some context on what is happening here:
When a player wants to login, we send an async call to Login with some params and we also give it a callback method (OnLogin) to be executed when the login has finished. The client connects to the game server and sends some info about itself to the server. When the server recieves the info about the player, verifies it and if valid then runs the game world scene for the player and adds it for other players as well.

Something nice i figured out of just experimenting is that when you use the Callable.From() to call such methods you can pass whatever you want, it does not have to be Godot Variant.

@kkalinovk

That was the trick. Thanks for the help everyone!

1 Like

I have further experimented with this pattern and there is one thing you should keep in mind when using this approach - the method that you call with Callable.From() should return void or Variant, so it only does work because the method is async void. AFAIK this would not be an issue in a godot project, but making async methods return void instead of Task is something that is advised NOT being done in C# ^^.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.