Tutorial 5 - Network Culling Using Sync Groups
Summary
Use sync groups to partition a scene into groups which can be used to control what entities are synced to a player. Sync Groups are collections of entities that are synced to clients. The tutorial will partition a scene into regions which will be assigned to different sync groups. Players will be added to and removed from sync groups letting them only see entities in the regions near to them. This tutorial uses Tutorial 2 or 3 as a base.
Requirements
- Reactor Requirements
- Completion of Tutorials 1 and 2.
Setup
Make the Platform Larger and Tilted
- Select the 'Platform'.
- Set the transform scale to (50, 1, 50).
- Set the transform rotation to (-5, 0, 0).
Create a Sphere Entity Prefab
- Create a sphere.
- Add a ksEntityComponent.
- Add a Rigidbody component.
- Make the sphere a prefab in the 'Resources' folder.
- Delete the sphere from the hierarchy.
Scripting
Create a Server Spawner Room Script to Spawn Spheres
The server spawner room script spawns spheres continuously on the high end of the tilted platform which roll down it.
- Select the 'Room' object.
- In the 'Add Component' menu, select 'Reactor->New Server Room Script'.
- Name the script 'ServerSpawnerRoom'.
ServerSpawnerRoom.cs
using KS.Reactor.Server;
using KS.Reactor;
public class ServerSpawnerRoom : ksServerRoomScript
{
// The number of spheres to spawn per second.
[ksEditable] public float SpawnRate = 10;
// The size of the platform.
[ksEditable] public float MapSize = 50;
private ksRandom m_random = new ksRandom();
private float m_spawnTimer;
// Called when the script is attached.
public override void Initialize()
{
Room.OnUpdate[0] += Update;
if (SpawnRate > 0)
{
m_spawnTimer = 1f / SpawnRate;
}
}
// Called when the script is detached.
public override void Detached()
{
Room.OnUpdate[0] -= Update;
}
// Called during the update cycle.
private void Update()
{
if (SpawnRate <= 0)
{
return;
}
m_spawnTimer -= Time.Delta;
while (m_spawnTimer <= 0f)
{
m_spawnTimer += 1f / SpawnRate;
// Spawn a sphere at a random location along the top edge of the platform at a height of 10.
float dist = MapSize / 2f - 1f;
ksVector3 position = new ksVector3(m_random.NextFloat(-dist, dist), 10f, dist);
Room.SpawnEntity("Sphere", position);
}
}
}
Create a Server Sync Group Room Script to Partition the World into Sync Groups
The server sync group room script divides the world into cells along the XZ plane. Each cell corresponds to a sync group.
Sync groups are used to control which entities are synced to which players. All entities belong to exactly one sync group, and a player can belong to any number of sync groups and will receive all entities in those sync groups. Zero is a special sync group; entities in group zero will always sync to all players. All entities are in sync group zero by default. Sync group ids are 32-bit uints, however the highest 2 bits are reserved, so the highest possible sync group id is 2^30 - 1. The script assigns entities to sync groups based on which cell they are in. It assigns players to sync groups for cells near the player's avatar.
- Select the 'Room' object.
- In the 'Add Component' menu, select 'Reactor->New Server Room Script'.
- Name the script 'ServerSyncGroupRoom'.
ServerSyncGroupRoom.cs
using System;
using KS.Reactor.Server;
using KS.Reactor;
public class ServerSyncGroupRoom : ksServerRoomScript
{
// The size of sync group cells.
[ksEditable] public int CellSize = 10;
// Sync all cells intersecting the rectangle with these half extents around the player.
[ksEditable] public ksVector2 SyncHalfExtents = new ksVector2(14f, 10f);
// Called when the script is attached.
public override void Initialize()
{
Room.OnUpdate[0] += Update;
}
// Called when the script is detached.
public override void Detached()
{
Room.OnUpdate[0] -= Update;
}
// Called during the update cycle.
public void Update()
{
// Assign all entities to a sync group based on their XZ position.
foreach (ksIServerEntity entity in Room.DynamicEntities)
{
entity.SyncGroup = GetSyncGroup(entity.Transform.Position.XZ);
}
// Determine which sync groups each player needs to receive.
foreach (ksIServerPlayer player in Room.Players)
{
if (player.IsVirtual)
{
// Virtual players (bots) don't need sync groups.
continue;
}
if (player.ControlledEntities.Count == 0)
{
// The player is not controlling an entity with a player controller. Do not set sync groups.
continue;
}
// Get the sync groups the player current is receiving.
uint[] groups = player.GetSyncGroups();
// Calculate the range of sync groups cells around the player they need to receive.
ksVector2 pos = player.ControlledEntities[0].Transform.Position.XZ;
int x1 = (int)Math.Floor((pos.X - SyncHalfExtents.X) / CellSize);
int x2 = (int)Math.Floor((pos.X + SyncHalfExtents.X) / CellSize);
int y1 = (int)Math.Floor((pos.Y - SyncHalfExtents.Y) / CellSize);
int y2 = (int)Math.Floor((pos.Y + SyncHalfExtents.Y) / CellSize);
for (int x = x1; x <= x2; x++)
{
for (int y = y1; y <= y2; y++)
{
// Convert cell x, y to a sync group.
uint group = GetSyncGroup(x, y);
// Check if the player is already in this sync group.
int index = Array.IndexOf(groups, group);
if (index < 0)
{
// The player was not in this sync group, so add it to the group.
player.AddToSyncGroup(group);
}
else
{
// The player was already in the sync group. Set the group at that index to zero so we know not
// to remove the player from the group later.
groups[index] = 0;
}
}
}
// Remove the player from any sync groups it no longer needs (weren't set to zero earlier).
foreach (uint group in groups)
{
if (group != 0)
{
player.RemoveFromSyncGroup(group);
}
}
}
}
public uint GetSyncGroup(ksVector2 position)
{
// Convert position to cell coordinates and get sync group from cell coordinates.
int x = (int)Math.Floor(position.X / CellSize);
int y = (int)Math.Floor(position.Y / CellSize);
return GetSyncGroup(x, y);
}
// Convert cell coordinates to a sync group.
private uint GetSyncGroup(int x, int y)
{
bool negX = x < 0;
bool negY = y < 0;
// Add one to ensure we don't get zero as a sync group. Zero is a special sync group that all players always receive.
x = Math.Abs(x) + 1;
y = Math.Abs(y) + 1;
// multiply y by 256. This means we can have a maximum of 512 (256 positive, 256 negative) columns of sync group cells along the X-axis.
// If you need more, increase this number.
uint group = (uint)(x + y * 256);
// sign flags.
if (negX)
{
group |= 1u << 29;
}
if (negY)
{
group |= 1u << 28;
}
return group;
}
}
Testing
- Build your scene config (CTRL + F2).
- Start a local server.
- Enter play mode.
- Select the 'Main Camera' object.
- Change the 'FollowCamera' distance to 30 to see spheres appear and disappear as they move into or out of sync group cells near the player.
- Change the 'FollowCamera' distance to 10 and you should not see spheres appearing and disappearing.
Spheres will spawn on the high end of the platform and roll all the way down. If you set the 'FollowCamera' distance to 30, you can see the spheres popping into and out of existance as they move into and out of sync group cells near your avatar. As you move around, the sync groups cells you are receiving should change so you only receive nearby cells.