Add objects spawning to networking
This commit is contained in:
@@ -10,6 +10,7 @@ enum class NetworkMessageIDs : uint8
|
||||
Handshake,
|
||||
HandshakeReply,
|
||||
ReplicatedObject,
|
||||
SpawnObject,
|
||||
|
||||
MAX,
|
||||
};
|
||||
@@ -17,8 +18,11 @@ enum class NetworkMessageIDs : uint8
|
||||
class NetworkInternal
|
||||
{
|
||||
public:
|
||||
static void NetworkReplicatorClientConnected(NetworkClient* client);
|
||||
static void NetworkReplicatorClientDisconnected(NetworkClient* client);
|
||||
static void NetworkReplicatorClear();
|
||||
static void NetworkReplicatorPreUpdate();
|
||||
static void NetworkReplicatorUpdate();
|
||||
static void OnNetworkMessageReplicatedObject(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
|
||||
static void OnNetworkMessageSpawnObject(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer);
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ NetworkPeer* NetworkManager::Peer = nullptr;
|
||||
NetworkManagerMode NetworkManager::Mode = NetworkManagerMode::Offline;
|
||||
NetworkConnectionState NetworkManager::State = NetworkConnectionState::Offline;
|
||||
uint32 NetworkManager::Frame = 0;
|
||||
uint32 NetworkManager::LocalClientId = 0;
|
||||
NetworkClient* NetworkManager::LocalClient = nullptr;
|
||||
Array<NetworkClient*> NetworkManager::Clients;
|
||||
Action NetworkManager::StateChanged;
|
||||
@@ -38,7 +39,7 @@ namespace
|
||||
|
||||
PACK_STRUCT(struct NetworkMessageHandshake
|
||||
{
|
||||
NetworkMessageIDs ID;
|
||||
NetworkMessageIDs ID = NetworkMessageIDs::Handshake;
|
||||
uint32 EngineBuild;
|
||||
uint32 EngineProtocolVersion;
|
||||
uint32 GameProtocolVersion;
|
||||
@@ -49,7 +50,7 @@ PACK_STRUCT(struct NetworkMessageHandshake
|
||||
|
||||
PACK_STRUCT(struct NetworkMessageHandshakeReply
|
||||
{
|
||||
NetworkMessageIDs ID;
|
||||
NetworkMessageIDs ID = NetworkMessageIDs::HandshakeReply;
|
||||
uint32 ClientId;
|
||||
int32 Result;
|
||||
});
|
||||
@@ -75,7 +76,6 @@ void OnNetworkMessageHandshake(NetworkEvent& event, NetworkClient* client, Netwo
|
||||
|
||||
// Reply to the handshake message with a result
|
||||
NetworkMessageHandshakeReply replyData;
|
||||
replyData.ID = NetworkMessageIDs::HandshakeReply;
|
||||
replyData.Result = connectionData.Result;
|
||||
replyData.ClientId = client->ClientId;
|
||||
NetworkMessage msgReply = peer->BeginSendMessage();
|
||||
@@ -95,6 +95,7 @@ void OnNetworkMessageHandshake(NetworkEvent& event, NetworkClient* client, Netwo
|
||||
client->State = NetworkConnectionState::Connected;
|
||||
LOG(Info, "Client id={0} connected", event.Sender.ConnectionId);
|
||||
NetworkManager::ClientConnected(client);
|
||||
NetworkInternal::NetworkReplicatorClientConnected(client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +113,7 @@ void OnNetworkMessageHandshakeReply(NetworkEvent& event, NetworkClient* client,
|
||||
}
|
||||
|
||||
// Client got connected with server
|
||||
NetworkManager::LocalClientId = msgData.ClientId;
|
||||
NetworkManager::LocalClient->ClientId = msgData.ClientId;
|
||||
NetworkManager::LocalClient->State = NetworkConnectionState::Connected;
|
||||
NetworkManager::State = NetworkConnectionState::Connected;
|
||||
@@ -127,6 +129,7 @@ namespace
|
||||
OnNetworkMessageHandshake,
|
||||
OnNetworkMessageHandshakeReply,
|
||||
NetworkInternal::OnNetworkMessageReplicatedObject,
|
||||
NetworkInternal::OnNetworkMessageSpawnObject,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -233,12 +236,14 @@ bool NetworkManager::StartServer()
|
||||
PROFILE_CPU();
|
||||
Stop();
|
||||
|
||||
LOG(Info, "Starting network manager as server");
|
||||
Mode = NetworkManagerMode::Server;
|
||||
if (StartPeer())
|
||||
return true;
|
||||
if (!Peer->Listen())
|
||||
return true;
|
||||
NextClientId++;
|
||||
LocalClientId = ServerClientId;
|
||||
NextClientId = ServerClientId + 1;
|
||||
|
||||
State = NetworkConnectionState::Connected;
|
||||
StateChanged();
|
||||
@@ -250,12 +255,15 @@ bool NetworkManager::StartClient()
|
||||
PROFILE_CPU();
|
||||
Stop();
|
||||
|
||||
LOG(Info, "Starting network manager as client");
|
||||
Mode = NetworkManagerMode::Client;
|
||||
if (StartPeer())
|
||||
return true;
|
||||
if (!Peer->Connect())
|
||||
return true;
|
||||
LocalClient = New<NetworkClient>(0, NetworkConnection{ 0 });
|
||||
LocalClientId = 0; // Id gets assigned by server later after connection
|
||||
NextClientId = 0;
|
||||
LocalClient = New<NetworkClient>(LocalClientId, NetworkConnection{ 0 });
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -265,12 +273,15 @@ bool NetworkManager::StartHost()
|
||||
PROFILE_CPU();
|
||||
Stop();
|
||||
|
||||
LOG(Info, "Starting network manager as host");
|
||||
Mode = NetworkManagerMode::Host;
|
||||
if (StartPeer())
|
||||
return true;
|
||||
if (!Peer->Listen())
|
||||
return true;
|
||||
LocalClient = New<NetworkClient>(NextClientId++, NetworkConnection{ 0 });
|
||||
LocalClientId = ServerClientId;
|
||||
NextClientId = ServerClientId + 1;
|
||||
LocalClient = New<NetworkClient>(LocalClientId, NetworkConnection{ 0 });
|
||||
|
||||
// Auto-connect host
|
||||
LocalClient->State = NetworkConnectionState::Connected;
|
||||
@@ -287,6 +298,7 @@ void NetworkManager::Stop()
|
||||
return;
|
||||
PROFILE_CPU();
|
||||
|
||||
LOG(Info, "Stopping network manager");
|
||||
State = NetworkConnectionState::Disconnecting;
|
||||
if (LocalClient)
|
||||
LocalClient->State = NetworkConnectionState::Disconnecting;
|
||||
@@ -355,7 +367,6 @@ void NetworkManagerService::Update()
|
||||
|
||||
// Send initial handshake message from client to server
|
||||
NetworkMessageHandshake msgData;
|
||||
msgData.ID = NetworkMessageIDs::Handshake;
|
||||
msgData.EngineBuild = FLAXENGINE_VERSION_BUILD;
|
||||
msgData.EngineProtocolVersion = NETWORK_PROTOCOL_VERSION;
|
||||
msgData.GameProtocolVersion = GameProtocolVersion;
|
||||
@@ -395,6 +406,7 @@ void NetworkManagerService::Update()
|
||||
client->State = NetworkConnectionState::Disconnecting;
|
||||
LOG(Info, "Client id={0} disconnected", event.Sender.ConnectionId);
|
||||
NetworkManager::Clients.RemoveKeepOrder(client);
|
||||
NetworkInternal::NetworkReplicatorClientDisconnected(client);
|
||||
NetworkManager::ClientDisconnected(client);
|
||||
client->State = NetworkConnectionState::Disconnected;
|
||||
Delete(client);
|
||||
|
||||
@@ -74,6 +74,16 @@ public:
|
||||
/// </summary>
|
||||
API_FIELD(ReadOnly) static uint32 Frame;
|
||||
|
||||
/// <summary>
|
||||
/// Server client identifier. Constant value of 0.
|
||||
/// </summary>
|
||||
API_FIELD(ReadOnly) static constexpr uint32 ServerClientId = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Local client identifier. Valid even on server that doesn't have LocalClient.
|
||||
/// </summary>
|
||||
API_FIELD(ReadOnly) static uint32 LocalClientId;
|
||||
|
||||
/// <summary>
|
||||
/// Local client, valid only when Network Manager is running in client or host mode (server doesn't have a client).
|
||||
/// </summary>
|
||||
|
||||
@@ -26,22 +26,34 @@
|
||||
|
||||
PACK_STRUCT(struct NetworkMessageReplicatedObject
|
||||
{
|
||||
NetworkMessageIDs ID;
|
||||
NetworkMessageIDs ID = NetworkMessageIDs::ReplicatedObject;
|
||||
uint32 OwnerFrame;
|
||||
Guid ObjectId; // TODO: introduce networked-ids to synchronize unique ids as ushort (less data over network)
|
||||
Guid OwnerId;
|
||||
Guid ParentId;
|
||||
char ObjectTypeName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
|
||||
uint16 DataSize;
|
||||
});
|
||||
|
||||
PACK_STRUCT(struct NetworkMessageSpawnObject
|
||||
{
|
||||
NetworkMessageIDs ID = NetworkMessageIDs::SpawnObject;
|
||||
Guid ObjectId;
|
||||
Guid ParentId;
|
||||
uint32 OwnerClientId;
|
||||
char ObjectTypeName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
|
||||
});
|
||||
|
||||
struct NetworkReplicatedObject
|
||||
{
|
||||
ScriptingObjectReference<ScriptingObject> Object;
|
||||
Guid ObjectId;
|
||||
Guid OwnerId;
|
||||
uint32 LastOwnerFrameSync = 0;
|
||||
Guid ParentId;
|
||||
uint32 OwnerClientId;
|
||||
uint32 LastOwnerFrame = 0;
|
||||
NetworkObjectRole Role;
|
||||
uint8 Spawned = false;
|
||||
#if NETWORK_REPLICATOR_DEBUG_LOG
|
||||
bool InvalidTypeWarn = false;
|
||||
uint8 InvalidTypeWarn = false;
|
||||
#endif
|
||||
|
||||
bool operator==(const NetworkReplicatedObject& other) const
|
||||
@@ -80,9 +92,11 @@ namespace
|
||||
{
|
||||
CriticalSection ObjectsLock;
|
||||
HashSet<NetworkReplicatedObject> Objects;
|
||||
Array<ScriptingObjectReference<ScriptingObject>> SpawnQueue;
|
||||
Dictionary<Guid, Guid> IdsRemappingTable;
|
||||
NetworkStream* CachedWriteStream = nullptr;
|
||||
NetworkStream* CachedReadStream = nullptr;
|
||||
Array<NetworkClient*> NewClients;
|
||||
Array<NetworkConnection> CachedTargets;
|
||||
Dictionary<ScriptingTypeHandle, Serializer> SerializersTable;
|
||||
}
|
||||
@@ -115,16 +129,25 @@ void INetworkSerializable_Deserialize(void* instance, NetworkStream* stream, voi
|
||||
((INetworkSerializable*)instance)->Deserialize(stream);
|
||||
}
|
||||
|
||||
NetworkReplicatedObject* ResolveObject(Guid objectId, Guid ownerId, char objectTypeName[128])
|
||||
NetworkReplicatedObject* ResolveObject(Guid objectId)
|
||||
{
|
||||
// Lookup object
|
||||
IdsRemappingTable.TryGet(objectId, objectId);
|
||||
const auto it = Objects.Find(objectId);
|
||||
auto it = Objects.Find(objectId);
|
||||
if (it != Objects.End())
|
||||
return &it->Item;
|
||||
IdsRemappingTable.TryGet(objectId, objectId);
|
||||
it = Objects.Find(objectId);
|
||||
return it != Objects.End() ? &it->Item : nullptr;
|
||||
}
|
||||
|
||||
NetworkReplicatedObject* ResolveObject(Guid objectId, Guid parentId, char objectTypeName[128])
|
||||
{
|
||||
// Lookup object
|
||||
NetworkReplicatedObject* obj = ResolveObject(objectId);
|
||||
if (obj)
|
||||
return obj;
|
||||
|
||||
// Try to find the object within the same parent (eg. spawned locally on both client and server)
|
||||
IdsRemappingTable.TryGet(ownerId, ownerId);
|
||||
IdsRemappingTable.TryGet(parentId, parentId);
|
||||
const ScriptingTypeHandle objectType = Scripting::FindScriptingType(StringAnsiView(objectTypeName));
|
||||
if (!objectType)
|
||||
return nullptr;
|
||||
@@ -132,8 +155,8 @@ NetworkReplicatedObject* ResolveObject(Guid objectId, Guid ownerId, char objectT
|
||||
{
|
||||
auto& item = e.Item;
|
||||
const ScriptingObject* obj = item.Object.Get();
|
||||
if (item.OwnerId == ownerId &&
|
||||
item.LastOwnerFrameSync == 0 &&
|
||||
if (item.LastOwnerFrame == 0 &&
|
||||
item.ParentId == parentId &&
|
||||
obj &&
|
||||
obj->GetTypeHandle() == objectType)
|
||||
{
|
||||
@@ -204,11 +227,11 @@ bool NetworkReplicator::InvokeSerializer(const ScriptingTypeHandle& typeHandle,
|
||||
return false;
|
||||
}
|
||||
|
||||
void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* owner)
|
||||
void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* parent)
|
||||
{
|
||||
if (!obj || NetworkManager::State == NetworkConnectionState::Offline)
|
||||
return;
|
||||
CHECK(owner && owner != obj);
|
||||
CHECK(parent && parent != obj);
|
||||
ScopeLock lock(ObjectsLock);
|
||||
if (Objects.Contains(obj))
|
||||
return;
|
||||
@@ -217,13 +240,71 @@ void NetworkReplicator::AddObject(ScriptingObject* obj, ScriptingObject* owner)
|
||||
NetworkReplicatedObject item;
|
||||
item.Object = obj;
|
||||
item.ObjectId = obj->GetID();
|
||||
item.OwnerId = owner->GetID();
|
||||
item.ParentId = parent->GetID();
|
||||
item.OwnerClientId = NetworkManager::ServerClientId; // Server owns objects by default
|
||||
item.Role = NetworkManager::IsClient() ? NetworkObjectRole::Replicated : NetworkObjectRole::OwnedAuthoritative;
|
||||
#if NETWORK_REPLICATOR_DEBUG_LOG
|
||||
LOG(Info, "[NetworkReplicator] Add new object {}:{}, owned by {}:{}", item.ToString(), obj->GetType().ToString(), item.OwnerId.ToString(), owner->GetType().ToString());
|
||||
LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent->GetType().ToString());
|
||||
#endif
|
||||
Objects.Add(MoveTemp(item));
|
||||
}
|
||||
|
||||
void NetworkReplicator::SpawnObject(ScriptingObject* obj)
|
||||
{
|
||||
if (!obj || NetworkManager::State == NetworkConnectionState::Offline)
|
||||
return;
|
||||
ScopeLock lock(ObjectsLock);
|
||||
auto it = Objects.Find(obj->GetID());
|
||||
if (it == Objects.End())
|
||||
{
|
||||
// Ensure that object is added to the replication locally
|
||||
AddObject(obj, nullptr);
|
||||
it = Objects.Find(obj->GetID());
|
||||
}
|
||||
|
||||
// Register for spawning (batched spawning during update)
|
||||
ASSERT_LOW_LAYER(!SpawnQueue.Contains(obj));
|
||||
SpawnQueue.Add(obj);
|
||||
}
|
||||
|
||||
uint32 NetworkReplicator::GetObjectClientId(ScriptingObject* obj)
|
||||
{
|
||||
uint32 id = 0;
|
||||
if (obj)
|
||||
{
|
||||
ScopeLock lock(ObjectsLock);
|
||||
const auto it = Objects.Find(obj->GetID());
|
||||
if (it != Objects.End())
|
||||
id = it->Item.OwnerClientId;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
NetworkObjectRole NetworkReplicator::GetObjectRole(ScriptingObject* obj)
|
||||
{
|
||||
NetworkObjectRole role = NetworkObjectRole::None;
|
||||
if (obj)
|
||||
{
|
||||
ScopeLock lock(ObjectsLock);
|
||||
const auto it = Objects.Find(obj->GetID());
|
||||
if (it != Objects.End())
|
||||
role = it->Item.Role;
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
void NetworkInternal::NetworkReplicatorClientConnected(NetworkClient* client)
|
||||
{
|
||||
ScopeLock lock(ObjectsLock);
|
||||
NewClients.Add(client);
|
||||
}
|
||||
|
||||
void NetworkInternal::NetworkReplicatorClientDisconnected(NetworkClient* client)
|
||||
{
|
||||
ScopeLock lock(ObjectsLock);
|
||||
NewClients.Remove(client);
|
||||
}
|
||||
|
||||
void NetworkInternal::NetworkReplicatorClear()
|
||||
{
|
||||
ScopeLock lock(ObjectsLock);
|
||||
@@ -238,6 +319,7 @@ void NetworkInternal::NetworkReplicatorClear()
|
||||
IdsRemappingTable.SetCapacity(0);
|
||||
SAFE_DELETE(CachedWriteStream);
|
||||
SAFE_DELETE(CachedReadStream);
|
||||
NewClients.Clear();
|
||||
CachedTargets.Resize(0);
|
||||
}
|
||||
|
||||
@@ -255,30 +337,105 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
return;
|
||||
if (CachedWriteStream == nullptr)
|
||||
CachedWriteStream = New<NetworkStream>();
|
||||
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)
|
||||
// TODO: network authority (eg. object owned by client that can affect server)
|
||||
|
||||
if (NetworkManager::IsClient())
|
||||
if (!isClient && NewClients.Count() != 0)
|
||||
{
|
||||
// TODO: client logic to apply replication changes
|
||||
// TODO: client logic to send owned objects to the server
|
||||
// Sync any previously spawned objects with late-joining clients
|
||||
PROFILE_CPU_NAMED("NewClients");
|
||||
// TODO: try iterative loop over several frames to reduce both server and client perf-spikes in case of large amount of spawned objects
|
||||
CachedTargets.Clear();
|
||||
for (NetworkClient* client : NewClients)
|
||||
CachedTargets.Add(client->Connection);
|
||||
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
|
||||
{
|
||||
auto& item = it->Item;
|
||||
ScriptingObject* obj = item.Object.Get();
|
||||
if (!obj || !item.Spawned)
|
||||
continue;
|
||||
|
||||
// Send spawn message
|
||||
NetworkMessageSpawnObject msgData;
|
||||
msgData.ObjectId = item.ObjectId;
|
||||
msgData.ParentId = item.ParentId;
|
||||
msgData.OwnerClientId = item.OwnerClientId;
|
||||
// TODO: support spawning whole prefabs
|
||||
const StringAnsiView& objectTypeName = obj->GetType().Fullname;
|
||||
Platform::MemoryCopy(msgData.ObjectTypeName, objectTypeName.Get(), objectTypeName.Length());
|
||||
msgData.ObjectTypeName[objectTypeName.Length()] = 0;
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgData);
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
|
||||
}
|
||||
NewClients.Clear();
|
||||
}
|
||||
|
||||
// Collect clients for replication (from server)
|
||||
CachedTargets.Clear();
|
||||
for (const NetworkClient* client : NetworkManager::Clients)
|
||||
{
|
||||
if (client->State == NetworkConnectionState::Connected)
|
||||
CachedTargets.Add(client->Connection);
|
||||
}
|
||||
if (!isClient && CachedTargets.Count() == 0)
|
||||
{
|
||||
// Early exit if server has nobody to send data to
|
||||
Scripting::ObjectsLookupIdMapping.Set(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawn
|
||||
if (SpawnQueue.Count() != 0)
|
||||
{
|
||||
PROFILE_CPU_NAMED("SpawnQueue");
|
||||
for (ScriptingObjectReference<ScriptingObject>& e : SpawnQueue)
|
||||
{
|
||||
ScriptingObject* obj = e.Get();
|
||||
const auto it = Objects.Find(obj->GetID());
|
||||
if (it == Objects.End())
|
||||
continue; // Skip deleted objects
|
||||
auto& item = it->Item;
|
||||
if (item.OwnerClientId != NetworkManager::LocalClientId || item.Role != NetworkObjectRole::OwnedAuthoritative)
|
||||
continue; // Skip spawning objects that we don't own
|
||||
|
||||
// Send spawn message
|
||||
NetworkMessageSpawnObject msgData;
|
||||
msgData.ObjectId = item.ObjectId;
|
||||
msgData.ParentId = item.ParentId;
|
||||
msgData.OwnerClientId = item.OwnerClientId;
|
||||
if (isClient)
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId);
|
||||
IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId);
|
||||
}
|
||||
// TODO: support spawning whole prefabs
|
||||
const StringAnsiView& objectTypeName = obj->GetType().Fullname;
|
||||
Platform::MemoryCopy(msgData.ObjectTypeName, objectTypeName.Get(), objectTypeName.Length());
|
||||
msgData.ObjectTypeName[objectTypeName.Length()] = 0;
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
msg.WriteStructure(msgData);
|
||||
if (isClient)
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
|
||||
else
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
|
||||
|
||||
item.Spawned = true;
|
||||
}
|
||||
SpawnQueue.Clear();
|
||||
}
|
||||
|
||||
if (isClient)
|
||||
{
|
||||
// TODO: client logic to replicate owned objects to the server
|
||||
}
|
||||
else
|
||||
{
|
||||
// Collect clients for replication
|
||||
CachedTargets.Clear();
|
||||
// TODO: per-object relevancy for connected clients (eg. skip replicating actor to far players)
|
||||
for (const NetworkClient* client : NetworkManager::Clients)
|
||||
{
|
||||
if (client->State == NetworkConnectionState::Connected)
|
||||
{
|
||||
CachedTargets.Add(client->Connection);
|
||||
}
|
||||
}
|
||||
|
||||
// Brute force synchronize all networked objects with clients
|
||||
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
|
||||
{
|
||||
@@ -288,7 +445,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
{
|
||||
// Object got deleted
|
||||
#if NETWORK_REPLICATOR_DEBUG_LOG
|
||||
LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.OwnerId.ToString());
|
||||
LOG(Info, "[NetworkReplicator] Remove object {}, owned by {}", item.ToString(), item.ParentId.ToString());
|
||||
#endif
|
||||
Objects.Remove(it);
|
||||
continue;
|
||||
@@ -314,10 +471,9 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
const uint32 size = stream->GetPosition();
|
||||
ASSERT(size <= MAX_uint16)
|
||||
NetworkMessageReplicatedObject msgData;
|
||||
msgData.ID = NetworkMessageIDs::ReplicatedObject;
|
||||
msgData.OwnerFrame = NetworkManager::Frame;
|
||||
msgData.ObjectId = item.ObjectId;
|
||||
msgData.OwnerId = item.OwnerId;
|
||||
msgData.ParentId = item.ParentId;
|
||||
const StringAnsiView& objectTypeName = obj->GetType().Fullname;
|
||||
Platform::MemoryCopy(msgData.ObjectTypeName, objectTypeName.Get(), objectTypeName.Length());
|
||||
msgData.ObjectTypeName[objectTypeName.Length()] = 0;
|
||||
@@ -326,6 +482,7 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
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)
|
||||
peer->EndSendMessage(NetworkChannelType::Unreliable, msg, CachedTargets);
|
||||
|
||||
// TODO: stats for bytes send per object type
|
||||
@@ -342,7 +499,7 @@ void NetworkInternal::OnNetworkMessageReplicatedObject(NetworkEvent& event, Netw
|
||||
NetworkMessageReplicatedObject msgData;
|
||||
event.Message.ReadStructure(msgData);
|
||||
ScopeLock lock(ObjectsLock);
|
||||
NetworkReplicatedObject* e = ResolveObject(msgData.ObjectId, msgData.OwnerId, msgData.ObjectTypeName);
|
||||
NetworkReplicatedObject* e = ResolveObject(msgData.ObjectId, msgData.ParentId, msgData.ObjectTypeName);
|
||||
if (e)
|
||||
{
|
||||
auto& item = *e;
|
||||
@@ -350,10 +507,15 @@ void NetworkInternal::OnNetworkMessageReplicatedObject(NetworkEvent& event, Netw
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
// Drop object replication if it has old data (eg. newer message was already processed due to unordered channel usage)
|
||||
if (item.LastOwnerFrameSync >= msgData.OwnerFrame)
|
||||
// Reject replication from someone who is not an object owner
|
||||
if (client && e->OwnerClientId != client->ClientId)
|
||||
return;
|
||||
item.LastOwnerFrameSync = msgData.OwnerFrame;
|
||||
ASSERT(e->Role != NetworkObjectRole::OwnedAuthoritative); // Ensure that we don't replicate object that we own
|
||||
|
||||
// Drop object replication if it has old data (eg. newer message was already processed due to unordered channel usage)
|
||||
if (item.LastOwnerFrame >= msgData.OwnerFrame)
|
||||
return;
|
||||
item.LastOwnerFrame = msgData.OwnerFrame;
|
||||
|
||||
// Setup message reading stream
|
||||
if (CachedReadStream == nullptr)
|
||||
@@ -379,3 +541,65 @@ void NetworkInternal::OnNetworkMessageReplicatedObject(NetworkEvent& event, Netw
|
||||
// TODO: put message to the queue to be resolved later (eg. object replication came before spawn packet) - use TTL to prevent memory overgrowing
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkInternal::OnNetworkMessageSpawnObject(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
||||
{
|
||||
NetworkMessageSpawnObject msgData;
|
||||
event.Message.ReadStructure(msgData);
|
||||
ScopeLock lock(ObjectsLock);
|
||||
NetworkReplicatedObject* e = ResolveObject(msgData.ObjectId, msgData.ParentId, msgData.ObjectTypeName);
|
||||
if (e)
|
||||
{
|
||||
auto& item = *e;
|
||||
item.Spawned = true;
|
||||
if (NetworkManager::IsClient())
|
||||
{
|
||||
// Server always knows the best so update ownership of the existing object
|
||||
item.OwnerClientId = msgData.OwnerClientId;
|
||||
if (item.Role == NetworkObjectRole::OwnedAuthoritative)
|
||||
item.Role = NetworkObjectRole::Replicated;
|
||||
}
|
||||
else if (item.OwnerClientId != msgData.OwnerClientId)
|
||||
{
|
||||
// Other client spawned object with a different owner
|
||||
// TODO: send reply message to inform about proper object ownership that client
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recreate object locally
|
||||
// TODO: support spawning whole prefabs
|
||||
const ScriptingTypeHandle objectType = Scripting::FindScriptingType(StringAnsiView(msgData.ObjectTypeName));
|
||||
ScriptingObject* obj = ScriptingObject::NewObject(objectType);
|
||||
if (!obj)
|
||||
{
|
||||
#if NETWORK_REPLICATOR_DEBUG_LOG
|
||||
LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(msgData.ObjectTypeName));
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
obj->RegisterObject();
|
||||
const NetworkReplicatedObject* parent = ResolveObject(msgData.ParentId);
|
||||
|
||||
// Add object to the list
|
||||
NetworkReplicatedObject item;
|
||||
item.Object = obj;
|
||||
item.ObjectId = obj->GetID();
|
||||
item.ParentId = parent ? parent->ObjectId : Guid::Empty;
|
||||
item.OwnerClientId = client ? client->ClientId : NetworkManager::ServerClientId;
|
||||
item.Role = NetworkObjectRole::Replicated;
|
||||
item.Spawned = true;
|
||||
#if NETWORK_REPLICATOR_DEBUG_LOG
|
||||
LOG(Info, "[NetworkReplicator] Add new object {}:{}, parent {}:{}", item.ToString(), obj->GetType().ToString(), item.ParentId.ToString(), parent ? parent->Object->GetType().ToString() : String::Empty);
|
||||
#endif
|
||||
Objects.Add(MoveTemp(item));
|
||||
|
||||
// Boost future lookups by using indirection
|
||||
#if NETWORK_REPLICATOR_DEBUG_LOG
|
||||
LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgData.ObjectId, item.ToString(), obj->GetType().ToString());
|
||||
#endif
|
||||
IdsRemappingTable.Add(msgData.ObjectId, item.ObjectId);
|
||||
|
||||
// TODO: if we're server then spawn this object further on other clients
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
#include "Engine/Scripting/ScriptingObject.h"
|
||||
#include "Engine/Scripting/ScriptingType.h"
|
||||
|
||||
/// <summary>
|
||||
/// The high-level network object role and authority. Used to define who owns the object and when it can be simulated or just replicated.
|
||||
/// </summary>
|
||||
API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkObjectRole : byte
|
||||
{
|
||||
// Not replicated object.
|
||||
None = 0,
|
||||
// Server/client owns the object and replicates it to others. Only owning client can simulate object and provides current state.
|
||||
OwnedAuthoritative,
|
||||
// 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,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// High-level networking replication system for game objects.
|
||||
/// </summary>
|
||||
@@ -41,8 +56,29 @@ public:
|
||||
/// </summary>
|
||||
/// <remarks>Does nothing if network is offline.</remarks>
|
||||
/// <param name="obj">The object to replicate.</param>
|
||||
/// <param name="owner">The owner of the object (eg. player that spawned it).</param>
|
||||
API_FUNCTION() static void AddObject(ScriptingObject* obj, ScriptingObject* owner);
|
||||
/// <param name="parent">The parent of the object (eg. player that spawned it).</param>
|
||||
API_FUNCTION() static void AddObject(ScriptingObject* obj, ScriptingObject* parent);
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the object to the other clients. Can be spawned by the owner who locally created it (eg. from prefab).
|
||||
/// </summary>
|
||||
/// <remarks>Does nothing if network is offline.</remarks>
|
||||
/// <param name="obj">The object to spawn on other clients.</param>
|
||||
API_FUNCTION() static void SpawnObject(ScriptingObject* obj);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Client Id of the network object owner.
|
||||
/// </summary>
|
||||
/// <param name="obj">The network object.</param>
|
||||
/// <returns>The Client Id.</returns>
|
||||
API_FUNCTION() static uint32 GetObjectClientId(ScriptingObject* obj);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the role of the network object used locally (eg. to check if can simulate object).
|
||||
/// </summary>
|
||||
/// <param name="obj">The network object.</param>
|
||||
/// <returns>The object role.</returns>
|
||||
API_FUNCTION() static NetworkObjectRole GetObjectRole(ScriptingObject* obj);
|
||||
|
||||
private:
|
||||
#if !COMPILE_WITHOUT_CSHARP
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Flax.Build.Plugins
|
||||
internal const string NetworkReplicated = "NetworkReplicated";
|
||||
private const string Thunk1 = "INetworkSerializable_Serialize";
|
||||
private const string Thunk2 = "INetworkSerializable_Deserialize";
|
||||
private static Dictionary<string, InBuildSerializer> _inBuildSerializers = new Dictionary<string, InBuildSerializer>()
|
||||
private static readonly Dictionary<string, InBuildSerializer> _inBuildSerializers = new Dictionary<string, InBuildSerializer>()
|
||||
{
|
||||
{ "System.Boolean", new InBuildSerializer("WriteBoolean", "ReadBoolean") },
|
||||
{ "System.Single", new InBuildSerializer("WriteSingle", "ReadSingle") },
|
||||
|
||||
Reference in New Issue
Block a user