Răsfoiți Sursa

Add C# examples to High-level Multiplayer documentation

Co-authored-by: Raul Santos <[email protected]>
Shawn Hardern 11 luni în urmă
părinte
comite
f975e49bd7
1 a modificat fișierele cu 258 adăugiri și 11 ștergeri
  1. 258 11
      tutorials/networking/high_level_multiplayer.rst

+ 258 - 11
tutorials/networking/high_level_multiplayer.rst

@@ -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
 -------------------------------