diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index 0219e1769..421512ce0 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -556,6 +556,7 @@ namespace FlaxEditor internal void OnPlayEnding() { + FlaxEngine.Networking.NetworkManager.Stop(); // Shutdown any multiplayer from playmode PlayModeEnding?.Invoke(); } diff --git a/Source/Engine/Networking/NetworkClient.h b/Source/Engine/Networking/NetworkClient.h new file mode 100644 index 000000000..85e7fba2d --- /dev/null +++ b/Source/Engine/Networking/NetworkClient.h @@ -0,0 +1,29 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Types.h" +#include "NetworkConnection.h" +#include "NetworkConnectionState.h" +#include "Engine/Scripting/ScriptingObject.h" + +/// +/// High-level network client object (local or connected to the server). +/// +API_CLASS(sealed, NoSpawn, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkClient final : public ScriptingObject +{ + DECLARE_SCRIPTING_TYPE_NO_SPAWN(NetworkClient); + friend class NetworkManager; + explicit NetworkClient(NetworkConnection connection); + +public: + /// + /// Identifier of the client (connection id from local peer). + /// + API_FIELD(ReadOnly) NetworkConnection Connection; + + /// + /// Client connection state. + /// + API_FIELD(ReadOnly) NetworkConnectionState State; +}; diff --git a/Source/Engine/Networking/NetworkConnectionState.h b/Source/Engine/Networking/NetworkConnectionState.h new file mode 100644 index 000000000..001c740f4 --- /dev/null +++ b/Source/Engine/Networking/NetworkConnectionState.h @@ -0,0 +1,36 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Config.h" + +/// +/// The high-level network connection state. +/// +API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkConnectionState +{ + /// + /// Not connected. + /// + Offline = 0, + + /// + /// Connection process was started but not yet finished. + /// + Connecting, + + /// + /// Connection has been made. + /// + Connected, + + /// + /// Disconnection process was started but not yet finished. + /// + Disconnecting, + + /// + /// Connection ended. + /// + Disconnected, +}; diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp new file mode 100644 index 000000000..43b937bf8 --- /dev/null +++ b/Source/Engine/Networking/NetworkManager.cpp @@ -0,0 +1,400 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#include "NetworkManager.h" +#include "NetworkClient.h" +#include "NetworkPeer.h" +#include "NetworkEvent.h" +#include "NetworkChannelType.h" +#include "NetworkSettings.h" +#include "FlaxEngine.Gen.h" +#include "Engine/Core/Log.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Engine/EngineService.h" +#include "Engine/Engine/Time.h" +#include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Scripting/Scripting.h" + +float NetworkManager::NetworkFPS = 60.0f; +NetworkManagerMode NetworkManager::Mode = NetworkManagerMode::Offline; +NetworkConnectionState NetworkManager::State = NetworkConnectionState::Offline; +NetworkClient* NetworkManager::LocalClient = nullptr; +Array NetworkManager::Clients; +Action NetworkManager::StateChanged; +Delegate NetworkManager::ClientConnecting; +Delegate NetworkManager::ClientConnected; +Delegate NetworkManager::ClientDisconnected; + +enum class NetworkMessageIDs : uint8 +{ + None = 0, + Handshake, + HandshakeReply, +}; + +PACK_STRUCT(struct NetworkMessageHandshake + { + NetworkMessageIDs ID; + uint32 EngineBuild; + uint32 EngineProtocolVersion; + uint32 GameProtocolVersion; + byte Platform; + byte Architecture; + uint16 PayloadDataSize; + }); + +PACK_STRUCT(struct NetworkMessageHandshakeReply + { + NetworkMessageIDs ID; + int32 Result; + }); + +namespace +{ + NetworkPeer* Peer = nullptr; + uint32 GameProtocolVersion = 0; + double LastUpdateTime = 0; +} + +class NetworkManagerService : public EngineService +{ +public: + NetworkManagerService() + : EngineService(TEXT("Network Manager"), 1000) + { + } + + void Update() override; + + void Dispose() override + { + // Ensure to dispose any resources upon exiting + NetworkManager::Stop(); + } +}; + +NetworkManagerService NetworkManagerServiceInstance; + +bool StartPeer() +{ + PROFILE_CPU(); + ASSERT_LOW_LAYER(!Peer); + NetworkManager::State = NetworkConnectionState::Connecting; + NetworkManager::StateChanged(); + const auto& settings = *NetworkSettings::Get(); + + // Create Network Peer that will use underlying INetworkDriver to send messages over the network + NetworkConfig networkConfig; + if (NetworkManager::Mode == NetworkManagerMode::Client) + { + // Client + networkConfig.Address = settings.Address; + networkConfig.Port = settings.Port; + networkConfig.ConnectionsLimit = 1; + } + else + { + // Server or Host + networkConfig.Address = TEXT("any"); + networkConfig.Port = settings.Port; + networkConfig.ConnectionsLimit = (uint16)settings.MaxClients; + } + const ScriptingTypeHandle networkDriverType = Scripting::FindScriptingType(settings.NetworkDriver); + if (!networkDriverType) + { + LOG(Error, "Unknown Network Driver type {0}", String(settings.NetworkDriver)); + return true; + } + networkConfig.NetworkDriver = ScriptingObject::NewObject(networkDriverType); + Peer = NetworkPeer::CreatePeer(networkConfig); + if (!Peer) + { + LOG(Error, "Failed to create Network Peer at {0}:{1}", networkConfig.Address, networkConfig.Port); + return true; + } + + return false; +} + +void StopPeer() +{ + if (!Peer) + return; + PROFILE_CPU(); + if (NetworkManager::Mode == NetworkManagerMode::Client) + Peer->Disconnect(); + NetworkPeer::ShutdownPeer(Peer); + Peer = nullptr; +} + +void NetworkSettings::Apply() +{ + NetworkManager::NetworkFPS = NetworkFPS; + GameProtocolVersion = ProtocolVersion; +} + +NetworkClient::NetworkClient(NetworkConnection connection) + : ScriptingObject(SpawnParams(Guid::New(), TypeInitializer)) + , Connection(connection) + , State(NetworkConnectionState::Connecting) +{ +} + +NetworkClient* NetworkManager::GetClient(const NetworkConnection& connection) +{ + if (connection.ConnectionId == 0) + return LocalClient; + for (NetworkClient* client : Clients) + { + if (client->Connection == connection) + return client; + } + return nullptr; +} + +bool NetworkManager::StartServer() +{ + PROFILE_CPU(); + Stop(); + + Mode = NetworkManagerMode::Server; + if (StartPeer()) + return true; + if (!Peer->Listen()) + return true; + + State = NetworkConnectionState::Connected; + StateChanged(); + return false; +} + +bool NetworkManager::StartClient() +{ + PROFILE_CPU(); + Stop(); + + Mode = NetworkManagerMode::Client; + if (StartPeer()) + return true; + if (!Peer->Connect()) + return true; + LocalClient = New(NetworkConnection{0}); + + return false; +} + +bool NetworkManager::StartHost() +{ + PROFILE_CPU(); + Stop(); + + Mode = NetworkManagerMode::Host; + if (StartPeer()) + return true; + if (!Peer->Listen()) + return true; + LocalClient = New(NetworkConnection{0}); + + // Auto-connect host + LocalClient->State = NetworkConnectionState::Connected; + ClientConnected(LocalClient); + + State = NetworkConnectionState::Connected; + StateChanged(); + return false; +} + +void NetworkManager::Stop() +{ + if (Mode == NetworkManagerMode::Offline && State == NetworkConnectionState::Offline) + return; + PROFILE_CPU(); + + State = NetworkConnectionState::Disconnecting; + if (LocalClient) + LocalClient->State = NetworkConnectionState::Disconnecting; + for (NetworkClient* client : Clients) + client->State = NetworkConnectionState::Disconnecting; + StateChanged(); + + for (int32 i = Clients.Count() - 1; i >= 0; i--) + { + NetworkClient* client = Clients[i]; + ClientDisconnected(client); + client->State = NetworkConnectionState::Disconnected; + Delete(client); + Clients.RemoveAt(i); + } + StopPeer(); + if (LocalClient) + { + LocalClient->State = NetworkConnectionState::Disconnected; + Delete(LocalClient); + LocalClient = nullptr; + } + + State = NetworkConnectionState::Disconnected; + Mode = NetworkManagerMode::Offline; + StateChanged(); +} + +void NetworkManagerService::Update() +{ + const double currentTime = Time::Update.UnscaledTime.GetTotalSeconds(); + const float minDeltaTime = NetworkManager::NetworkFPS > 0 ? 1.0f / NetworkManager::NetworkFPS : 0.0f; + if (NetworkManager::Mode == NetworkManagerMode::Offline || (float)(currentTime - LastUpdateTime) < minDeltaTime) + return; + PROFILE_CPU(); + LastUpdateTime = currentTime; + // TODO: convert into TaskGraphSystems and use async jobs + + // Process network messages + NetworkEvent event; + while (Peer->PopEvent(event)) + { + switch (event.EventType) + { + case NetworkEventType::Connected: + LOG(Info, "Incoming connection with Id={0}", event.Sender.ConnectionId); + if (NetworkManager::IsClient()) + { + // Initialize client connection data + NetworkClientConnectionData connectionData; + connectionData.Client = NetworkManager::LocalClient; + connectionData.Result = 0; + connectionData.Platform = PLATFORM_TYPE; + connectionData.Architecture = PLATFORM_ARCH; + NetworkManager::ClientConnecting(connectionData); // Allow client to validate connection or inject custom connection data + if (connectionData.Result != 0) + { + LOG(Info, "Connection blocked with result {0}.", connectionData.Result); + NetworkManager::Stop(); + break; + } + + // Send initial handshake message from client to server + NetworkMessageHandshake msgData; + msgData.ID = NetworkMessageIDs::Handshake; + msgData.EngineBuild = FLAXENGINE_VERSION_BUILD; + msgData.EngineProtocolVersion = 1; + msgData.GameProtocolVersion = GameProtocolVersion; + msgData.Platform = (byte)connectionData.Platform; + msgData.Architecture = (byte)connectionData.Architecture; + msgData.PayloadDataSize = (uint16)connectionData.PayloadData.Count(); + NetworkMessage msg = Peer->BeginSendMessage(); + msg.WriteStructure(msgData); + msg.WriteBytes(connectionData.PayloadData.Get(), connectionData.PayloadData.Count()); + Peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg); + } + else + { + // Create incoming client + auto client = New(event.Sender); + NetworkManager::Clients.Add(client); + } + break; + case NetworkEventType::Disconnected: + case NetworkEventType::Timeout: + LOG(Info, "{1} with Id={0}", event.Sender.ConnectionId, event.EventType == NetworkEventType::Disconnected ? TEXT("Disconnected") : TEXT("Disconnected on timeout")); + if (NetworkManager::IsClient()) + { + // Server disconnected from client + NetworkManager::Stop(); + return; + } + else + { + // Client disconnected from server/host + NetworkClient* client = NetworkManager::GetClient(event.Sender); + if (!client) + { + LOG(Error, "Unknown client"); + break; + } + client->State = NetworkConnectionState::Disconnecting; + LOG(Info, "Client id={0} disconnected", event.Sender.ConnectionId); + NetworkManager::Clients.RemoveKeepOrder(client); + NetworkManager::ClientDisconnected(client); + client->State = NetworkConnectionState::Disconnected; + Delete(client); + } + break; + case NetworkEventType::Message: + { + // Process network message + NetworkClient* client = NetworkManager::GetClient(event.Sender); + if (!client && NetworkManager::Mode != NetworkManagerMode::Client) + { + LOG(Error, "Unknown client"); + break; + } + uint8 id = *event.Message.Buffer; + switch ((NetworkMessageIDs)id) + { + case NetworkMessageIDs::Handshake: + { + // Read client connection data + NetworkMessageHandshake msgData; + event.Message.ReadStructure(msgData); + NetworkClientConnectionData connectionData; + connectionData.Client = client; + connectionData.Result = 0; + connectionData.Platform = (PlatformType)msgData.Platform; + connectionData.Architecture = (ArchitectureType)msgData.Architecture; + connectionData.PayloadData.Resize(msgData.PayloadDataSize); + event.Message.ReadBytes(connectionData.PayloadData.Get(), msgData.PayloadDataSize); + NetworkManager::ClientConnecting(connectionData); // Allow server to validate connection + + // Reply to the handshake message with a result + NetworkMessageHandshakeReply replyData; + replyData.ID = NetworkMessageIDs::HandshakeReply; + replyData.Result = connectionData.Result; + NetworkMessage msgReply = Peer->BeginSendMessage(); + msgReply.WriteStructure(replyData); + Peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msgReply, event.Sender); + + // Update client based on connection result + if (connectionData.Result != 0) + { + LOG(Info, "Connection blocked with result {0} from client id={1}.", connectionData.Result, event.Sender.ConnectionId); + client->State = NetworkConnectionState::Disconnecting; + Peer->Disconnect(event.Sender); + client->State = NetworkConnectionState::Disconnected; + } + else + { + client->State = NetworkConnectionState::Connected; + LOG(Info, "Client id={0} connected", event.Sender.ConnectionId); + NetworkManager::ClientConnected(client); + } + break; + } + case NetworkMessageIDs::HandshakeReply: + { + ASSERT_LOW_LAYER(NetworkManager::IsClient()); + NetworkMessageHandshakeReply msgData; + event.Message.ReadStructure(msgData); + if (msgData.Result != 0) + { + // Server failed to connect with client + // TODO: feed game with result from msgData.Result + NetworkManager::Stop(); + return; + } + + // Client got connected with server + NetworkManager::LocalClient->State = NetworkConnectionState::Connected; + NetworkManager::State = NetworkConnectionState::Connected; + NetworkManager::StateChanged(); + break; + } +#if !BUILD_RELEASE + default: + LOG(Warning, "Unknown message id={0} from connection {1}", id, event.Sender.ConnectionId); +#endif + } + } + Peer->RecycleMessage(event.Message); + break; + } + } +} diff --git a/Source/Engine/Networking/NetworkManager.h b/Source/Engine/Networking/NetworkManager.h new file mode 100644 index 000000000..23dd4ead6 --- /dev/null +++ b/Source/Engine/Networking/NetworkManager.h @@ -0,0 +1,150 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#pragma once + +#include "NetworkConnection.h" +#include "Types.h" +#include "NetworkConnectionState.h" +#include "Engine/Core/Delegate.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Scripting/ScriptingType.h" + +/// +/// The high-level network manage modes. +/// +API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkManagerMode +{ + // Disabled. + Offline = 0, + // Server-only without a client (host a game but not participate). + Server, + // Client-only (connected to Server or Host). + Client, + // Both server and client (other clients can connect). + Host, +}; + +/// +/// The high-level network client connection data. Can be used to accept/deny new connection. +/// +API_STRUCT(Namespace="FlaxEngine.Networking") struct NetworkClientConnectionData +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkClientConnectionData); + // The incoming client. + API_FIELD() NetworkClient* Client; + // 0 if accept new connection, error code otherwise. Send back to the client. + API_FIELD() int32 Result; + // Client platform type (can be different that current one when using cross-play). + API_FIELD() PlatformType Platform; + // Client platform architecture (can be different that current one when using cross-play). + API_FIELD() ArchitectureType Architecture; + // Custom data to send from client to server as part of connection initialization and verification. Can contain game build info, platform data or certificate needed upon joining the game. + API_FIELD() Array PayloadData; +}; + +/// +/// High-level networking manager for multiplayer games. +/// +API_CLASS(static, Namespace = "FlaxEngine.Networking") class FLAXENGINE_API NetworkManager +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkManager); +public: + /// + /// The target amount of the network logic updates per second (frequency of replication, events sending and network ticking). Use 0 to run every game update. + /// + API_FIELD() static float NetworkFPS; + + /// + /// Current manager mode. + /// + API_FIELD(ReadOnly) static NetworkManagerMode Mode; + + /// + /// Current network connection state. + /// + API_FIELD(ReadOnly) static NetworkConnectionState State; + + /// + /// Local client, valid only when Network Manager is running in client or host mode (server doesn't have a client). + /// + API_FIELD(ReadOnly) static NetworkClient* LocalClient; + + /// + /// List of all clients: connecting, connected and disconnected. Empty on clients. + /// + API_FIELD(ReadOnly) static Array Clients; + +public: + /// + /// Event called when network manager state gets changed (eg. client connected to the server, or server shutdown started). + /// + API_EVENT() static Action StateChanged; + + /// + /// Event called when new client is connecting. Can be used to accept/deny connection. Called on client to fill PayloadData send and called on server/host to validate incoming client connection (eg. with PayloadData validation). + /// + API_EVENT() static Delegate ClientConnecting; + + /// + /// Event called after new client successfully connected. + /// + API_EVENT() static Delegate ClientConnected; + + /// + /// Event called after new client successfully disconnected. + /// + API_EVENT() static Delegate ClientDisconnected; + +public: + // Returns true if network is a client. + API_PROPERTY() FORCE_INLINE static bool IsClient() + { + return Mode == NetworkManagerMode::Client; + } + + // Returns true if network is a server. + API_PROPERTY() FORCE_INLINE static bool IsServer() + { + return Mode == NetworkManagerMode::Server; + } + + // Returns true if network is a host (both client and server). + API_PROPERTY() FORCE_INLINE static bool IsHost() + { + return Mode == NetworkManagerMode::Host; + } + + // Returns true if network is connected and online. + API_PROPERTY() FORCE_INLINE static bool IsConnected() + { + return State == NetworkConnectionState::Connected; + } + + /// + /// Gets the network client for a given connection. Returns null if failed to find it. + /// + /// Network connection identifier. + /// Found client or null. + API_FUNCTION() static NetworkClient* GetClient(API_PARAM(Ref) const NetworkConnection& connection); + +public: + /// + /// Starts the network in server mode. Returns true if failed (eg. invalid config). + /// + API_FUNCTION() static bool StartServer(); + + /// + /// Starts the network in client mode. Returns true if failed (eg. invalid config). + /// + API_FUNCTION() static bool StartClient(); + + /// + /// Starts the network in host mode. Returns true if failed (eg. invalid config). + /// + API_FUNCTION() static bool StartHost(); + + /// + /// Stops the network. + /// + API_FUNCTION() static void Stop(); +}; diff --git a/Source/Engine/Networking/NetworkSettings.h b/Source/Engine/Networking/NetworkSettings.h index 9802737c6..f2cd6b2c6 100644 --- a/Source/Engine/Networking/NetworkSettings.h +++ b/Source/Engine/Networking/NetworkSettings.h @@ -19,6 +19,12 @@ public: API_FIELD(Attributes="EditorOrder(0), EditorDisplay(\"General\")") int32 MaxClients = 100; + /// + /// Network protocol version of the game. Network clients and server can use only the same protocol version (verified upon client joining). + /// + API_FIELD(Attributes="EditorOrder(10), EditorDisplay(\"General\")") + uint32 ProtocolVersion = 1; + /// /// The target amount of the network system updates per second. Higher values provide better network synchronization (eg. 60 for shooters), lower values reduce network usage and performance impact (eg. 30 for strategy games). Can be used to tweak networking performance impact on game. Cannot be higher that UpdateFPS (from Time Settings). Use 0 to run every game update. /// diff --git a/Source/Engine/Networking/Types.h b/Source/Engine/Networking/Types.h index 42c8ac213..fbf9f918f 100644 --- a/Source/Engine/Networking/Types.h +++ b/Source/Engine/Networking/Types.h @@ -4,9 +4,11 @@ enum class NetworkChannelType; enum class NetworkEventType; +enum class NetworkConnectionState; class INetworkDriver; class NetworkPeer; +class NetworkClient; struct NetworkEvent; struct NetworkConnection;