Reintroduce proper multiplayer

- Can now re-open singleplayer server
  so other players can join over internet
- Multiplayer menu supports all this
- Fix issue where players would also
  send other players' position as RPCs
main
copygirl 5 years ago
parent aa5b6269c2
commit c435ae1a1c
  1. 1
      scene/GameScene.tscn
  2. 4
      src/EscapeMenuAppearance.cs
  3. 151
      src/EscapeMenuMultiplayer.cs
  4. 17
      src/Network/DeSerializer.Impl.cs
  5. 10
      src/Network/IntegratedServer.cs
  6. 13
      src/Network/NetworkSync.cs
  7. 4
      src/Objects/Player.cs
  8. 22
      src/Scenes/Client.cs
  9. 90
      src/Scenes/Server.cs
  10. 5
      src/Utility/Extensions.cs

@ -11,6 +11,5 @@ BlockContainerPath = NodePath("World/Blocks")
[node name="World" type="Node" parent="."] [node name="World" type="Node" parent="."]
[node name="Players" type="Node" parent="World"] [node name="Players" type="Node" parent="World"]
pause_mode = 1
[node name="Blocks" type="Node" parent="World"] [node name="Blocks" type="Node" parent="World"]

@ -20,7 +20,7 @@ public class EscapeMenuAppearance : CenterContainer
ColorSlider.Value = GD.Randf(); ColorSlider.Value = GD.Randf();
ColorPreview.Modulate = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F); ColorPreview.Modulate = Color.FromHsv((float)ColorSlider.Value, 1.0F, 1.0F);
this.GetClient().OnConnected += () => this.GetClient().Connected += () =>
this.GetClient().RPC(Player.ChangeAppearance, DisplayName.Text, ColorPreview.Modulate); this.GetClient().RPC(Player.ChangeAppearance, DisplayName.Text, ColorPreview.Modulate);
} }
@ -50,7 +50,7 @@ public class EscapeMenuAppearance : CenterContainer
if (IsVisibleInTree()) return; if (IsVisibleInTree()) return;
var client = this.GetClient(); var client = this.GetClient();
// TODO: Find a better way to know if we're connected? // TODO: Find a better way to know if we're connected?
if (client.CustomMultiplayer.NetworkPeer?.GetConnectionStatus() == NetworkedMultiplayerPeer.ConnectionStatus.Connected) if (client.Status == NetworkedMultiplayerPeer.ConnectionStatus.Connected)
client.RPC(Player.ChangeAppearance, DisplayName.Text, ColorPreview.Modulate); client.RPC(Player.ChangeAppearance, DisplayName.Text, ColorPreview.Modulate);
} }
} }

@ -1,5 +1,7 @@
using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Godot; using Godot;
using static Godot.NetworkedMultiplayerPeer;
public class EscapeMenuMultiplayer : Container public class EscapeMenuMultiplayer : Container
{ {
@ -17,7 +19,7 @@ public class EscapeMenuMultiplayer : Container
public Button ClientDisConnect { get; private set; } public Button ClientDisConnect { get; private set; }
public LineEdit ClientAddress { get; private set; } public LineEdit ClientAddress { get; private set; }
public IntegratedServer Server { get; private set; } public IntegratedServer IntegratedServer { get; private set; }
public override void _Ready() public override void _Ready()
{ {
@ -30,54 +32,57 @@ public class EscapeMenuMultiplayer : Container
ServerPort.PlaceholderText = DEFAULT_PORT.ToString(); ServerPort.PlaceholderText = DEFAULT_PORT.ToString();
ClientAddress.PlaceholderText = $"localhost:{DEFAULT_PORT}"; ClientAddress.PlaceholderText = $"localhost:{DEFAULT_PORT}";
this.GetClient().StatusChanged += OnStatusChanged;
CallDeferred(nameof(SetupIntegratedServer)); CallDeferred(nameof(SetupIntegratedServer));
} }
private void SetupIntegratedServer() private void SetupIntegratedServer()
{ {
Server = new IntegratedServer(); IntegratedServer = new IntegratedServer();
this.GetClient().AddChild(Server); this.GetClient().AddChild(IntegratedServer);
CallDeferred(nameof(StartIntegratedServerAndConnect)); CallDeferred(nameof(StartIntegratedServerAndConnect));
} }
private void StartIntegratedServerAndConnect() private void StartIntegratedServerAndConnect()
{ {
Server.Start(DEFAULT_PORT); var port = IntegratedServer.Server.StartSingleplayer();
this.GetClient().Connect("localhost", DEFAULT_PORT); this.GetClient().Connect("127.0.0.1", port);
} }
private void OnStatusChanged(ConnectionStatus status)
{
switch (status) {
case ConnectionStatus.Disconnected:
Status.Text = "Disconnected";
Status.Modulate = Colors.Red;
break;
case ConnectionStatus.Connecting:
Status.Text = "Connecting ...";
Status.Modulate = Colors.Yellow;
break;
case ConnectionStatus.Connected:
if (IntegratedServer == null) {
Status.Text = "Connected!";
Status.Modulate = Colors.Green;
} else if (IntegratedServer.Server.IsSingleplayer) {
Status.Text = "Singleplayer";
Status.Modulate = Colors.White;
} else {
Status.Text = "Server Running";
Status.Modulate = Colors.Green;
}
break;
}
ServerPort.Editable = IntegratedServer != null;
ServerOpenClose.Disabled = IntegratedServer == null;
ServerOpenClose.Text = (IntegratedServer?.Server.IsSingleplayer == false) ? "Close Server" : "Open Server";
ClientDisConnect.Text = ((IntegratedServer != null) || (status == ConnectionStatus.Disconnected)) ? "Connect" : "Disconnect";
// private void OnNetworkStatusChanged(NetworkStatus status) var pauseMode = (IntegratedServer?.Server.IsSingleplayer == true) ? PauseModeEnum.Stop : PauseModeEnum.Process;
// { this.GetClient().GetNode("World").PauseMode = pauseMode;
// switch (status) { if (IntegratedServer != null) IntegratedServer.Server.GetNode("World").PauseMode = pauseMode;
// case NetworkStatus.NoConnection:
// Status.Text = "No Connection"; // TODO: Allow starting up the integrated server again when disconnected.
// Status.Modulate = Colors.Red; }
// break;
// case NetworkStatus.ServerRunning:
// Status.Text = "Server Running";
// Status.Modulate = Colors.Green;
// break;
// case NetworkStatus.Connecting:
// Status.Text = "Connecting ...";
// Status.Modulate = Colors.Yellow;
// break;
// case NetworkStatus.Authenticating:
// Status.Text = "Authenticating ...";
// Status.Modulate = Colors.YellowGreen;
// break;
// case NetworkStatus.ConnectedToServer:
// Status.Text = "Connected to Server";
// Status.Modulate = Colors.Green;
// break;
// }
// var noConnection = status == NetworkStatus.NoConnection;
// ServerPort.Editable = noConnection;
// ServerOpenClose.Text = (status == NetworkStatus.ServerRunning) ? "Stop Server" : "Start Server";
// ServerOpenClose.Disabled = status > NetworkStatus.ServerRunning;
// ClientAddress.Editable = noConnection;
// ClientDisConnect.Text = (status < NetworkStatus.Connecting) ? "Connect" : "Disconnect";
// ClientDisConnect.Disabled = status == NetworkStatus.ServerRunning;
// }
#pragma warning disable IDE0051 #pragma warning disable IDE0051
@ -95,32 +100,62 @@ public class EscapeMenuMultiplayer : Container
} }
} }
private void _on_HideAddress_toggled(bool pressed)
=> ClientAddress.Secret = pressed;
private void _on_ServerOpenClose_pressed() private void _on_ServerOpenClose_pressed()
{ {
// if (GetTree().NetworkPeer == null) { var server = IntegratedServer?.Server;
// var port = Network.DEFAULT_PORT; var client = this.GetClient();
// if (ServerPort.Text.Length > 0) if (server?.IsRunning != true) throw new InvalidOperationException();
// port = ushort.Parse(ServerPort.Text);
// Network.Instance.StartServer(port); if (server.IsSingleplayer) {
// } else Network.Instance.StopServer(); var port = (ServerPort.Text.Length > 0) ? ushort.Parse(ServerPort.Text) : DEFAULT_PORT;
client.Disconnect();
server.Stop();
server.Start(port);
client.Connect("127.0.0.1", port);
// TODO: Pause server processing (including packets, RPC, Sync) until client reconnects?
// If we're doing that, also make sure to re-map packet and RPC targets to point to new NetworkID.
} else {
client.Disconnect();
server.Stop();
var port = server.StartSingleplayer();
client.Connect("127.0.0.1", port);
}
ServerOpenClose.Text = server.IsSingleplayer ? "Open Server" : "Close Server";
} }
private void _on_ClientDisConnect_pressed() private void _on_ClientDisConnect_pressed()
{ {
// if (GetTree().NetworkPeer == null) { var client = this.GetClient();
// var address = "localhost";
// var port = DEFAULT_PORT;
// if (ClientAddress.Text.Length > 0) {
// // TODO: Verify input some more, support IPv6?
// var split = ClientAddress.Text.Split(':');
// address = (split.Length > 1) ? split[0] : ClientAddress.Text;
// port = (split.Length > 1) ? ushort.Parse(split[1]) : port;
// }
// Network.Instance.ConnectToServer(address, port);
// } else Network.Instance.DisconnectFromServer();
}
private void _on_HideAddress_toggled(bool pressed) if (IntegratedServer != null) {
=> ClientAddress.Secret = pressed; IntegratedServer.Server.Stop();
IntegratedServer.GetParent().RemoveChild(IntegratedServer);
IntegratedServer.QueueFree();
IntegratedServer = null;
client.Disconnect();
NetworkSync.ClearAllObjects();
}
if (client.Status == ConnectionStatus.Disconnected) {
var address = "localhost";
var port = DEFAULT_PORT;
if (ClientAddress.Text.Length > 0) {
// TODO: Verify input some more, support IPv6?
var split = ClientAddress.Text.Split(':');
address = (split.Length > 1) ? split[0] : ClientAddress.Text;
port = (split.Length > 1) ? ushort.Parse(split[1]) : port;
}
client.Connect(address, port);
} else {
client.Disconnect();
NetworkSync.ClearAllObjects();
}
}
} }

@ -184,12 +184,12 @@ public class DictionaryDeSerializerGenerator
private readonly IDeSerializer _valueDeSerializer = private readonly IDeSerializer _valueDeSerializer =
DeSerializerRegistry.Get<TKey>(true); DeSerializerRegistry.Get<TKey>(true);
public override void Serialize(Game game, BinaryWriter writer, TDictionary value) public override void Serialize(Game game, BinaryWriter writer, TDictionary dict)
{ {
writer.Write(value.Count); writer.Write(dict.Count);
foreach (var element in value) { foreach (var (key, value) in dict) {
_keyDeSerializer.Serialize(game, writer, element.Key); _keyDeSerializer.Serialize(game, writer, key);
_valueDeSerializer.Serialize(game, writer, element.Value); _valueDeSerializer.Serialize(game, writer, value);
} }
} }
@ -222,7 +222,12 @@ public class SyncedObjectDeSerializerGenerator
public override void Serialize(Game game, BinaryWriter writer, TObj value) public override void Serialize(Game game, BinaryWriter writer, TObj value)
=> writer.Write(value.GetSyncID()); => writer.Write(value.GetSyncID());
public override TObj Deserialize(Game game, BinaryReader reader) public override TObj Deserialize(Game game, BinaryReader reader)
=> (TObj)game.GetObjectBySyncID(reader.ReadUInt32()); {
var id = reader.ReadUInt32();
var value = (TObj)game.GetObjectBySyncID(id);
if (value == null) throw new Exception($"Could not find synced object of type {typeof(TObj)} with ID {id}");
return value;
}
} }
} }

@ -16,12 +16,16 @@ public class IntegratedServer : Node
_sceneTree.CurrentScene = scene; _sceneTree.CurrentScene = scene;
Server = _sceneTree.Root.GetChild<Server>(0); Server = _sceneTree.Root.GetChild<Server>(0);
// Spawn default blocks.
for (var x = -6; x <= 6; x++) {
var block = Server.Spawn<Block>();
block.Position = new BlockPos(x, 3);
block.Color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
block.Unbreakable = true;
}
} }
public override void _Process(float delta) => _sceneTree.Idle(delta); public override void _Process(float delta) => _sceneTree.Idle(delta);
public override void _PhysicsProcess(float delta) => _sceneTree.Iteration(delta); public override void _PhysicsProcess(float delta) => _sceneTree.Iteration(delta);
public override void _ExitTree() => _sceneTree.Finish(); public override void _ExitTree() => _sceneTree.Finish();
public void Start(ushort port) => Server.Start(port);
public void Stop() => Server.Stop();
} }

@ -101,6 +101,19 @@ public static class NetworkSync
NetworkPackets.Send(server, new []{ networkID }, packet); NetworkPackets.Send(server, new []{ networkID }, packet);
} }
internal static void ClearAllObjects()
{
foreach (var (node, _) in _statusByObject) {
if (!Godot.Object.IsInstanceValid(node)) continue;
node.GetParent().RemoveChild(node);
node.QueueFree();
}
_statusByObject.Clear();
_statusBySyncID.Clear();
_dirtyObjects.Clear();
_syncIDCounter = 1;
}
public static uint GetSyncID(this Node obj) public static uint GetSyncID(this Node obj)
=> GetSyncStatus(obj).SyncID; => GetSyncStatus(obj).SyncID;

@ -12,7 +12,7 @@ public class Player : KinematicBody2D, IInitializer
public Sprite Sprite { get; private set; } public Sprite Sprite { get; private set; }
public bool IsLocal { get; private set; } public bool IsLocal { get; private set; } = false;
[SyncProperty] [SyncProperty]
public new Vector2 Position { public new Vector2 Position {
@ -61,7 +61,7 @@ public class Player : KinematicBody2D, IInitializer
if ((Position.y > 9000) && (this.GetGame() is Server server)) if ((Position.y > 9000) && (this.GetGame() is Server server))
this.RPC(new []{ server.GetNetworkID(this) }, ResetPosition, Vector2.Zero); this.RPC(new []{ server.GetNetworkID(this) }, ResetPosition, Vector2.Zero);
this.GetClient()?.RPC(Move, Position); if (IsLocal) this.GetClient()?.RPC(Move, Position);
} }
public override void _PhysicsProcess(float delta) public override void _PhysicsProcess(float delta)

@ -1,14 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Godot; using Godot;
using static Godot.NetworkedMultiplayerPeer;
public class Client : Game public class Client : Game
{ {
[Export] public NodePath CursorPath { get; set; } [Export] public NodePath CursorPath { get; set; }
public Cursor Cursor { get; private set; } public Cursor Cursor { get; private set; }
public event Action OnConnected; public ConnectionStatus Status => CustomMultiplayer.NetworkPeer?.GetConnectionStatus() ?? ConnectionStatus.Disconnected;
public event Action OnDisconnected;
public event Action Connected;
public event Action Disconnected;
public event Action<ConnectionStatus> StatusChanged;
public Client() public Client()
{ {
@ -36,10 +40,13 @@ public class Client : Game
{ {
if (CustomMultiplayer.NetworkPeer != null) if (CustomMultiplayer.NetworkPeer != null)
throw new InvalidOperationException("Client connection is already open"); throw new InvalidOperationException("Client connection is already open");
var peer = new NetworkedMultiplayerENet();
var peer = new NetworkedMultiplayerENet();
var error = peer.CreateClient(address, port); var error = peer.CreateClient(address, port);
if (error != Error.Ok) throw new Exception($"Error when connecting: {error}"); if (error != Error.Ok) throw new Exception($"Error when connecting: {error}");
CustomMultiplayer.NetworkPeer = peer; CustomMultiplayer.NetworkPeer = peer;
StatusChanged?.Invoke(Status);
} }
public void Disconnect() public void Disconnect()
@ -47,12 +54,17 @@ public class Client : Game
if (CustomMultiplayer.NetworkPeer == null) return; if (CustomMultiplayer.NetworkPeer == null) return;
((NetworkedMultiplayerENet)CustomMultiplayer.NetworkPeer).CloseConnection(); ((NetworkedMultiplayerENet)CustomMultiplayer.NetworkPeer).CloseConnection();
CustomMultiplayer.NetworkPeer = null; CustomMultiplayer.NetworkPeer = null;
OnDisconnected?.Invoke();
Disconnected?.Invoke();
StatusChanged?.Invoke(Status);
} }
private void OnConnectedToServer() private void OnConnectedToServer()
=> OnConnected?.Invoke(); {
Connected?.Invoke();
StatusChanged?.Invoke(Status);
}
private void OnPacketReceived(int id, byte[] bytes) private void OnPacketReceived(int id, byte[] bytes)
{ {

@ -8,6 +8,13 @@ public class Server : Game
private readonly Dictionary<NetworkID, Player> _playersByNetworkID = new Dictionary<NetworkID, Player>(); private readonly Dictionary<NetworkID, Player> _playersByNetworkID = new Dictionary<NetworkID, Player>();
private readonly Dictionary<Player, NetworkID> _networkIDByPlayer = new Dictionary<Player, NetworkID>(); private readonly Dictionary<Player, NetworkID> _networkIDByPlayer = new Dictionary<Player, NetworkID>();
private Player _localPlayer = null;
private bool _isLocalPlayerConnected = false;
public NetworkedMultiplayerENet Peer => (NetworkedMultiplayerENet)CustomMultiplayer.NetworkPeer;
public bool IsRunning => Peer != null;
public bool IsSingleplayer { get; private set; }
public Server() public Server()
{ {
CustomMultiplayer = new MultiplayerAPI { RootNode = this }; CustomMultiplayer = new MultiplayerAPI { RootNode = this };
@ -24,30 +31,48 @@ public class Server : Game
} }
public ushort StartSingleplayer()
{
for (var retries = 0; ; retries++) {
try {
IsSingleplayer = true;
// TODO: When `get_local_port` is available, just use port 0 for an auto-assigned port.
// Also see this PR: https://github.com/godotengine/godot/pull/48235
var port = (ushort)GD.RandRange(42000, 43000);
Start(port, "127.0.0.1", 1);
return port;
} catch (Exception ex) {
// Do throw the "Server is already running" exception.
// 3 retries should be well enough to find a random unused port.
if ((ex is InvalidOperationException) || (retries == 2)) throw;
}
}
}
public void Start(ushort port) public void Start(ushort port)
=> Start(port, "*", 32);
private void Start(ushort port, string bindIP, int maxClients)
{ {
if (CustomMultiplayer.NetworkPeer != null) if (IsRunning) throw new InvalidOperationException("Server is already running");
throw new InvalidOperationException("Server is already running");
var peer = new NetworkedMultiplayerENet(); var peer = new NetworkedMultiplayerENet();
var error = peer.CreateServer(port); peer.SetBindIp(bindIP);
peer.ServerRelay = false;
var error = peer.CreateServer(port, maxClients);
if (error != Error.Ok) throw new Exception($"Error when starting the server: {error}"); if (error != Error.Ok) throw new Exception($"Error when starting the server: {error}");
CustomMultiplayer.NetworkPeer = peer;
// Spawn default blocks. CustomMultiplayer.NetworkPeer = peer;
for (var x = -6; x <= 6; x++) {
var block = this.Spawn<Block>();
block.Position = new BlockPos(x, 3);
block.Color = Color.FromHsv(GD.Randf(), 0.1F, 1.0F);
block.Unbreakable = true;
}
} }
public void Stop() public void Stop()
{ {
if (CustomMultiplayer.NetworkPeer != null) if (!IsRunning) throw new InvalidOperationException("Server is not running");
throw new InvalidOperationException("Server is not running");
((NetworkedMultiplayerENet)CustomMultiplayer.NetworkPeer).CloseConnection(); Peer.CloseConnection();
CustomMultiplayer.NetworkPeer = null; CustomMultiplayer.NetworkPeer = null;
IsSingleplayer = false;
_isLocalPlayerConnected = false;
} }
@ -60,15 +85,34 @@ public class Server : Game
private void OnPeerConnected(int id) private void OnPeerConnected(int id)
{ {
var networkID = new NetworkID(id); var networkID = new NetworkID(id);
NetworkSync.SendAllObjects(this, networkID);
var player = this.Spawn<Player>(); if (IsSingleplayer) {
player.Position = Vector2.Zero; if (Peer.GetPeerAddress(id) != "127.0.0.1")
player.Color = Colors.Red; { Peer.DisconnectPeer(id, true); return; }
CustomMultiplayer.RefuseNewNetworkConnections = true;
}
_playersByNetworkID.Add(networkID, player); Player player;
_networkIDByPlayer.Add(player, networkID); if ((_localPlayer != null) && !_isLocalPlayerConnected &&
(Peer.GetPeerAddress(id) == "127.0.0.1")) {
player = _localPlayer;
_isLocalPlayerConnected = true;
var oldNetworkID = GetNetworkID(player);
_playersByNetworkID.Remove(oldNetworkID);
_playersByNetworkID.Add(networkID, player);
_networkIDByPlayer[player] = networkID;
} else {
NetworkSync.SendAllObjects(this, networkID);
player = this.Spawn<Player>();
player.Position = Vector2.Zero;
player.Color = Colors.Red;
_playersByNetworkID.Add(networkID, player);
_networkIDByPlayer.Add(player, networkID);
}
if (IsSingleplayer) _localPlayer = player;
player.RPC(new []{ networkID }, player.SetLocal); player.RPC(new []{ networkID }, player.SetLocal);
} }
@ -76,6 +120,10 @@ public class Server : Game
{ {
var networkID = new NetworkID(id); var networkID = new NetworkID(id);
var player = GetPlayer(networkID); var player = GetPlayer(networkID);
// Local player stays around for reconnecting.
if (_localPlayer == player) return;
player.Destroy(); player.Destroy();
_playersByNetworkID.Remove(networkID); _playersByNetworkID.Remove(networkID);
_networkIDByPlayer.Remove(player); _networkIDByPlayer.Remove(player);

@ -1,3 +1,4 @@
using System.Collections.Generic;
using Godot; using Godot;
public static class Extensions public static class Extensions
@ -16,6 +17,10 @@ public static class Extensions
(instance as IInitializer)?.Initialize(); (instance as IInitializer)?.Initialize();
return instance; return instance;
} }
public static void Deconstruct<TKey, TValue>(
this KeyValuePair<TKey, TValue> kvp, out TKey key, out TValue value)
{ key = kvp.Key; value = kvp.Value; }
} }
public interface IInitializer public interface IInitializer

Loading…
Cancel
Save