Add **Network Replication Hierarchy** for robust control over replication in multiplayer games
This commit is contained in:
199
Source/Engine/Networking/NetworkReplicationHierarchy.cpp
Normal file
199
Source/Engine/Networking/NetworkReplicationHierarchy.cpp
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
|
||||
|
||||
#include "NetworkReplicationHierarchy.h"
|
||||
#include "NetworkManager.h"
|
||||
#include "Engine/Level/Actor.h"
|
||||
#include "Engine/Level/SceneObject.h"
|
||||
|
||||
uint16 NetworkReplicationNodeObjectCounter = 0;
|
||||
NetworkClientsMask NetworkClientsMask::All = { MAX_uint64, MAX_uint64 };
|
||||
|
||||
Actor* NetworkReplicationHierarchyObject::GetActor() const
|
||||
{
|
||||
auto* actor = ScriptingObject::Cast<Actor>(Object);
|
||||
if (!actor)
|
||||
{
|
||||
if (const auto* sceneObject = ScriptingObject::Cast<SceneObject>(Object))
|
||||
actor = sceneObject->GetParent();
|
||||
}
|
||||
return actor;
|
||||
}
|
||||
|
||||
void NetworkReplicationHierarchyUpdateResult::Init()
|
||||
{
|
||||
_clientsHaveLocation = false;
|
||||
_clients.Resize(NetworkManager::Clients.Count());
|
||||
_clientsMask = NetworkClientsMask();
|
||||
for (int32 i = 0; i < _clients.Count(); i++)
|
||||
_clientsMask.SetBit(i);
|
||||
_entries.Clear();
|
||||
ReplicationScale = 1.0f;
|
||||
}
|
||||
|
||||
void NetworkReplicationHierarchyUpdateResult::SetClientLocation(int32 clientIndex, const Vector3& location)
|
||||
{
|
||||
CHECK(clientIndex >= 0 && clientIndex < _clients.Count());
|
||||
_clientsHaveLocation = true;
|
||||
Client& client = _clients[clientIndex];
|
||||
client.HasLocation = true;
|
||||
client.Location = location;
|
||||
}
|
||||
|
||||
bool NetworkReplicationHierarchyUpdateResult::GetClientLocation(int32 clientIndex, Vector3& location) const
|
||||
{
|
||||
CHECK_RETURN(clientIndex >= 0 && clientIndex < _clients.Count(), false);
|
||||
const Client& client = _clients[clientIndex];
|
||||
location = client.Location;
|
||||
return client.HasLocation;
|
||||
}
|
||||
|
||||
void NetworkReplicationNode::AddObject(NetworkReplicationHierarchyObject obj)
|
||||
{
|
||||
ASSERT(obj.Object && obj.ReplicationFPS > 0.0f);
|
||||
|
||||
// Randomize initial replication update to spread rep rates more evenly for large scenes that register all objects within the same frame
|
||||
obj.ReplicationUpdatesLeft = NetworkReplicationNodeObjectCounter++ % Math::Clamp(Math::RoundToInt(NetworkManager::NetworkFPS / obj.ReplicationFPS), 1, 60);
|
||||
|
||||
Objects.Add(obj);
|
||||
}
|
||||
|
||||
bool NetworkReplicationNode::RemoveObject(ScriptingObject* obj)
|
||||
{
|
||||
return !Objects.Remove(obj);
|
||||
}
|
||||
|
||||
bool NetworkReplicationNode::DirtyObject(ScriptingObject* obj)
|
||||
{
|
||||
const int32 index = Objects.Find(obj);
|
||||
if (index != -1)
|
||||
{
|
||||
NetworkReplicationHierarchyObject& e = Objects[index];
|
||||
e.ReplicationUpdatesLeft = 0;
|
||||
}
|
||||
return index != -1;
|
||||
}
|
||||
|
||||
void NetworkReplicationNode::Update(NetworkReplicationHierarchyUpdateResult* result)
|
||||
{
|
||||
CHECK(result);
|
||||
const float networkFPS = NetworkManager::NetworkFPS / result->ReplicationScale;
|
||||
for (NetworkReplicationHierarchyObject& obj : Objects)
|
||||
{
|
||||
if (obj.ReplicationUpdatesLeft > 0)
|
||||
{
|
||||
// Move to the next frame
|
||||
obj.ReplicationUpdatesLeft--;
|
||||
}
|
||||
else
|
||||
{
|
||||
NetworkClientsMask targetClients = result->GetClientsMask();
|
||||
if (result->_clientsHaveLocation)
|
||||
{
|
||||
// Cull object against viewers locations
|
||||
if (const Actor* actor = obj.GetActor())
|
||||
{
|
||||
const Vector3 objPosition = actor->GetPosition();
|
||||
const Real cullDistanceSq = Math::Square(obj.CullDistance);
|
||||
for (int32 clientIndex = 0; clientIndex < result->_clients.Count(); clientIndex++)
|
||||
{
|
||||
const auto& client = result->_clients[clientIndex];
|
||||
if (client.HasLocation)
|
||||
{
|
||||
const Real distanceSq = Vector3::DistanceSquared(objPosition, client.Location);
|
||||
// TODO: scale down replication FPS when object is far away from all clients (eg. by 10-50%)
|
||||
if (distanceSq >= cullDistanceSq)
|
||||
{
|
||||
// Object is too far from this viewer so don't send data to him
|
||||
targetClients.UnsetBit(clientIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetClients)
|
||||
{
|
||||
// Replicate this frame
|
||||
result->AddObject(obj.Object, targetClients);
|
||||
}
|
||||
|
||||
// Calculate frames until next replication
|
||||
obj.ReplicationUpdatesLeft = (uint16)Math::Clamp<int32>(Math::RoundToInt(networkFPS / obj.ReplicationFPS) - 1, 0, MAX_uint16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NetworkReplicationGridNode::~NetworkReplicationGridNode()
|
||||
{
|
||||
for (const auto& e : _children)
|
||||
Delete(e.Value.Node);
|
||||
}
|
||||
|
||||
void NetworkReplicationGridNode::AddObject(NetworkReplicationHierarchyObject obj)
|
||||
{
|
||||
// Chunk actors locations into a grid coordinates
|
||||
Int3 coord = Int3::Zero;
|
||||
if (const Actor* actor = obj.GetActor())
|
||||
{
|
||||
coord = actor->GetPosition() / CellSize;
|
||||
}
|
||||
|
||||
Cell* cell = _children.TryGet(coord);
|
||||
if (!cell)
|
||||
{
|
||||
// Allocate new cell
|
||||
cell = &_children[coord];
|
||||
cell->Node = New<NetworkReplicationNode>();
|
||||
cell->MinCullDistance = obj.CullDistance;
|
||||
}
|
||||
cell->Node->AddObject(obj);
|
||||
|
||||
// Cache minimum culling distance for a whole cell to skip it at once
|
||||
cell->MinCullDistance = Math::Min(cell->MinCullDistance, obj.CullDistance);
|
||||
}
|
||||
|
||||
bool NetworkReplicationGridNode::RemoveObject(ScriptingObject* obj)
|
||||
{
|
||||
for (const auto& e : _children)
|
||||
{
|
||||
if (e.Value.Node->RemoveObject(obj))
|
||||
{
|
||||
// TODO: remove empty cells?
|
||||
// TODO: update MinCullDistance for cell?
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void NetworkReplicationGridNode::Update(NetworkReplicationHierarchyUpdateResult* result)
|
||||
{
|
||||
CHECK(result);
|
||||
if (result->_clientsHaveLocation)
|
||||
{
|
||||
// Update only cells within a range
|
||||
const Real cellRadiusSq = Math::Square(CellSize * 1.414f);
|
||||
for (const auto& e : _children)
|
||||
{
|
||||
const Vector3 cellPosition = (e.Key * CellSize) + (CellSize * 0.5f);
|
||||
Real distanceSq = MAX_Real;
|
||||
for (auto& client : result->_clients)
|
||||
{
|
||||
if (client.HasLocation)
|
||||
distanceSq = Math::Min(distanceSq, Vector3::DistanceSquared(cellPosition, client.Location));
|
||||
}
|
||||
const Real minCullDistanceSq = Math::Square(e.Value.MinCullDistance);
|
||||
if (distanceSq < minCullDistanceSq + cellRadiusSq)
|
||||
{
|
||||
e.Value.Node->Update(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Brute-force over all cells
|
||||
for (const auto& e : _children)
|
||||
{
|
||||
e.Value.Node->Update(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user