From 789b4bacecce5554b62745645e88c4526c7fdf42 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sat, 14 May 2022 19:04:34 +0300 Subject: [PATCH] netcode: replicate other players for connecting client --- .gitignore | 19 +- Source/Game/Game.Build.cs | 3 +- Source/Game/GameMode/GameModeManager.cs | 271 ++++++++++++++++++- Source/Game/Network/NetworkManager.cs | 3 +- Source/Game/Network/NetworkManager_Client.cs | 25 +- Source/Game/Network/NetworkManager_Server.cs | 43 ++- Source/Game/Player/PlayerMovement.cs | 2 +- Tests/{Cabrito => }/ConsoleTests.cs | 0 Tests/GoakeTests.csproj | 15 +- Tests/{MapParser => }/MapParserTests.cs | 0 10 files changed, 358 insertions(+), 23 deletions(-) rename Tests/{Cabrito => }/ConsoleTests.cs (100%) rename Tests/{MapParser => }/MapParserTests.cs (100%) diff --git a/.gitignore b/.gitignore index 01073d5..633f1dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,23 @@ -GoakeFlax.sln - -Binaries/* +Binaries/* Cache/* Logs/* -packages/* +Output/* +/*.sln +Source/obj/ Source/**/*.csproj Source/**/*.Gen.cs *.csproj.user *.suo +*.DotSettings.user .vs/* -Output/* +.vscode/ +.idea/* + */SceneData/*/Lightmaps/* */SceneData/*/SkyLights/* -.vscode/ -Source/obj/ -.idea/* -*.DotSettings.user + +packages/* Tests/bin/ Tests/obj/ Assets/desktop.ini diff --git a/Source/Game/Game.Build.cs b/Source/Game/Game.Build.cs index d1d3ea0..a2cbee7 100644 --- a/Source/Game/Game.Build.cs +++ b/Source/Game/Game.Build.cs @@ -23,10 +23,9 @@ public class Game : GameModule //options.CompileEnv.PreprocessorDefinitions.Add("COMPILE_WITH_CSG_BUILDER"); //options.PublicDefinitions.Add("COMPILE_WITH_CSG_BUILDER"); - options.PublicDependencies.Add("FidelityFXFSR"); - base.Setup(options); + options.PublicDependencies.Add("FidelityFXFSR"); // Here you can modify the build options for your game module // To reference another module use: options.PublicDependencies.Add("Audio"); // To add C++ define use: options.PublicDefinitions.Add("COMPILE_WITH_FLAX"); diff --git a/Source/Game/GameMode/GameModeManager.cs b/Source/Game/GameMode/GameModeManager.cs index a8fa679..9bab02e 100644 --- a/Source/Game/GameMode/GameModeManager.cs +++ b/Source/Game/GameMode/GameModeManager.cs @@ -10,34 +10,184 @@ namespace Game { public enum GameModeMessageType : byte { + WelcomePlayer, SpawnPlayer, PlayerInput, - + PlayerPosition, // debug } + + public static class GameModeManager { + private class WorldState + { + public ulong frame; + public List actors; + + public Dictionary playerFrameHistory; + + public WorldState() + { + actors = new List(); + playerFrameHistory = new Dictionary(); + } + } + + private class PlayerFrame + { + public ulong frame; + public Vector3 position; + } + private static Dictionary players; + private static Dictionary playerConnections; + private static bool spawned = false; + + private static WorldState worldState; + private static PlayerFrame[] localPlayerFrameHistory; public static void Init() { players = new Dictionary(); + playerConnections = new Dictionary(); + localPlayerFrameHistory = new PlayerFrame[120]; + worldState = new WorldState(); NetworkManager.OnMessage += OnMessage; Level.SceneLoaded += OnLevelLoaded; + Scripting.LateUpdate += OnLateUpdatePre; } public static void Cleanup() { NetworkManager.OnMessage -= OnMessage; Level.SceneLoaded -= OnLevelLoaded; + Scripting.LateUpdate -= OnLateUpdatePre; + } + + private static PlayerFrame GetPlayerFrame(uint playerIndex, ulong playerFrameIndex) + { + PlayerFrame[] playerFrames = worldState.playerFrameHistory[playerIndex]; + PlayerFrame playerFrame = playerFrames[playerFrameIndex % 120]; + + if (playerFrame.frame != playerFrameIndex) + return null; + + return playerFrame; } public static void OnLevelLoaded(Scene scene, Guid assetGuid) { + worldState.frame = 0; Console.Print("level loaded"); } + public static void OnLateUpdatePre() + { + try + { + NetworkManager.IsServer = NetworkManager.server != null; + NetworkManager.IsClient = NetworkManager.client != null && NetworkManager.server == null; + OnLateUpdate(); + } + finally + { + NetworkManager.IsServer = false; + NetworkManager.IsClient = false; + } + } + + public static void OnLateUpdate() + { + if (NetworkManager.IsServer) + { + foreach (KeyValuePair kv in players) + { + var playerId = kv.Key; + var playerActor = kv.Value; + + var playerFrames = worldState.playerFrameHistory[playerId]; + var playerFrame = playerFrames[worldState.frame % 120]; + + playerFrame.frame = worldState.frame; + playerFrame.position = playerActor.Position; + } + + foreach (KeyValuePair kv in players) + { + var playerId = kv.Key; + foreach (KeyValuePair kv2 in players) + { + if (kv2.Key == playerId) + continue; + + var otherPlayerActor = kv2.Value; + + // TODO: relevancy checks here etc. + + { + NetworkMessage message = NetworkManager.ServerBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.PlayerPosition); + message.WriteUInt64(worldState.frame); + message.WriteUInt32(kv2.Key); + message.WriteSingle(otherPlayerActor.Position.X); + message.WriteSingle(otherPlayerActor.Position.Y); + message.WriteSingle(otherPlayerActor.Position.Z); + NetworkManager.ServerEndSendMessage(ref message, playerConnections[playerId]); + } + } + } + } + + if (NetworkManager.IsClient) + { + if (!spawned) + return; + + PlayerActor playerActor = Level.GetActors().First(x => + x.GetScript().PlayerId == NetworkManager.LocalPlayerClientId); + + var playerFrame = localPlayerFrameHistory[worldState.frame % 120]; + + playerFrame.frame = worldState.frame; + playerFrame.position = playerActor.Position; + + { + NetworkMessage message = NetworkManager.ClientBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.PlayerPosition); + message.WriteUInt64(worldState.frame); + message.WriteUInt32(NetworkManager.LocalPlayerClientId); + message.WriteSingle(playerActor.Position.X); + message.WriteSingle(playerActor.Position.Y); + message.WriteSingle(playerActor.Position.Z); + NetworkManager.ClientEndSendMessage(ref message); + } + } + + worldState.frame++; + + /*foreach (KeyValuePair kv in players) + { + var playerId = kv.Key; + var playerActor = kv.Value; + var playerConnection = playerConnections[playerId]; + + if (NetworkManager.IsLocalClient && playerId == NetworkManager.LocalPlayerClientId) + continue; + + { + NetworkMessage message = NetworkManager.ServerBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.PlayerPosition); + message.WriteUInt64(0); + message.WriteSingle(playerActor.Position.X); + message.WriteSingle(playerActor.Position.Y); + message.WriteSingle(playerActor.Position.Z); + NetworkManager.ServerEndSendMessage(ref message, playerConnections[playerId]); + } + }*/ + } + public static bool OnMessage(ref NetworkEvent networkEvent) { byte messageTypeByte = networkEvent.Message.ReadByte(); @@ -52,9 +202,38 @@ namespace Game //Console.Print($"GameModeManager: {messageType}"); switch (messageType) { + case GameModeMessageType.WelcomePlayer: + { + if (NetworkManager.IsClient) + { + worldState.frame = networkEvent.Message.ReadUInt64(); + int numActors = (int)networkEvent.Message.ReadUInt32(); + for (int i=0; i 0.0001) + { + Console.Print("player drifted, len: " + (playerFramePosition - reportedPosition).Length); + + { + NetworkMessage message = NetworkManager.ServerBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.PlayerPosition); + /*message.WriteUInt64(reportedFrame); + message.WriteUInt32(playerId); + message.WriteSingle(playerFramePosition.X); + message.WriteSingle(playerFramePosition.Y); + message.WriteSingle(playerFramePosition.Z);*/ + message.WriteUInt64(worldState.frame); + message.WriteUInt32(playerId); + message.WriteSingle(playerActor.Position.X); + message.WriteSingle(playerActor.Position.Y); + message.WriteSingle(playerActor.Position.Z); + NetworkManager.ServerEndSendMessage(ref message, playerConnections[playerId]); + } + } + } + } + else if (NetworkManager.IsClient) + { + Console.Print($"we drifted, corrected. client frame: {worldState.frame}, server frame: {reportedFrame}"); + PlayerActor playerActor = Level.GetActors().FirstOrDefault(x => + x.GetScript().PlayerId == reportedPlayerId); + + if (playerActor != null) + playerActor.SetPosition(reportedPosition); + } + + break; + } default: Console.PrintError($"GameModeManager: Unhandled message type: {messageType}"); return false; @@ -84,6 +321,22 @@ namespace Game public static bool OnClientConnecting(NetworkConnection connection) { + if (connection.ConnectionId != NetworkManager.LocalPlayerClientId) + { + Console.Print("sending welcome: frame " + worldState.frame); + NetworkMessage message = NetworkManager.ServerBeginSendMessage(); + message.WriteByte((byte)GameModeMessageType.WelcomePlayer); + message.WriteUInt64(worldState.frame); + message.WriteUInt32((uint)worldState.actors.Count); + foreach (PlayerActor playerActor in worldState.actors) + { + message.WriteUInt32(playerActor.GetScript().PlayerId); + message.WriteSingle(playerActor.Position.X); + message.WriteSingle(playerActor.Position.Y); + message.WriteSingle(playerActor.Position.Z); + } + NetworkManager.ServerEndSendMessage(ref message, connection); + } return true; } @@ -99,12 +352,19 @@ namespace Game Vector3 eulerAngles = randomSpawn.Orientation.EulerAngles; players.Add(playerId, null); + playerConnections.Add(playerId, connection); + var playerFrames = new PlayerFrame[120]; + for (int i = 0; i < playerFrames.Length; i++) + playerFrames[i] = new PlayerFrame(); + worldState.playerFrameHistory.Add(playerId, playerFrames); + SpawnPlayer(playerId, position, eulerAngles); { NetworkMessage message = NetworkManager.ServerBeginSendMessage(); message.WriteByte((byte)GameModeMessageType.SpawnPlayer); message.WriteUInt32(playerId); + message.WriteUInt64(worldState.frame); message.WriteSingle(position.X); message.WriteSingle(position.Y); message.WriteSingle(position.Z); @@ -122,6 +382,8 @@ namespace Game if (NetworkManager.IsLocalClient) return; // Handled by listenserver + spawned = true; + string workDir = Directory.GetCurrentDirectory(); string prefabPath = Path.Combine(workDir, "Content", "Common"); var playerPrefab = Content.Load(Path.Combine(prefabPath, "PlayerPrefab.prefab")); @@ -132,7 +394,12 @@ namespace Game playerActor.Initialize(playerId); playerActor.Teleport(position, eulerAngles); + if (!players.ContainsKey(playerId)) + players.Add(playerId, null); players[playerId] = playerActor; + + if (NetworkManager.IsServer) + worldState.actors.Add(playerActor); } private static void UpdatePlayerInput(uint playerId, PlayerInputState inputState) diff --git a/Source/Game/Network/NetworkManager.cs b/Source/Game/Network/NetworkManager.cs index 2253236..7c37b90 100644 --- a/Source/Game/Network/NetworkManager.cs +++ b/Source/Game/Network/NetworkManager.cs @@ -43,7 +43,8 @@ namespace Game private static readonly ushort MaximumClients = 32; public static OnMessageDecl OnMessage; - public static bool IsServer => server != null; + public static bool IsServer = false; + public static bool IsClient = false; public static bool IsLocalClient = false; // Context dependant, true when message is handled by local client public static void Init() diff --git a/Source/Game/Network/NetworkManager_Client.cs b/Source/Game/Network/NetworkManager_Client.cs index ba75482..5330733 100644 --- a/Source/Game/Network/NetworkManager_Client.cs +++ b/Source/Game/Network/NetworkManager_Client.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using FlaxEngine; using FlaxEngine.Networking; using Console = Cabrito.Console; @@ -62,12 +63,34 @@ namespace Game { try { - IsLocalClient = IsServer; + IsLocalClient = server != null; + IsClient = true; OnNetworkMessage(ref networkEvent); + + if (networkEvent.Message.Position > 0 && + networkEvent.Message.Position < networkEvent.Message.Length) + { + string err = + $"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}."; + + networkEvent.Message.Position = 0; + byte[] messageBytes = new byte[networkEvent.Message.Length]; + unsafe + { + fixed (byte* messageBytePtr = &messageBytes[0]) + networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length); + } + + string messageBytesStr = string.Join(", ", + messageBytes.Select(x => "0x" + ((int)x).ToString("X2"))); + + Console.PrintError(err + $"Message dump: {messageBytesStr}"); + } } finally { IsLocalClient = false; + IsClient = false; client.RecycleMessage(networkEvent.Message); } break; diff --git a/Source/Game/Network/NetworkManager_Server.cs b/Source/Game/Network/NetworkManager_Server.cs index 75f51b6..c27da04 100644 --- a/Source/Game/Network/NetworkManager_Server.cs +++ b/Source/Game/Network/NetworkManager_Server.cs @@ -103,15 +103,24 @@ namespace Game { Console.Print($"Client({networkEvent.Sender.ConnectionId}) is trying to connect"); - if (GameModeManager.OnClientConnecting(networkEvent.Sender)) + try { - ConnectedClients.Add(networkEvent.Sender); - Console.Print($"Client({networkEvent.Sender.ConnectionId}) connected. Total clients: {ConnectedClients.Count}"); + IsServer = true; + if (GameModeManager.OnClientConnecting(networkEvent.Sender)) + { + ConnectedClients.Add(networkEvent.Sender); + Console.Print( + $"Client({networkEvent.Sender.ConnectionId}) connected. Total clients: {ConnectedClients.Count}"); - GameModeManager.OnClientConnected(networkEvent.Sender); + GameModeManager.OnClientConnected(networkEvent.Sender); + } + else + Console.Print($"Client({networkEvent.Sender.ConnectionId}) connection refused"); + } + finally + { + IsServer = false; } - else - Console.Print($"Client({networkEvent.Sender.ConnectionId}) connection refused"); break; } @@ -128,10 +137,32 @@ namespace Game { try { + IsServer = true; OnNetworkMessage(ref networkEvent); + + if (networkEvent.Message.Position > 0 && + networkEvent.Message.Position < networkEvent.Message.Length) + { + string err = + $"Network message was not fully read: {networkEvent.Message.Position} / {networkEvent.Message.Length}."; + + networkEvent.Message.Position = 0; + byte[] messageBytes = new byte[networkEvent.Message.Length]; + unsafe + { + fixed (byte* messageBytePtr = &messageBytes[0]) + networkEvent.Message.ReadBytes(messageBytePtr, (int)networkEvent.Message.Length); + } + + string messageBytesStr = string.Join(", ", + messageBytes.Select(x => "0x" + ((int)x).ToString("X2"))); + + Console.PrintError(err + $"Message dump: {messageBytesStr}"); + } } finally { + IsServer = false; server.RecycleMessage(networkEvent.Message); } break; diff --git a/Source/Game/Player/PlayerMovement.cs b/Source/Game/Player/PlayerMovement.cs index fa3bb4c..4bdeebb 100644 --- a/Source/Game/Player/PlayerMovement.cs +++ b/Source/Game/Player/PlayerMovement.cs @@ -79,7 +79,7 @@ namespace Game private readonly bool predicting = false; private readonly List touchingActors = new List(); - private int currentInputFrame; + public int currentInputFrame; private int currentInputFrame2; private Vector3 currentVelocity; diff --git a/Tests/Cabrito/ConsoleTests.cs b/Tests/ConsoleTests.cs similarity index 100% rename from Tests/Cabrito/ConsoleTests.cs rename to Tests/ConsoleTests.cs diff --git a/Tests/GoakeTests.csproj b/Tests/GoakeTests.csproj index 1a6b7d4..861f859 100644 --- a/Tests/GoakeTests.csproj +++ b/Tests/GoakeTests.csproj @@ -8,7 +8,7 @@ False - Editor.Windows.Development;Game.Windows.Development + Editor.Windows.Development;Game.Windows.Development;Release AnyCPU @@ -33,6 +33,19 @@ true + + BUILD_DEVELOPMENT;PLATFORM_WINDOWS;FLAX_EDITOR;FLAX;FLAX_ASSERTIONS;FLAX_1;FLAX_1_4 + + + + BUILD_DEVELOPMENT;PLATFORM_WINDOWS;FLAX_GAME;FLAX;FLAX_ASSERTIONS;FLAX_1;FLAX_1_4 + + + + BUILD_RELEASE;PLATFORM_WINDOWS;FLAX_GAME;FLAX;FLAX_ASSERTIONS;FLAX_1;FLAX_1_4 + true + + diff --git a/Tests/MapParser/MapParserTests.cs b/Tests/MapParserTests.cs similarity index 100% rename from Tests/MapParser/MapParserTests.cs rename to Tests/MapParserTests.cs