Skip to main content

Area of Interest

Overview

The AOISpatialHash system implements three advanced features that extend beyond basic spatial partitioning:

  1. Custom Groups - non-spatial player grouping (teams, channels, private groups)
  2. AOI LOD (Level of Detail) - multi-tiered update distribution with reduced frequency for distant cells
  3. Presenter Mode - global broadcasting for special entities (speakers, hosts, streamers)

1. Custom Groups

Concept

Custom Groups allow players to form logical groups independent of their spatial location in the world. This enables team-based gameplay, private channels, and role-based communication that transcends physical proximity.

Implementation details

Data structure

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 12-14):

private AOIGroup[,] AOICells;
private readonly List<AOIGroup> ActiveCells = new List<AOIGroup>();
private readonly List<AOIGroup> ActiveGroups = new List<AOIGroup>(); // groups created by players with unique tag
  • ActiveGroups: stores all custom groups (separate from spatial cells)
  • Each custom group has a unique GroupId (byte identifier)
  • Groups are created lazily when the first player joins

Creating/joining a custom group

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 381-404):

public void CreateOrJoinCustomGroup(byte groupId, FNEPlayer entity)
{
var aoigroup = ActiveGroups.Find(x => x.GroupId == groupId);
if (aoigroup == null)
{
aoigroup = new AOIGroup(groupId);
ActiveGroups.Add(aoigroup);
}

if (!aoigroup.Contains(entity))
{
aoigroup.AddParticipant(entity);

AOIGlobal.RemoveParticipant(entity);
for (int i = ActiveCells.Count - 1; i >= 0; i--)
{
if (ActiveCells[i].Contains(entity))
{
ActiveCells[i].RemoveParticipant(entity);
}
}
}

}

Process flow:

  1. Find or create: searches for an existing group by groupId, creates one if not found
  2. Add player: adds the player to the custom group
  3. Remove from spatial systems:
    • Removes the player from AOIGlobal (if in global mode)
    • Removes the player from all spatial cells (if in spatial mode)
  4. Result: the player now receives updates only from their custom group, not from spatial cells

Leaving a custom group

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 406-418):

public void RemoveCustomGroup(byte groupId, FNEPlayer entity)
{
var aoigroup = ActiveGroups.Find(x => x.GroupId == groupId);
if (aoigroup != null)
{
aoigroup.RemoveParticipant(entity);
ForcedUpdatePlayerCellStatus(entity);
if (!aoigroup.IsActive)
{
ActiveGroups.Remove(aoigroup);
}
}
}

Process flow:

  1. Find group: locates the custom group
  2. Remove player: removes the player from the group
  3. Rejoin spatial system: calls ForcedUpdatePlayerCellStatus() to re-add the player to spatial cells based on their current position
  4. Cleanup: if the group becomes empty (!IsActive), removes it from ActiveGroups

Update distribution for custom groups

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 180-208):

public void OnEntityDataArrived(NetworkEntity entity, object operationData)
{
bool isPresenter = false;
if (PresenterInGroupCast)
{
isPresenter = entity.IsPresenter;
}
// if user is present in Custom Group, get the reference to that group
// and enqueue data in that group
if (entity.GroupId != 0 || isPresenter)
{
var group = FindGroupById(entity.GroupId);

if (group != null)
{
EnqueueDataInRelevantLod(group, operationData);
}

if (isPresenter)
{
EnqueueDataInRelevantGroups(entity, operationData);
}
}
else
{
EnqueueDataInRelevantGroups(entity, operationData);
}

}

Key behavior:

  • If entity.GroupId != 0: the entity is in a custom group
  • Updates are sent only to that custom group (not to spatial cells)
  • Custom groups bypass spatial partitioning entirely

Visual representation

Spatial Grid Custom Groups
+-----+-----+-----+ +--------------+
| P1 | P2 | | | Team Alpha |
+-----+-----+-----+ | P1, P3, P5 |
| P3 | P4 | | +--------------+
+-----+-----+-----+ +--------------+
| P5 | | P6 | | Team Beta |
+-----+-----+-----+ | P2, P4 |
+--------------+

When P1 (in Team Alpha) sends update:
- Sent to Team Alpha (P1, P3, P5)
- NOT sent to spatial cells
- NOT sent to Team Beta

Use cases

  • Team-based games: players on the same team communicate regardless of location
  • Private channels: VIP sections, admin channels, private conversations
  • Role-based groups: moderators, event hosts, special access groups
  • Event groups: temporary groups for specific activities

Benefits

  • Logical grouping: players can communicate based on relationships, not just proximity
  • Privacy: custom groups are isolated from spatial updates
  • Flexibility: players can be in multiple groups (via different GroupId values)
  • Performance: smaller group sizes mean less network traffic per update

2. AOI LOD (Level of Detail)

Concept

LOD (Level of Detail) reduces update frequency for distant cells, providing a performance optimization that maintains awareness of distant entities while reducing network bandwidth.

Implementation details

LOD configuration

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 20-22):

private uint counterLod = 0;
private uint ActivationCount = 10;
private uint LodLevel = 2; // starting from zero
  • LodLevel: maximum LOD depth (0 = disabled, 1 = depth 2 only, 2 = depth 2 and 3)
  • counterLod: global counter that cycles to control update frequency
  • LOD is initialized via the Init() method

Update distribution with LOD

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 247-264):

private void EnqueueDataInRelevantLod(AOIGroup cell, object operationData)
{
if (cell.IsActive)
{
cell.EnqueueData(operationData);
}
EnqueueEntityDataInNeighbouringCells(cell.Index_X, cell.Index_Z, operationData);

if (LodLevel > 0 && counterLod % 2 == 0)
{
EnqueueEntityDataInNeighbouringCellsByDepth(cell.Index_X, cell.Index_Z, 2, operationData);
}
if (LodLevel > 1 && counterLod % 4 == 0)
{
EnqueueEntityDataInNeighbouringCellsByDepth(cell.Index_X, cell.Index_Z, 3, operationData);
}
counterLod++;
}

LOD distribution strategy:

  1. Immediate neighbors (depth 1): always updated every tick

    • 8 neighboring cells (Moore neighborhood)
    • Full update frequency
  2. LOD level 2 (depth 2): updated every 2 ticks (counterLod % 2 == 0)

    • Cells at distance 2 from the source
    • 50% update frequency
  3. LOD level 3 (depth 3): updated every 4 ticks (counterLod % 4 == 0)

    • Cells at distance 3 from the source
    • 25% update frequency

Immediate neighbors (depth 1)

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 266-276):

private void EnqueueEntityDataInNeighbouringCells(int x, int z, object operationData)
{
EnqueueDataToRelevantCell(x - 1, z, operationData); // Up
EnqueueDataToRelevantCell(x + 1, z, operationData); // Down
EnqueueDataToRelevantCell(x, z - 1, operationData); // Left
EnqueueDataToRelevantCell(x, z + 1, operationData); // Right
EnqueueDataToRelevantCell(x - 1, z - 1, operationData); // Diagonal 1
EnqueueDataToRelevantCell(x + 1, z - 1, operationData); // Diagonal 2
EnqueueDataToRelevantCell(x + 1, z + 1, operationData); // Diagonal 3
EnqueueDataToRelevantCell(x - 1, z + 1, operationData); // Diagonal 4
}

An 8-cell Moore neighborhood always receives updates.

LOD depth distribution

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 278-304):

private void EnqueueEntityDataInNeighbouringCellsByDepth(int x, int z, int depth, object operationData)
{
// Define corner coordinates
int bottomLeftX = Math.Clamp(x - depth, 0, gridWidth - 1);
int bottomLeftZ = Math.Clamp(z - depth, 0, gridWidth - 1);
int bottomRightX = Math.Clamp(x + depth, 0, gridWidth - 1);
int topLeftZ = Math.Clamp(z + depth, 0, gridWidth - 1);

EnqueueDataToRelevantCell(bottomLeftX, bottomLeftZ, operationData); // Bottom-left
EnqueueDataToRelevantCell(bottomRightX, bottomLeftZ, operationData); // Bottom-right
EnqueueDataToRelevantCell(bottomRightX, topLeftZ, operationData); // Top-right
EnqueueDataToRelevantCell(bottomLeftX, topLeftZ, operationData); // Top-left

// Process cells along bottom and top rows (excluding corners)
for (int i = bottomLeftX + 1; i < bottomRightX; i++)
{
EnqueueDataToRelevantCell(i, bottomLeftZ, operationData); // Bottom row
EnqueueDataToRelevantCell(i, topLeftZ, operationData); // Top row
}

// Process cells along left and right columns (excluding corners)
for (int j = bottomLeftZ + 1; j < topLeftZ; j++)
{
EnqueueDataToRelevantCell(bottomLeftX, j, operationData); // Left column
EnqueueDataToRelevantCell(bottomRightX, j, operationData); // Right column
}
}

Distribution pattern:

  • Creates a perimeter at the specified depth
  • Sends updates to all cells on the perimeter (corners + edges)
  • Excludes cells closer than the depth (already covered by the immediate neighbors)

Visual representation

LOD Distribution Pattern (Depth 2):

+-----+-----+-----+-----+-----+
| LOD | | | | LOD | <- Depth 2 corners
+-----+-----+-----+-----+-----+
| | N | N | N | | <- Depth 1 neighbors
+-----+-----+-----+-----+-----+
| | N | E | N | | <- Entity (E)
+-----+-----+-----+-----+-----+
| | N | N | N | |
+-----+-----+-----+-----+-----+
| LOD | | | | LOD |
+-----+-----+-----+-----+-----+

Update Frequencies:
- E (Entity): Every tick
- N (Neighbors, Depth 1): Every tick
- LOD (Depth 2): Every 2 ticks (50% frequency)
- LOD (Depth 3): Every 4 ticks (25% frequency)

Update frequency timeline

Tick 1: E, N, LOD2, LOD3 updated
Tick 2: E, N, LOD2 updated (LOD3 skipped)
Tick 3: E, N, LOD2, LOD3 updated
Tick 4: E, N, LOD2 updated (LOD3 skipped)
Tick 5: E, N, LOD2, LOD3 updated
...

Pattern:
- E, N: 100% (every tick)
- LOD2: 50% (every 2 ticks)
- LOD3: 25% (every 4 ticks)

Benefits

  • Bandwidth reduction: distant cells receive fewer updates (50-75% reduction)
  • Performance scaling: the system handles more entities with the same bandwidth
  • Smooth degradation: distant entities are still visible, just less frequently
  • Configurable: LodLevel can be adjusted per room/application

Trade-offs

  • Latency: distant updates arrive less frequently (acceptable for non-critical entities)
  • Perception: distant entities may appear slightly "choppy" (acceptable trade-off)
  • Complexity: requires careful tuning of LodLevel for optimal balance

3. Presenter Mode

Concept

Presenter Mode enables global broadcasting for special entities (speakers, hosts, streamers) that need to be visible to all players regardless of spatial location or group membership.

Implementation details

Presenter configuration

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (line 26):

public bool PresenterInGroupCast { get; private set; }
  • PresenterInGroupCast: flag that enables presenter mode (set during Init())
  • When enabled, entities with IsPresenter == true broadcast globally

Presenter detection

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 180-208):

public void OnEntityDataArrived(NetworkEntity entity, object operationData)
{
bool isPresenter = false;
if (PresenterInGroupCast)
{
isPresenter = entity.IsPresenter;
}
// if user is present in Custom Group, get the reference to that group
// and enqueue data in that group
if (entity.GroupId != 0 || isPresenter)
{
var group = FindGroupById(entity.GroupId);

if (group != null)
{
EnqueueDataInRelevantLod(group, operationData);
}

if (isPresenter)
{
EnqueueDataInRelevantGroups(entity, operationData);
}
}
else
{
EnqueueDataInRelevantGroups(entity, operationData);
}

}

Key logic:

  • Checks the PresenterInGroupCast flag first
  • If enabled, checks entity.IsPresenter
  • Presenters get special handling in update distribution

Presenter broadcasting

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 210-245):

private void EnqueueDataInRelevantGroups(NetworkEntity entity, object operationData)
{
if (AOIGlobal.IsActive)
{
AOIGlobal.EnqueueData(operationData);
}

if (isAOIActive)
{
if (entity.IsPresenter)
{
// enqueue update in all active cells
for (int i = ActiveCells.Count - 1; i >= 0; i--)
{
EnqueueDataInRelevantLod(ActiveCells[i], operationData);
}
}
else
{
var cords = GetIndex(entity.TransformLite);
int x = cords.Item1;
int z = cords.Item2;
var cell = AOICells[x, z];
if (cell == null)
{
var aOIGroup = new AOIGroup();
aOIGroup.Subscribe = SubscribeToSpatialGrid;
aOIGroup.UnSubscribe = UnSubscribeToSpatialGrid;
AOICells[x, z] = aOIGroup;
aOIGroup.SetIndexes(x, z);
cell = aOIGroup;
}
EnqueueDataInRelevantLod(cell, operationData);
}
}
}

Presenter behavior:

  • Normal entity: updates sent to its own cell + neighbors
  • Presenter entity: updates sent to all active cells in the grid

Visual representation

Normal Entity Update:
+-----+-----+-----+-----+
| | N | N | |
+-----+-----+-----+-----+
| N | E | N | | <- Only nearby cells
+-----+-----+-----+-----+
| N | N | N | |
+-----+-----+-----+-----+

Presenter Entity Update:
+-----+-----+-----+-----+
| A | A | A | A |
+-----+-----+-----+-----+
| A | P | A | A | <- ALL cells receive
+-----+-----+-----+-----+
| A | A | A | A |
+-----+-----+-----+-----+

Presenter + custom group interaction

When a presenter is also in a custom group:

From EntangleServer/Modules/Lobby/AreaOfInterest/AOISpatialHash.cs (lines 189-201):

if (entity.GroupId != 0 || isPresenter)
{
var group = FindGroupById(entity.GroupId);

if (group != null)
{
EnqueueDataInRelevantLod(group, operationData);
}

if (isPresenter)
{
EnqueueDataInRelevantGroups(entity, operationData);
}
}

Dual distribution:

  1. Custom group: update sent to custom group members
  2. Global broadcast: update also sent to all spatial cells (presenter behavior)

This ensures presenters are visible to both their group members and the entire spatial grid.

Use cases

  • Keynote speakers: conference speakers visible to all attendees
  • Event hosts: hosts that need global visibility
  • Streamers/broadcasters: content creators broadcasting to all viewers
  • System announcements: important messages that must reach everyone
  • VIP presentations: special presentations requiring global attention

Benefits

  • Global visibility: presenters are always visible regardless of location
  • Flexible: can combine with custom groups for hybrid distribution
  • Simple flag: easy to enable/disable per room
  • Performance: only active cells receive updates (not empty cells)

Trade-offs

  • Bandwidth: presenters generate more network traffic (broadcast to all cells)
  • Scalability: many presenters can increase load significantly
  • Use sparingly: should be reserved for truly important entities

Feature interaction matrix

Feature combinationUpdate distribution
Normal entityOwn cell + 8 neighbors + LOD
Entity in custom groupCustom group only (no spatial)
Presenter entityAll active cells + LOD
Presenter in custom groupCustom group + all active cells + LOD
Presenter (global mode)AOIGlobal (all players)

Performance considerations

Custom groups

  • Benefit: smaller group sizes mean less bandwidth per update
  • Cost: additional lookup (FindGroupById) per update
  • Optimization: consider using Dictionary<byte, AOIGroup> instead of List.Find() for O(1) lookup

LOD system

  • Benefit: 50-75% bandwidth reduction for distant cells
  • Cost: slight complexity in update distribution logic
  • Optimization: the counterLod increment is O(1), minimal overhead

Presenter mode

  • Benefit: ensures important entities are always visible
  • Cost: high bandwidth (broadcast to all active cells)
  • Optimization: limit the number of presenters, use sparingly

Configuration recommendations

Custom groups

  • Use for: teams, private channels, role-based groups
  • Limit: a reasonable number of groups per room (e.g. under 50)
  • Group size: optimal 5-20 players per group

LOD system

  • LodLevel = 0: disabled (maximum bandwidth, best quality)
  • LodLevel = 1: depth 2 only (balanced)
  • LodLevel = 2: depth 2 and 3 (maximum bandwidth savings)
  • Recommended: start with LodLevel = 1, adjust based on performance

Presenter mode

  • Enable only when needed (PresenterInGroupCast = true)
  • Limit presenters to 1-3 per room
  • Monitor bandwidth when presenters are active

Summary

These three features extend the AOI system beyond simple spatial partitioning:

  1. Custom groups: enable logical grouping independent of location
  2. LOD system: optimize bandwidth with tiered update frequencies
  3. Presenter mode: ensure global visibility for important entities

Together, they provide a flexible, performant system that scales from small groups to large-scale multiplayer environments while maintaining fine-grained control over update distribution.