mirror of
https://github.com/godotengine/godot-docs.git
synced 2025-12-31 17:49:03 +03:00
Add C# examples to High-level Multiplayer documentation
Co-authored-by: Raul Santos <raulsntos@gmail.com>
This commit is contained in:
@@ -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()
|
||||
@@ -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``
|
||||
@@ -159,16 +184,27 @@ 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
|
||||
----------------------
|
||||
|
||||
@@ -176,7 +212,8 @@ Remote procedure calls, or RPCs, are functions that can be called on other peers
|
||||
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():
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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``:
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
-------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user