Refactor network object spawnign to batch all subobjects with a single spawn message to reduce invalid objects linkage on different clients
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
#include "Engine/Core/Log.h"
|
||||
#include "Engine/Core/Collections/HashSet.h"
|
||||
#include "Engine/Core/Collections/Dictionary.h"
|
||||
#include "Engine/Core/Collections/ChunkedArray.h"
|
||||
#include "Engine/Core/Types/DataContainer.h"
|
||||
#include "Engine/Platform/CriticalSection.h"
|
||||
#include "Engine/Engine/EngineService.h"
|
||||
@@ -51,11 +52,16 @@ PACK_STRUCT(struct NetworkMessageObjectReplicate
|
||||
PACK_STRUCT(struct NetworkMessageObjectSpawn
|
||||
{
|
||||
NetworkMessageIDs ID = NetworkMessageIDs::ObjectSpawn;
|
||||
uint32 OwnerClientId;
|
||||
Guid PrefabId;
|
||||
uint16 ItemsCount;
|
||||
});
|
||||
|
||||
PACK_STRUCT(struct NetworkMessageObjectSpawnItem
|
||||
{
|
||||
Guid ObjectId;
|
||||
Guid ParentId;
|
||||
Guid PrefabId;
|
||||
Guid PrefabObjectID;
|
||||
uint32 OwnerClientId;
|
||||
char ObjectTypeName[128]; // TODO: introduce networked-name to synchronize unique names as ushort (less data over network)
|
||||
});
|
||||
|
||||
@@ -135,6 +141,11 @@ struct SpawnItem
|
||||
NetworkObjectRole Role;
|
||||
};
|
||||
|
||||
struct SpawnGroup
|
||||
{
|
||||
Array<SpawnItem*, InlinedAllocation<8>> Items;
|
||||
};
|
||||
|
||||
struct DespawnItem
|
||||
{
|
||||
Guid Id;
|
||||
@@ -309,35 +320,54 @@ FORCE_INLINE void GetNetworkName(char buffer[128], const StringAnsiView& name)
|
||||
buffer[name.Length()] = 0;
|
||||
}
|
||||
|
||||
void SendObjectSpawnMessage(const NetworkReplicatedObject& item, ScriptingObject* obj)
|
||||
void SendObjectSpawnMessage(const SpawnGroup& group, const Array<NetworkClient*>& clients)
|
||||
{
|
||||
NetworkMessageObjectSpawn msgData;
|
||||
msgData.ObjectId = item.ObjectId;
|
||||
msgData.ParentId = item.ParentId;
|
||||
const bool isClient = NetworkManager::IsClient();
|
||||
if (isClient)
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(msgData.ObjectId, &msgData.ObjectId);
|
||||
IdsRemappingTable.KeyOf(msgData.ParentId, &msgData.ParentId);
|
||||
}
|
||||
msgData.PrefabId = Guid::Empty;
|
||||
msgData.PrefabObjectID = Guid::Empty;
|
||||
auto* objScene = ScriptingObject::Cast<SceneObject>(obj);
|
||||
if (objScene && objScene->HasPrefabLink())
|
||||
{
|
||||
msgData.PrefabId = objScene->GetPrefabID();
|
||||
msgData.PrefabObjectID = objScene->GetPrefabObjectID();
|
||||
}
|
||||
msgData.OwnerClientId = item.OwnerClientId;
|
||||
GetNetworkName(msgData.ObjectTypeName, obj->GetType().Fullname);
|
||||
auto* peer = NetworkManager::Peer;
|
||||
NetworkMessage msg = peer->BeginSendMessage();
|
||||
NetworkMessageObjectSpawn msgData;
|
||||
msgData.ItemsCount = group.Items.Count();
|
||||
{
|
||||
// The first object is a root of the group (eg. prefab instance root actor)
|
||||
SpawnItem* e = group.Items[0];
|
||||
ScriptingObject* obj = e->Object.Get();
|
||||
msgData.OwnerClientId = e->OwnerClientId;
|
||||
auto* objScene = ScriptingObject::Cast<SceneObject>(obj);
|
||||
msgData.PrefabId = objScene && objScene->HasPrefabLink() ? objScene->GetPrefabID() : Guid::Empty;
|
||||
|
||||
// Setup clients that should receive this spawn message
|
||||
auto it = Objects.Find(obj->GetID());
|
||||
auto& item = it->Item;
|
||||
BuildCachedTargets(clients, item.TargetClientIds);
|
||||
}
|
||||
msg.WriteStructure(msgData);
|
||||
for (SpawnItem* e : group.Items)
|
||||
{
|
||||
ScriptingObject* obj = e->Object.Get();
|
||||
auto it = Objects.Find(obj->GetID());
|
||||
auto& item = it->Item;
|
||||
|
||||
// Add object into spawn message
|
||||
NetworkMessageObjectSpawnItem msgDataItem;
|
||||
msgDataItem.ObjectId = item.ObjectId;
|
||||
msgDataItem.ParentId = item.ParentId;
|
||||
if (isClient)
|
||||
{
|
||||
// Remap local client object ids into server ids
|
||||
IdsRemappingTable.KeyOf(msgDataItem.ObjectId, &msgDataItem.ObjectId);
|
||||
IdsRemappingTable.KeyOf(msgDataItem.ParentId, &msgDataItem.ParentId);
|
||||
}
|
||||
msgDataItem.PrefabObjectID = Guid::Empty;
|
||||
auto* objScene = ScriptingObject::Cast<SceneObject>(obj);
|
||||
if (objScene && objScene->HasPrefabLink())
|
||||
msgDataItem.PrefabObjectID = objScene->GetPrefabObjectID();
|
||||
GetNetworkName(msgDataItem.ObjectTypeName, obj->GetType().Fullname);
|
||||
msg.WriteStructure(msgDataItem);
|
||||
}
|
||||
if (isClient)
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg);
|
||||
peer->EndSendMessage(NetworkChannelType::Reliable, msg);
|
||||
else
|
||||
peer->EndSendMessage(NetworkChannelType::ReliableOrdered, msg, CachedTargets);
|
||||
peer->EndSendMessage(NetworkChannelType::Reliable, msg, CachedTargets);
|
||||
}
|
||||
|
||||
void SendObjectRoleMessage(const NetworkReplicatedObject& item, const NetworkClient* excludedClient = nullptr)
|
||||
@@ -394,6 +424,44 @@ SceneObject* FindPrefabObject(Actor* a, const Guid& prefabObjectId)
|
||||
return result;
|
||||
}
|
||||
|
||||
void SetupObjectSpawnGroupItem(ScriptingObject* obj, Array<SpawnGroup, InlinedAllocation<8>>& spawnGroups, SpawnItem& spawnItem)
|
||||
{
|
||||
// Check if can fit this object into any of the existing groups (eg. script which can be spawned with parent actor)
|
||||
SpawnGroup* group = nullptr;
|
||||
for (auto& g : spawnGroups)
|
||||
{
|
||||
ScriptingObject* groupRoot = g.Items[0]->Object.Get();
|
||||
if (IsParentOf(obj, groupRoot))
|
||||
{
|
||||
// Reuse existing group (append)
|
||||
g.Items.Add(&spawnItem);
|
||||
group = &g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (group)
|
||||
return;
|
||||
|
||||
// Check if can override any of the existing groups (eg. actor which should be spawned before scripts)
|
||||
for (auto& g : spawnGroups)
|
||||
{
|
||||
ScriptingObject* groupRoot = g.Items[0]->Object.Get();
|
||||
if (IsParentOf(groupRoot, obj))
|
||||
{
|
||||
// Reuse existing group (as a root)
|
||||
g.Items.Insert(0, &spawnItem);
|
||||
group = &g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (group)
|
||||
return;
|
||||
|
||||
// Create new group
|
||||
group = &spawnGroups.AddOne();
|
||||
group->Items.Add(&spawnItem);
|
||||
}
|
||||
|
||||
#if !COMPILE_WITHOUT_CSHARP
|
||||
|
||||
#include "Engine/Scripting/ManagedCLR/MUtils.h"
|
||||
@@ -814,14 +882,29 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
// 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
|
||||
ChunkedArray<SpawnItem, 256> spawnItems;
|
||||
Array<SpawnGroup, InlinedAllocation<8>> spawnGroups;
|
||||
for (auto it = Objects.Begin(); it.IsNotEnd(); ++it)
|
||||
{
|
||||
auto& item = it->Item;
|
||||
ScriptingObject* obj = item.Object.Get();
|
||||
if (!obj || !item.Spawned)
|
||||
continue;
|
||||
BuildCachedTargets(NewClients, item.TargetClientIds);
|
||||
SendObjectSpawnMessage(item, obj);
|
||||
|
||||
// Setup spawn item for this object
|
||||
auto& spawnItem = spawnItems.AddOne();
|
||||
spawnItem.Object = obj;
|
||||
spawnItem.Targets.Link(item.TargetClientIds);
|
||||
spawnItem.OwnerClientId = item.OwnerClientId;
|
||||
spawnItem.Role = item.Role;
|
||||
|
||||
SetupObjectSpawnGroupItem(obj, spawnGroups, spawnItem);
|
||||
}
|
||||
|
||||
// Groups of objects to spawn
|
||||
for (SpawnGroup& g : spawnGroups)
|
||||
{
|
||||
SendObjectSpawnMessage(g, NewClients);
|
||||
}
|
||||
NewClients.Clear();
|
||||
}
|
||||
@@ -865,9 +948,11 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
if (SpawnQueue.Count() != 0)
|
||||
{
|
||||
PROFILE_CPU_NAMED("SpawnQueue");
|
||||
|
||||
// Propagate hierarchical ownership from spawned parent to spawned child objects (eg. spawned script and spawned actor with set hierarchical ownership on actor which should affect script too)
|
||||
// TODO: maybe we can propagate ownership within spawn groups only?
|
||||
for (SpawnItem& e : SpawnQueue)
|
||||
{
|
||||
// Propagate hierarchical ownership from spawned parent to spawned child objects (eg. spawned script and spawned actor with set hierarchical ownership on actor which should affect script too)
|
||||
if (e.HasOwnership && e.HierarchicalOwnership)
|
||||
{
|
||||
for (auto& q : SpawnQueue)
|
||||
@@ -881,6 +966,10 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Batch spawned objects into groups (eg. player actor with scripts and child actors merged as a single spawn message)
|
||||
// That's because NetworkReplicator::SpawnObject can be called in separate for different actors/scripts of a single prefab instance but we want to spawn it at once over the network
|
||||
Array<SpawnGroup, InlinedAllocation<8>> spawnGroups;
|
||||
for (SpawnItem& e : SpawnQueue)
|
||||
{
|
||||
ScriptingObject* obj = e.Object.Get();
|
||||
@@ -913,11 +1002,16 @@ void NetworkInternal::NetworkReplicatorUpdate()
|
||||
MISSING_CODE("Sending TargetClientIds over to server for partial object replication.");
|
||||
item.TargetClientIds = MoveTemp(e.Targets);
|
||||
}
|
||||
|
||||
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Spawn object ID={}", item.ToString());
|
||||
BuildCachedTargets(NetworkManager::Clients, item.TargetClientIds);
|
||||
SendObjectSpawnMessage(item, obj);
|
||||
item.Spawned = true;
|
||||
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Spawn object ID={}", item.ToString());
|
||||
|
||||
SetupObjectSpawnGroupItem(obj, spawnGroups, e);
|
||||
}
|
||||
|
||||
// Spawn groups of objects
|
||||
for (SpawnGroup& g : spawnGroups)
|
||||
{
|
||||
SendObjectSpawnMessage(g, NetworkManager::Clients);
|
||||
}
|
||||
SpawnQueue.Clear();
|
||||
}
|
||||
@@ -1085,102 +1179,135 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
|
||||
{
|
||||
NetworkMessageObjectSpawn msgData;
|
||||
event.Message.ReadStructure(msgData);
|
||||
auto* msgDataItems = (NetworkMessageObjectSpawnItem*)event.Message.SkipBytes(msgData.ItemsCount * sizeof(NetworkMessageObjectSpawnItem));
|
||||
if (msgData.ItemsCount == 0)
|
||||
return;
|
||||
ScopeLock lock(ObjectsLock);
|
||||
NetworkReplicatedObject* e = ResolveObject(msgData.ObjectId, msgData.ParentId, msgData.ObjectTypeName);
|
||||
if (e)
|
||||
|
||||
// Check if that object has been already spawned
|
||||
auto& rootItem = msgDataItems[0];
|
||||
NetworkReplicatedObject* root = ResolveObject(rootItem.ObjectId, rootItem.ParentId, rootItem.ObjectTypeName);
|
||||
if (root)
|
||||
{
|
||||
auto& item = *e;
|
||||
item.Spawned = true;
|
||||
if (NetworkManager::IsClient())
|
||||
// Object already exists locally so just synchronize the ownership (and mark as spawned)
|
||||
for (int32 i = 0; i < msgData.ItemsCount; i++)
|
||||
{
|
||||
// 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
|
||||
ScriptingObject* obj = nullptr;
|
||||
const NetworkReplicatedObject* parent = ResolveObject(msgData.ParentId);
|
||||
if (msgData.PrefabId.IsValid())
|
||||
{
|
||||
Actor* prefabInstance = nullptr;
|
||||
Actor* parentActor = parent && parent->Object && parent->Object->Is<Actor>() ? parent->Object.As<Actor>() : nullptr;
|
||||
if (parentActor && parentActor->GetPrefabID() == msgData.PrefabId)
|
||||
auto& msgDataItem = msgDataItems[i];
|
||||
NetworkReplicatedObject* e = ResolveObject(msgDataItem.ObjectId, msgDataItem.ParentId, msgDataItem.ObjectTypeName);
|
||||
auto& item = *e;
|
||||
item.Spawned = true;
|
||||
if (NetworkManager::IsClient())
|
||||
{
|
||||
// Reuse parent object as prefab instance
|
||||
prefabInstance = parentActor;
|
||||
// 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 (parentActor = Scripting::TryFindObject<Actor>(msgData.ParentId))
|
||||
else if (item.OwnerClientId != msgData.OwnerClientId)
|
||||
{
|
||||
// Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
|
||||
for (Actor* child : parentActor->Children)
|
||||
// Other client spawned object with a different owner
|
||||
// TODO: send reply message to inform about proper object ownership that client
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Recreate object locally (spawn only root)
|
||||
ScriptingObject* obj = nullptr;
|
||||
Actor* prefabInstance = nullptr;
|
||||
if (msgData.PrefabId.IsValid())
|
||||
{
|
||||
const NetworkReplicatedObject* parent = ResolveObject(rootItem.ParentId);
|
||||
Actor* parentActor = parent && parent->Object && parent->Object->Is<Actor>() ? parent->Object.As<Actor>() : nullptr;
|
||||
if (parentActor && parentActor->GetPrefabID() == msgData.PrefabId)
|
||||
{
|
||||
// Reuse parent object as prefab instance
|
||||
prefabInstance = parentActor;
|
||||
}
|
||||
else if (parentActor = Scripting::TryFindObject<Actor>(rootItem.ParentId))
|
||||
{
|
||||
// Try to find that spawned prefab (eg. prefab with networked script was spawned before so now we need to link it)
|
||||
for (Actor* child : parentActor->Children)
|
||||
{
|
||||
if (child->GetPrefabID() == msgData.PrefabId)
|
||||
{
|
||||
if (child->GetPrefabID() == msgData.PrefabId)
|
||||
if (Objects.Contains(child->GetID()))
|
||||
{
|
||||
if (Objects.Contains(child->GetID()))
|
||||
obj = FindPrefabObject(child, rootItem.PrefabObjectID);
|
||||
if (Objects.Contains(obj->GetID()))
|
||||
{
|
||||
obj = FindPrefabObject(child, msgData.PrefabObjectID);
|
||||
if (Objects.Contains(obj->GetID()))
|
||||
{
|
||||
// Other instance with already spawned network object
|
||||
obj = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reuse already spawned object within a parent
|
||||
prefabInstance = child;
|
||||
break;
|
||||
}
|
||||
// Other instance with already spawned network object
|
||||
obj = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reuse already spawned object within a parent
|
||||
prefabInstance = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!prefabInstance)
|
||||
{
|
||||
// Spawn prefab
|
||||
auto prefab = (Prefab*)LoadAsset(msgData.PrefabId, Prefab::TypeInitializer);
|
||||
if (!prefab)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
|
||||
return;
|
||||
}
|
||||
prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
|
||||
if (!prefabInstance)
|
||||
{
|
||||
// Spawn prefab
|
||||
auto prefab = (Prefab*)LoadAsset(msgData.PrefabId, Prefab::TypeInitializer);
|
||||
if (!prefab)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString());
|
||||
return;
|
||||
}
|
||||
prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr);
|
||||
if (!prefabInstance)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!obj)
|
||||
obj = FindPrefabObject(prefabInstance, msgData.PrefabObjectID);
|
||||
if (!obj)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgData.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
|
||||
Delete(prefabInstance);
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
if (!obj)
|
||||
obj = FindPrefabObject(prefabInstance, rootItem.PrefabObjectID);
|
||||
if (!obj)
|
||||
{
|
||||
// Spawn object
|
||||
const ScriptingTypeHandle objectType = Scripting::FindScriptingType(msgData.ObjectTypeName);
|
||||
obj = ScriptingObject::NewObject(objectType);
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", rootItem.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
|
||||
Delete(prefabInstance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Spawn object
|
||||
if (msgData.ItemsCount != 1)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Only prefab object spawning can contain more than one object (for type {})", String(rootItem.ObjectTypeName));
|
||||
return;
|
||||
}
|
||||
const ScriptingTypeHandle objectType = Scripting::FindScriptingType(rootItem.ObjectTypeName);
|
||||
obj = ScriptingObject::NewObject(objectType);
|
||||
if (!obj)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(rootItem.ObjectTypeName));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Setup all newly spawned objects
|
||||
for (int32 i = 0; i < msgData.ItemsCount; i++)
|
||||
{
|
||||
auto& msgDataItem = msgDataItems[i];
|
||||
if (i != 0)
|
||||
{
|
||||
obj = FindPrefabObject(prefabInstance, msgDataItem.PrefabObjectID);
|
||||
if (!obj)
|
||||
{
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", String(msgData.ObjectTypeName));
|
||||
NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find object {} in prefab {}", msgDataItem.PrefabObjectID.ToString(), msgData.PrefabId.ToString());
|
||||
Delete(prefabInstance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!obj->IsRegistered())
|
||||
obj->RegisterObject();
|
||||
const NetworkReplicatedObject* parent = ResolveObject(msgDataItem.ParentId);
|
||||
|
||||
// Add object to the list
|
||||
NetworkReplicatedObject item;
|
||||
@@ -1200,24 +1327,27 @@ void NetworkInternal::OnNetworkMessageObjectSpawn(NetworkEvent& event, NetworkCl
|
||||
Objects.Add(MoveTemp(item));
|
||||
|
||||
// Boost future lookups by using indirection
|
||||
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgData.ObjectId, item.ToString(), obj->GetType().ToString());
|
||||
IdsRemappingTable.Add(msgData.ObjectId, item.ObjectId);
|
||||
NETWORK_REPLICATOR_LOG(Info, "[NetworkReplicator] Remap object ID={} into object {}:{}", msgDataItem.ObjectId, item.ToString(), obj->GetType().ToString());
|
||||
IdsRemappingTable.Add(msgDataItem.ObjectId, item.ObjectId);
|
||||
|
||||
// Automatic parenting for scene objects
|
||||
auto sceneObject = ScriptingObject::Cast<SceneObject>(obj);
|
||||
if (sceneObject)
|
||||
{
|
||||
if (parent && parent->Object.Get() && parent->Object->Is<Actor>())
|
||||
if (parent && parent
|
||||
->
|
||||
Object.Get() && parent->Object->Is<Actor>()
|
||||
)
|
||||
sceneObject->SetParent(parent->Object.As<Actor>());
|
||||
else if (auto* parentActor = Scripting::TryFindObject<Actor>(msgData.ParentId))
|
||||
else if (auto* parentActor = Scripting::TryFindObject<Actor>(msgDataItem.ParentId))
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
void NetworkInternal::OnNetworkMessageObjectDespawn(NetworkEvent& event, NetworkClient* client, NetworkPeer* peer)
|
||||
|
||||
Reference in New Issue
Block a user