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:
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 -=.
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) |
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@rpcannotation. - The sending call can take either Node + StringName, or a C# delegate.
[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:
[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:
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 underlyingGodotObject, for rawCall(...)/Connect(...).wrapper.IsValid-truewhile the wrapped native instance is still alive.Fusion.Singleton- the nativeFusionsingleton 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