Add INetworkObject and implement various networking features

This commit is contained in:
Wojciech Figat
2022-11-02 16:47:00 +01:00
parent 957a34866b
commit b816e2b3a9
3 changed files with 199 additions and 61 deletions

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Compiler.h"
#include "Engine/Core/Config.h"
/// <summary>
/// Interface for objects for network replication to receive additional events.
/// </summary>
API_INTERFACE(Namespace = "FlaxEngine.Networking") class FLAXENGINE_API INetworkObject
{
DECLARE_SCRIPTING_TYPE_MINIMAL(INetworkObject);
public:
/// <summary>
/// Event called when network objects gets spawned.
/// </summary>
API_FUNCTION() virtual void OnNetworkSpawn() = 0;
/// <summary>
/// Event called when network objects gets despawned.
/// </summary>
API_FUNCTION() virtual void OnNetworkDespawn() = 0;
/// <summary>
/// Event called before network object gets replicated (before reading data).
/// </summary>
API_FUNCTION() virtual void OnNetworkSerialize() = 0;
/// <summary>
/// Event called when network objects gets replicated (after reading data).
/// </summary>
API_FUNCTION() virtual void OnNetworkDeserialize() = 0;
};

View File

@@ -5,16 +5,16 @@
#include "NetworkManager.h"
#include "NetworkInternal.h"
#include "NetworkStream.h"
#include "INetworkSerializable.h"
#include "NetworkMessage.h"
#include "NetworkPeer.h"
#include "NetworkChannelType.h"
#include "NetworkEvent.h"
#include "INetworkSerializable.h"
#include "INetworkObject.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/HashSet.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Core/Types/DataContainer.h"
#include "Engine/Core/Types/Pair.h"
#include "Engine/Platform/CriticalSection.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Level/Actor.h"
@@ -84,6 +84,7 @@ struct NetworkReplicatedObject
uint8 InvalidTypeWarn = false;
#endif
DataContainer<uint32> TargetClientIds;
INetworkObject* AsNetworkObject;
bool operator==(const NetworkReplicatedObject& other) const
{
@@ -121,6 +122,9 @@ struct SpawnItem
{
ScriptingObjectReference<ScriptingObject> Object;
DataContainer<uint32> Targets;
bool HasOwnership = false;
uint32 OwnerClientId;
NetworkObjectRole Role;
};
namespace
@@ -212,7 +216,6 @@ NetworkReplicatedObject* ResolveObject(Guid objectId, Guid parentId, char object
void BuildCachedTargets(const Array<NetworkClient*>& clients)
{
CachedTargets.Clear();
// TODO: skip building cached targets if previous frame send it to all clients
for (const NetworkClient* client : clients)
{
if (client->State == NetworkConnectionState::Connected)
@@ -230,14 +233,14 @@ void BuildCachedTargets(const Array<NetworkClient*>& clients, const NetworkClien
}
}
void BuildCachedTargets(const Array<NetworkClient*>& clients, const DataContainer<uint32>& clientIds)
void BuildCachedTargets(const Array<NetworkClient*>& clients, const DataContainer<uint32>& clientIds, const uint32 excludedClientId = NetworkManager::ServerClientId)
{
CachedTargets.Clear();
if (clientIds.IsValid())
{
for (const NetworkClient* client : clients)
{
if (client->State == NetworkConnectionState::Connected)
if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId)
{
for (int32 i = 0; i < clientIds.Length(); i++)
{
@@ -252,10 +255,9 @@ void BuildCachedTargets(const Array<NetworkClient*>& clients, const DataContaine
}
else
{
// TODO: skip building cached targets if previous frame send it to all clients
for (const NetworkClient* client : clients)
{
if (client->State == NetworkConnectionState::Connected)
if (client->State == NetworkConnectionState::Connected && client->ClientId != excludedClientId)
CachedTargets.Add(client->Connection);
}
}
@@ -263,7 +265,8 @@ void BuildCachedTargets(const Array<NetworkClient*>& clients, const DataContaine
FORCE_INLINE void BuildCachedTargets(const NetworkReplicatedObject& item)
{
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds);
// By default send object to all connected clients excluding the owner but with optional TargetClientIds list
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds, item.OwnerClientId);
}
void SendObjectSpawnMessage(const NetworkReplicatedObject& item, ScriptingObject* obj)
@@ -389,7 +392,7 @@ bool NetworkReplicator::InvokeSerializer(const ScriptingTypeHandle& typeHandle,
else if (const ScriptingTypeHandle baseTypeHandle = typeHandle.GetType().GetBaseType())
{
// Fallback to base type
return InvokeSerializer(baseTypeHandle, instance, stream, serialize);;
return InvokeSerializer(baseTypeHandle, instance, stream, serialize);
}
else
return true;
@@ -420,6 +423,7 @@ void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* parent)
// Add object to the list
NetworkReplicatedObject item;
item.Object = obj;
item.AsNetworkObject = ScriptingObject::ToInterface<INetworkObject>(obj);
item.ObjectId = obj->GetID();
item.ParentId = parent ? parent->GetID() : Guid::Empty;
item.OwnerClientId = NetworkManager::ServerClientId; // Server owns objects by default
@@ -491,6 +495,8 @@ void NetworkReplicator::DespawnObject(ScriptingObject* obj)
}
// Delete object locally
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDespawn();
DeleteNetworkObject(obj);
}
@@ -527,7 +533,21 @@ void NetworkReplicator::SetObjectOwnership(ScriptingObject* obj, uint32 ownerCli
ScopeLock lock(ObjectsLock);
const auto it = Objects.Find(obj->GetID());
if (it == Objects.End())
{
// Special case if we're just spawning this object
for (int32 i = 0; i < SpawnQueue.Count(); i++)
{
auto& item = SpawnQueue[i];
if (item.Object == obj)
{
item.HasOwnership = true;
item.OwnerClientId = ownerClientId;
item.Role = localRole;
break;
}
}
return;
}
auto& item = it->Item;
if (item.Object != obj)
return;
@@ -581,6 +601,26 @@ void NetworkInternal::NetworkReplicatorClientDisconnected(NetworkClient* client)
{
ScopeLock lock(ObjectsLock);
NewClients.Remove(client);
// Remove any objects owned by that client
const uint32 clientId = client->ClientId;
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
{
auto& item = it->Item;
ScriptingObject* obj = item.Object.Get();
if (obj && item.Spawned && item.OwnerClientId == clientId)
{
// Register for despawning (batched during update)
const Guid id = obj->GetID();
DespawnQueue.Add(id);
// Delete object locally
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDespawn();
DeleteNetworkObject(obj);
Objects.Remove(it);
}
}
}
void NetworkInternal::NetworkReplicatorClear()
@@ -596,6 +636,8 @@ void NetworkInternal::NetworkReplicatorClear()
if (obj && item.Spawned)
{
// Cleanup any spawned objects
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDespawn();
DeleteNetworkObject(obj);
Objects.Remove(it);
}
@@ -626,8 +668,6 @@ void NetworkInternal::NetworkReplicatorUpdate()
const bool isClient = NetworkManager::IsClient();
NetworkStream* stream = CachedWriteStream;
NetworkPeer* peer = NetworkManager::Peer;
// TODO: introduce NetworkReplicationHierarchy to optimize objects replication in large worlds (eg. batched culling networked scene objects that are too far from certain client to be relevant)
// TODO: per-object sync interval (in frames) - could be scaled by hierarchy (eg. game could slow down sync rate for objects far from player)
if (!isClient && NewClients.Count() != 0)
{
@@ -701,6 +741,11 @@ void NetworkInternal::NetworkReplicatorUpdate()
if (item.OwnerClientId != NetworkManager::LocalClientId || item.Role != NetworkObjectRole::OwnedAuthoritative)
continue; // Skip spawning objects that we don't own
if (e.HasOwnership)
{
item.Role = e.Role;
item.OwnerClientId = e.OwnerClientId;
}
if (e.Targets.IsValid())
{
// TODO: if we spawn object with custom set of targets clientsIds on client, then send it over to the server
@@ -710,69 +755,80 @@ void NetworkInternal::NetworkReplicatorUpdate()
}
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Spawn object ID={}", item.ToString());
BuildCachedTargets(item);
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds);
SendObjectSpawnMessage(item, obj);
item.Spawned = true;
}
SpawnQueue.Clear();
}
if (isClient)
// Brute force synchronize all networked objects with clients
// TODO: introduce NetworkReplicationHierarchy to optimize objects replication in large worlds (eg. batched culling networked scene objects that are too far from certain client to be relevant)
// TODO: per-object sync interval (in frames) - could be scaled by hierarchy (eg. game could slow down sync rate for objects far from player)
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
{
// TODO: client logic to replicate owned objects to the server
}
else
{
// Brute force synchronize all networked objects with clients
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
auto& item = it->Item;
ScriptingObject* obj = item.Object.Get();
if (!obj)
{
auto& item = it->Item;
ScriptingObject* obj = item.Object.Get();
if (!obj)
{
// Object got deleted
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString());
Objects.Remove(it);
continue;
}
// Object got deleted
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString());
Objects.Remove(it);
continue;
}
if (item.Role != NetworkObjectRole::OwnedAuthoritative && (!isClient && item.OwnerClientId != NetworkManager::LocalClientId))
continue; // Send replication messages of only owned objects or from other client objects
// Serialize object
stream->Initialize();
const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true);
if (failed)
{
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkSerialize();
// Serialize object
stream->Initialize();
const bool failed = NetworkReplicator::InvokeSerializer(obj->GetTypeHandle(), obj, stream, true);
if (failed)
{
#if NETWORK_REPLICATOR_DEBUG_LOG
if (!item.InvalidTypeWarn)
{
item.InvalidTypeWarn = true;
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
}
#endif
continue;
}
// Send object to clients
if (!item.InvalidTypeWarn)
{
item.InvalidTypeWarn = true;
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Cannot serialize object {} of type {} (missing serialization logic)", item.ToString(), obj->GetType().ToString());
}
#endif
continue;
}
// Send object to clients
{
const uint32 size = stream->GetPosition();
ASSERT(size <= MAX_uint16)
NetworkMessageObjectReplicate msgData;
msgData.OwnerFrame = NetworkManager::Frame;
msgData.ObjectId = item.ObjectId;
msgData.ParentId = item.ParentId;
if (isClient)
{
// Remap local client object ids into server ids
IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId);
IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId);
}
const StringAnsiView& objectTypeName = obj->GetType().Fullname;
Platform::MemoryCopy(msgData.ObjectTypeName, objectTypeName.Get(), objectTypeName.Length());
msgData.ObjectTypeName[objectTypeName.Length()] = 0;
msgData.DataSize = size;
// TODO: split object data (eg. more messages) if needed
NetworkMessage msg = peer->BeginSendMessage();
msg.WriteStructure(msgData);
msg.WriteBytes(stream->GetBuffer(), size);
if (isClient)
peer->EndSendMessage(NetworkChannelType::Unreliable, msg);
else
{
const uint32 size = stream->GetPosition();
ASSERT(size <= MAX_uint16)
NetworkMessageObjectReplicate msgData;
msgData.OwnerFrame = NetworkManager::Frame;
msgData.ObjectId = item.ObjectId;
msgData.ParentId = item.ParentId;
const StringAnsiView& objectTypeName = obj->GetType().Fullname;
Platform::MemoryCopy(msgData.ObjectTypeName, objectTypeName.Get(), objectTypeName.Length());
msgData.ObjectTypeName[objectTypeName.Length()] = 0;
msgData.DataSize = size;
// TODO: split object data (eg. more messages) if needed
NetworkMessage msg = peer->BeginSendMessage();
msg.WriteStructure(msgData);
msg.WriteBytes(stream->GetBuffer(), size);
// TODO: per-object relevancy for connected clients (eg. skip replicating actor to far players)
BuildCachedTargets(item);
peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets);
// TODO: stats for bytes send per object type
}
// TODO: stats for bytes send per object type
}
}
@@ -825,6 +881,9 @@ void NetworkInternal::OnNetworkMessageObjectReplicate(NetworkEvent& event, Netwo
#endif
}
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDeserialize();
// TODO: speed up replication of client-owned object to other clients from server
}
else
@@ -937,10 +996,16 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
// Add object to the list
NetworkReplicatedObject item;
item.Object = obj;
item.AsNetworkObject = ScriptingObject::ToInterface<INetworkObject>(obj);
item.ObjectId = obj->GetID();
item.ParentId = parent ? parent->ObjectId : Guid::Empty;
item.OwnerClientId = client ? client->ClientId : NetworkManager::ServerClientId;
item.OwnerClientId = msgData.OwnerClientId;
item.Role = NetworkObjectRole::Replicated;
if (item.OwnerClientId == NetworkManager::LocalClientId)
{
// Upgrade ownership automatically (eg. server spawned object that local client should own)
item.Role = NetworkObjectRole::OwnedAuthoritative;
}
item.Spawned = true;
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->Object->GetType().ToString() : String::Empty);
Objects.Add(MoveTemp(item));
@@ -959,6 +1024,9 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
sceneObject->SetParent(parentActor);
}
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkSpawn();
// TODO: if we're server then spawn this object further on other clients (use TargetClientIds for that object - eg. object spawned by client on client for certain set of other clients only)
}
}
@@ -981,6 +1049,8 @@ void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, Network
return;
// Remove object
if (item.AsNetworkObject)
item.AsNetworkObject->OnNetworkDespawn();
Objects.Remove(obj);
DeleteNetworkObject(obj);
}

View File

@@ -21,7 +21,7 @@ API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkObjectRole : byte
// Server/client gets replicated object from other server/client who owns it. Object cannot be simulated locally (any changes will be overriden by replication).
Replicated,
// Client gets replicated object from server but still can locally autonomously simulate it too. For example, client can control local pawn with real human input but will validate with server proper state (eg. to prevent cheats).
ReplicatedAutonomous,
ReplicatedSimulated,
};
/// <summary>
@@ -105,6 +105,40 @@ public:
/// <returns>The object role.</returns>
API_FUNCTION() static NetworkObjectRole GetObjectRole(ScriptingObject* obj);
/// <summary>
/// Checks if the network object is owned locally (thus current client has authority to manage it).
/// </summary>
/// <remarks>Equivalent to GetObjectRole == OwnedAuthoritative.</remarks>
/// <param name="obj">The network object.</param>
/// <returns>True if object is owned by this client, otherwise false.</returns>
API_FUNCTION() FORCE_INLINE static bool IsObjectOwned(ScriptingObject* obj)
{
return GetObjectRole(obj) == NetworkObjectRole::OwnedAuthoritative;
}
/// <summary>
/// Checks if the network object is simulated locally (thus current client has can modify it - changed might be overriden by other client who owns this object).
/// </summary>
/// <remarks>Equivalent to GetObjectRole != Replicated.</remarks>
/// <param name="obj">The network object.</param>
/// <returns>True if object is simulated on this client, otherwise false.</returns>
API_FUNCTION() FORCE_INLINE static bool IsObjectSimulated(ScriptingObject* obj)
{
return GetObjectRole(obj) != NetworkObjectRole::Replicated;
}
/// <summary>
/// Checks if the network object is replicated locally (any local changes might be overriden by other client who owns this object).
/// </summary>
/// <remarks>Equivalent to (GetObjectRole == Replicated or GetObjectRole == ReplicatedAutonomous).</remarks>
/// <param name="obj">The network object.</param>
/// <returns>True if object is simulated on this client, otherwise false.</returns>
API_FUNCTION() FORCE_INLINE static bool IsObjectReplicated(ScriptingObject* obj)
{
const NetworkObjectRole role = GetObjectRole(obj);
return role == NetworkObjectRole::Replicated || role == NetworkObjectRole::ReplicatedSimulated;
}
/// <summary>
/// Sets the network object ownership - owning client identifier and local role to use.
/// </summary>