Skip to content

Commit

Permalink
Add C# examples to High-level Multiplayer documentation
Browse files Browse the repository at this point in the history
Co-authored-by: Raul Santos <[email protected]>
  • Loading branch information
ShawnHardern and raulsntos committed Aug 23, 2024
1 parent 5dd72ba commit c4a17ab
Showing 1 changed file with 258 additions and 11 deletions.
269 changes: 258 additions & 11 deletions tutorials/networking/high_level_multiplayer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,24 @@ which will override ``multiplayer`` for the node at that path and all of its des
This allows sibling nodes to be configured with different peers, which makes it possible to run a server
and a client simultaneously in one instance of Godot.

::
.. tabs::
.. code-tab:: gdscript GDScript

# By default, these expressions are interchangeable.
multiplayer # Get the MultiplayerAPI object configured for this node.
get_tree().get_multiplayer() # Get the default MultiplayerAPI object.

.. code-tab:: csharp

// By default, these expressions are interchangeable.
Multiplayer; // Get the MultiplayerAPI object configured for this node.
GetTree().GetMultiplayer(); // Get the default MultiplayerAPI object.

To initialize networking, a ``MultiplayerPeer`` object must be created, initialized as a server or client,
and passed to the ``MultiplayerAPI``.

::
.. tabs::
.. code-tab:: gdscript GDScript

# Create client.
var peer = ENetMultiplayerPeer.new()
Expand All @@ -128,12 +136,29 @@ and passed to the ``MultiplayerAPI``.
peer.create_server(PORT, MAX_CLIENTS)
multiplayer.multiplayer_peer = peer

.. code-tab:: csharp

// Create client.
var peer = new ENetMultiplayerPeer();
peer.CreateClient(IPAddress, Port);
Multiplayer.MultiplayerPeer = peer;

// Create server.
var peer = new ENetMultiplayerPeer();
peer.CreateServer(Port, MaxClients);
Multiplayer.MultiplayerPeer = peer;

To terminate networking:

::
.. tabs::
.. code-tab:: gdscript GDScript

multiplayer.multiplayer_peer = null

.. code-tab:: csharp

Multiplayer.MultiplayerPeer = null;

.. warning::

When exporting to Android, make sure to enable the ``INTERNET``
Expand All @@ -159,24 +184,36 @@ The rest are only emitted on clients:

To get the unique ID of the associated peer:

::
.. tabs::
.. code-tab:: gdscript GDScript

multiplayer.get_unique_id()

.. code-tab:: csharp

Multiplayer.GetUniqueId();


To check whether the peer is server or client:

::
.. tabs::
.. code-tab:: gdscript GDScript

multiplayer.is_server()

.. code-tab:: csharp

Multiplayer.IsServer();

Remote procedure calls
----------------------

Remote procedure calls, or RPCs, are functions that can be called on other peers. To create one, use the ``@rpc`` annotation
before a function definition. To call an RPC, use ``Callable``'s method ``rpc()`` to call in every peer, or ``rpc_id()`` to
call in a specific peer.

::
.. tabs::
.. code-tab:: gdscript GDScript

func _ready():
if multiplayer.is_server():
Expand All @@ -186,6 +223,23 @@ call in a specific peer.
func print_once_per_client():
print("I will be printed to the console once per each connected client.")

.. code-tab:: csharp

public override void _Ready()
{
if (Multiplayer.IsServer())
{
Rpc(MethodName.PrintOncePerClient);
}
}

[Rpc]
private void PrintOncePerClient()
{
GD.Print("I will be printed to the console once per each connected client.");
}


RPCs will not serialize objects or callables.

For a remote call to be successful, the sending and receiving node need to have the same ``NodePath``, which means they
Expand All @@ -204,7 +258,7 @@ must have the same name. When using ``add_child()`` for nodes which are expected
**and** the NodePath. If an RPC resides in a script attached to ``/root/Main/Node1``, then it
must reside in precisely the same path and node on both the client script and the server
script. Function arguments are not checked for matching between the server and client code
(example: ``func sendstuff():`` and ``func sendstuff(arg1, arg2):`` **will pass** signature
(example: ``func sendstuff():`` and ``func sendstuff(arg1, arg2):`` **will pass** signature
matching).

If these conditions are not met (if all RPCs do not pass signature matching), the script may print an
Expand All @@ -215,10 +269,15 @@ must have the same name. When using ``add_child()`` for nodes which are expected

The annotation can take a number of arguments, which have default values. ``@rpc`` is equivalent to:

::
.. tabs::
.. code-tab:: gdscript GDScript

@rpc("authority", "call_remote", "unreliable", 0)

.. code-tab:: csharp

[Rpc(MultiplayerApi.RpcMode.Authority, CallLocal = false, TransferMode = MultiplayerPeer.TransferModeEnum.Unreliable, TransferChannel = 0)]

The parameters and their functions are as follows:

``mode``:
Expand All @@ -243,7 +302,8 @@ The first 3 can be passed in any order, but ``transfer_channel`` must always be

The function ``multiplayer.get_remote_sender_id()`` can be used to get the unique id of an rpc sender, when used within the function called by rpc.

::
.. tabs::
.. code-tab:: gdscript GDScript

func _on_some_input(): # Connected to some input.
transfer_some_input.rpc_id(1) # Send the input only to the server.
Expand All @@ -256,6 +316,22 @@ The function ``multiplayer.get_remote_sender_id()`` can be used to get the uniqu
var sender_id = multiplayer.get_remote_sender_id()
# Process the input and affect game logic.

.. code-tab:: csharp

private void OnSomeInput() // Connected to some input.
{
RpcId(1, MethodName.TransferSomeInput); // Send the input only to the server.
}

// Call local is required if the server is also a player.
[Rpc(MultiplayerApi.RpcMode.AnyPeer, CallLocal = true, TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
private void TransferSomeInput()
{
// The server knows who sent the input.
int senderId = Multiplayer.GetRemoteSenderId();
// Process the input and affect game logic.
}

Channels
--------
Modern networking protocols support channels, which are separate connections within the connection. This allows for multiple
Expand All @@ -276,7 +352,8 @@ Example lobby implementation
This is an example lobby that can handle peers joining and leaving, notify UI scenes through signals, and start the game after all clients
have loaded the game scene.

::
.. tabs::
.. code-tab:: gdscript GDScript

extends Node

Expand Down Expand Up @@ -388,9 +465,159 @@ have loaded the game scene.
players.clear()
server_disconnected.emit()

.. code-tab:: csharp

using Godot;

public partial class Lobby : Node
{
public static Lobby Instance { get; private set; }

// These signals can be connected to by a UI lobby scene or the game scene.
[Signal]
public delegate void PlayerConnectedEventHandler(int peerId, Godot.Collections.Dictionary<string, string> playerInfo);
[Signal]
public delegate void PlayerDisconnectedEventHandler(int peerId);
[Signal]
public delegate void ServerDisconnectedEventHandler();

private const int Port = 7000;
private const string DefaultServerIP = "127.0.0.1"; // IPv4 localhost
private const int MaxConnections = 20;

// This will contain player info for every player,
// with the keys being each player's unique IDs.
private Godot.Collections.Dictionary<long, Godot.Collections.Dictionary<string, string>> _players = new Godot.Collections.Dictionary<long, Godot.Collections.Dictionary<string, string>>();

// This is the local player info. This should be modified locally
// before the connection is made. It will be passed to every other peer.
// For example, the value of "name" can be set to something the player
// entered in a UI scene.
private Godot.Collections.Dictionary<string, string> _playerInfo = new Godot.Collections.Dictionary<string, string>()
{
{ "Name", "PlayerName" },
};

private int _playersLoaded = 0;

public override void _Ready()
{
Instance = this;
Multiplayer.PeerConnected += OnPlayerConnected;
Multiplayer.PeerDisconnected += OnPlayerDisconnected;
Multiplayer.ConnectedToServer += OnConnectOk;
Multiplayer.ConnectionFailed += OnConnectionFail;
Multiplayer.ServerDisconnected += OnServerDisconnected;
}

private Error JoinGame(string address = "")
{
if (string.IsNullOrEmpty(address))
{
address = DefaultServerIP;
}

var peer = new ENetMultiplayerPeer();
Error error = peer.CreateClient(address, Port);

if (error != Error.Ok)
{
return error;
}

Multiplayer.MultiplayerPeer = peer;
return Error.Ok;
}

private Error CreateGame()
{
var peer = new ENetMultiplayerPeer();
Error error = peer.CreateServer(Port, MaxConnections);

if (error != Error.Ok)
{
return error;
}

Multiplayer.MultiplayerPeer = peer;
_players[1] = _playerInfo;
EmitSignal(SignalName.PlayerConnected, 1, _playerInfo);
return Error.Ok;
}

private void RemoveMultiplayerPeer()
{
Multiplayer.MultiplayerPeer = null;
}

// When the server decides to start the game from a UI scene,
// do Rpc(Lobby.MethodName.LoadGame, filePath);
[Rpc(CallLocal = true,TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
private void LoadGame(string gameScenePath)
{
GetTree().ChangeSceneToFile(gameScenePath);
}

// Every peer will call this when they have loaded the game scene.
[Rpc(MultiplayerApi.RpcMode.AnyPeer,CallLocal = true,TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
private void PlayerLoaded()
{
if (Multiplayer.IsServer())
{
_playersLoaded += 1;
if (_playersLoaded == _players.Count)
{
GetNode<Game>("/root/Game").StartGame();
_playersLoaded = 0;
}
}
}

// When a peer connects, send them my player info.
// This allows transfer of all desired data for each player, not only the unique ID.
private void OnPlayerConnected(long id)
{
RpcId(id, MethodName.RegisterPlayer, _playerInfo);
}

[Rpc(MultiplayerApi.RpcMode.AnyPeer,TransferMode = MultiplayerPeer.TransferModeEnum.Reliable)]
private void RegisterPlayer(Godot.Collections.Dictionary<string, string> newPlayerInfo)
{
int newPlayerId = Multiplayer.GetRemoteSenderId();
_players[newPlayerId] = newPlayerInfo;
EmitSignal(SignalName.PlayerConnected, newPlayerId, newPlayerInfo);
}

private void OnPlayerDisconnected(long id)
{
_players.Remove(id);
EmitSignal(SignalName.PlayerDisconnected, id);
}

private void OnConnectOk()
{
int peerId = Multiplayer.GetUniqueId();
_players[peerId] = _playerInfo;
EmitSignal(SignalName.PlayerConnected, peerId, _playerInfo);
}

private void OnConnectionFail()
{
Multiplayer.MultiplayerPeer = null;
}

private void OnServerDisconnected()
{
Multiplayer.MultiplayerPeer = null;
_players.Clear();
EmitSignal(SignalName.ServerDisconnected);
}
}

The game scene's root node should be named Game. In the script attached to it:

::
.. tabs::
.. code-tab:: gdscript GDScript

extends Node3D # Or Node2D.

Expand All @@ -406,6 +633,26 @@ The game scene's root node should be named Game. In the script attached to it:
func start_game():
# All peers are ready to receive RPCs in this scene.

.. code-tab:: csharp

using Godot;

public partial class Game : Node3D // Or Node2D.
{
public override void _Ready()
{
// Preconfigure game.

Lobby.Instance.RpcId(1, Lobby.MethodName.PlayerLoaded); // Tell the server that this peer has loaded.
}

// Called only on the server.
public void StartGame()
{
// All peers are ready to receive RPCs in this scene.
}
}

Exporting for dedicated servers
-------------------------------

Expand Down

0 comments on commit c4a17ab

Please sign in to comment.