This document is about: V3 SHARED AUTHORITY
SWITCH TO

C# Bindings

The Fusion Godot SDK comes with a thin binding layer to support programming using C#.
It contains a static Fusion class, typed wrapper classes for every Fusion node and handle, C# events for signals, and enums that mirror the native constants.

This page is for developers already familiar with Fusion Godot's GDScript workflow (see the Quick Start Guide), and will only focus on what is different in C#.
The main concepts (connection, spawning, replication, RPCs) are identical - only the syntax has changed to fit C#'s coding style.

The Fusion Singleton

In GDScript, Fusion is a globally registered autoload you call directly.
In C#, Fusion is a static class in the FusionGodot namespace that forwards to the same native singleton.

All method names have changed from being snake_case to PascalCase:

C#
GDScript
Fusion.ConnectToPhoton("user_1");
Fusion.JoinOrCreateRoom("test-room");
int id = Fusion.GetLocalPlayerId();
Fusion.connect_to_photon("user_1")
Fusion.join_or_create_room("test-room")
var id = Fusion.get_local_player_id()

Signals

In C#, Fusion Godot's signals are exposed as standard C# events. As such, they can be subscribed to using += and unsubscribed from using -=.

C#
GDScript
public override void _Ready()
{
    // Connect via method group
    Fusion.PlayerJoined += OnPlayerJoined;
    
    // Connect via anonymous function
    Fusion.ConnectionFailed += err => GD.PrintErr($"connection failed: {err}");
}

private void OnPlayerJoined(int id, string userId)
{
    GD.Print($"player joined: {id}");
}
func _ready():
    # Connect via Callable
    Fusion.player_joined.connect(_on_player_joined)
    
    # Connect via anonymous function
    Fusion.connection_failed.connect(func(err): printerr("connection failed: ", err))


func _on_player_joined(id: int, user_id: String):
    print("player joined: ", id)

The same pattern applies to node wrappers.
Replicators, spawners, and other handles expose their signals as C# events as well.

Casting Nodes to Wrappers

Fusion nodes (FusionSpawner, FusionReplicator, FusionSharedReplicator, FusionServerReplicator, FusionInterestArea) are GDExtension classes. As such, using GetNode<T>() for Fusion's nodes is not possible.

To cast a Node instance to a Fusion type, use the AsX() extension method.
There is also an equivalent of GetNode<T>(NodePath) to retrieve a child node, GetX(NodePath):

Desired Node Type AsX() Method GetX() Method
FusionReplicator node.AsReplicator() node.GetReplicator(NodePath)
FusionSharedReplicator node.AsSharedReplicator() node.GetSharedReplicator(NodePath)
FusionServerReplicator node.AsServerReplicator() node.GetServerReplicator(NodePath)
FusionSpawner node.AsSpawner() node.GetSpawner(NodePath)
FusionInterestArea node.AsInterestArea() node.GetInterestArea(NodePath)
C#
GDScript
private FusionSpawner _spawner;
private FusionSharedReplicator _rep;

public override void _Ready()
{
    // Via AsX() casting
    _spawner = GetNode("FusionSpawner").AsSpawner();
    _rep = GetNode("FusionReplicator").AsSharedReplicator();

    // Via GetX() helper
    _spawner = this.GetSpawner("FusionSpawner");
    _rep = this.GetSharedReplicator("FusionReplicator");
}
@onready var _spawner: FusionSpawner = $FusionSpawner
@onready var _rep: FusionSharedReplicator = $FusionReplicator

While creating a wrapper via GetX()/AsX() is cheap, it is best to avoid re-wrapping a node repeatedly to avoid unnecessary GC pressure.
Prefer caching the wrapper in a field (as above) and reusing it whenever possible.

RPCs

RPCs work the same way as in GDScript: receiving methods use Godot's standard RPC annotation, and calls are sent through the Fusion singleton.

Two things change in C#:

  • The receiving method uses the C# [Rpc] attribute instead of the GDScript @rpc annotation.
  • The sending call can take either Node + StringName, or a C# delegate.
C#
GDScript
[Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true)]
public void TakeDamage(int amount)
{
    _health -= amount;
}

public void Attack()
{
    // Option 1: via delegate
    Fusion.Rpc(TakeDamage, 10);

    // Option 2: via Node + StringName
    Fusion.Rpc(this, "TakeDamage", 10);
}
@rpc("any_peer", "call_local")
func take_damage(amount: int):
    health -= amount


func attack():
    Fusion.rpc(take_damage, 10)

When calling an RPC by Node + StringName, consider using the automatically generated MethodName class (e.g. Player.MethodName.TakeDamage) to avoid the performance impact of a String to StringName conversion.

RPC Context And Results

Inside an RPC handler, the RPC context can be obtained exactly the same as in GDScript:

C#
GDScript
[Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true)]
public void ChatMessage(string text)
{
    FusionRpcInfo info = Fusion.GetRpcInfo(); // Can be null if not executing as an RPC
    GD.Print($"{info.Sender} says '{text}' to {info.Target}");
}
@rpc("any_peer", "call_local")
func chat_message(text: string):
    var info := Fusion.get_rpc_info()
    print(info.sender, " says '", text, "' to ", info.target)

RPC calls return a FusionRpcResult handle for optional failure handling, chainable via OnFail:

C#
GDScript
Fusion.Rpc(TakeDamage, 10)
      .OnFail(Callable.From(() => GD.Print("RPC failed")), ttl: 2.0f);
Fusion.rpc(take_damage, 10) \
      .on_fail(func(): print("RPC failed"), 2.0)

Broadcast RPCs

Just like in GDScript, Broadcast RPC receivers must be registered and unregistered via Fusion.RegisterBroadcastReceiver(this) / Fusion.UnregisterBroadcastReceiver(this).

The Escape Hatch

If a native member is not surfaced by the binding (or you want to call something the typed API does not cover), you can obtain the native object for a given Fusion type directly using the Self member:

  • wrapper.Self - the underlying GodotObject, for raw Call(...) / Connect(...).
  • wrapper.IsValid - true while the wrapped native instance is still alive.
  • Fusion.Singleton - the native Fusion singleton object.

C#

// Anything the typed surface omits is still reachable:
_sharedReplicator.Self.Call("some_native_method", arg);

Quick Start Guide Example

The two scripts below are the C# equivalent of the Quick Start Guide (Shared Authority)'s main scene and player scene.

Main.cs - connect, join, spawn:

C#

using Godot;
using FusionGodot;

public partial class Main : Node2D
{
    private FusionSpawner _spawner;
    private Button _connectButton;

    public override void _Ready()
    {
        _spawner = this.GetSpawner("FusionSpawner");
        _spawner.AddSpawnableScene(GD.Load<PackedScene>("res://player.tscn"));

        _connectButton = GetNode<Button>("UIConnectButton");

        Fusion.RoomJoined += OnRoomJoined;
        _connectButton.Connect(Button.SignalName.Pressed, Callable.From(ConnectAndJoinRoom));
    }

    private void OnRoomJoined()
    {
        Node player = _spawner.Spawn();
        if (player is Node2D node2d)
            node2d.Position = new Vector2(GD.RandRange(100, 700), GD.RandRange(100, 500));
    }

    private void ConnectAndJoinRoom()
    {
        GD.Print("clicked connect");
        var userId = "user_" + GD.Randi();
        
        Fusion.ConnectToPhoton(userId);
        Fusion.ConnectedToPhoton += () =>
        {
            GD.Print($"trying to join/create room as user: {userId}");
            Fusion.JoinOrCreateRoom();
        };
    }
}

Player.cs - state-authority based movement plus an RPC:

C#

using Godot;
using FusionGodot;

public partial class Player : Node2D
{
    private const float Speed = 200f;

    private FusionSharedReplicator _sync;

    public override void _Ready()
    {
        _sync = this.GetSharedReplicator("FusionSharedReplicator");
        GetNode<Sprite2D>("Sprite2D").Modulate = _sync.HasAuthority() ? Colors.Green : Colors.Red;
    }

    public override void _Process(double delta)
    {
        if (_sync.HasAuthority())
        {
            Vector2 dir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
            Position += dir * Speed * (float)delta;
        }
    }

    public override void _Input(InputEvent ev)
    {
        if (ev is InputEventKey keyEvent && keyEvent.Pressed && !keyEvent.IsEcho() && keyEvent.Keycode == Key.E)
        {
            SendMessage();
        }
    }

    private void SendMessage()
    {
        Fusion.Rpc(Rpc_SendMessage, "Hello!");
    }

    [Rpc(MultiplayerApi.RpcMode.Authority, CallLocal = true)]
    public void Rpc_SendMessage(string text)
    {
        GetNode<Label>("MessageLabel").Text = text;
    }
}
Back to top