Photon Realtime Interest Groups: Spatial Filtering Without String IDs
Stop sending every player's position to every client. Use Photon Realtime Interest Groups to subscribe only to nearby grid cells.
28 Feb 2026, 22:57 UTC

The Problem: Every Player Hears Every Player
In a Photon Realtime room, a position update is not automatically filtered by distance. If twenty players move at once, each client receives twenty position packets per tick, even for players on the far side of the map. The client must receive, parse, and discard the irrelevant ones, wasting bandwidth and CPU cycles. This works for small rooms, but it becomes a bottleneck as player count or object density grows.
The useful takeaway: Photon Realtime provides Interest Groups—byte-scoped logical channels (0–255)—that let a client subscribe only to the updates it actually needs. You build the mapping from game space to group IDs; the server handles routing to subscribers.
What Interest Groups Actually Do
An Interest Group is not a spatial system built into Photon. It is a subscription mechanism. A client calls JoinGroup(byte) to start receiving events sent to that group, and LeaveGroup(byte) to stop. Events raised with a group target are forwarded only to clients currently subscribed to that group. Group 0 is the default; every client in the room is typically subscribed to it unless your code leaves it.
Because groups are bytes, you have at most 256 simultaneous channels. That is enough for a coarse grid, but not for a unique ID per object.
Worked Example: Grid Cells as Byte Groups
Divide the playable area into fixed cells. Derive a group ID from the cell coordinates and subscribe to the cell the player occupies. The example below assumes Photon Realtime or PUN 2 and runs on the local client after the player has joined a room.
Prerequisites and Permissions
- Run this code on the client that owns the player avatar.
- The client must be connected and joined to a room before calling
JoinGrouporLeaveGroup. - Confirm the exact API names in your SDK reference; property names for event targeting vary between Photon Realtime and PUN versions.
using UnityEngine;
using Photon.Pun;
public class GridInterest : MonoBehaviourPunCallbacks
{
const float CellSize = 10f;
const float HysteresisDistance = 2f;
const byte DefaultGroup = 0;
byte lastGroup = DefaultGroup;
Vector3 lastSwitchPosition;
void Update()
{
if (!PhotonNetwork.InRoom) return;
byte group = GroupIdForPosition(transform.position);
// Hysteresis: avoid rapid group switching when riding a cell boundary.
if (group != lastGroup &&
Vector3.Distance(transform.position, lastSwitchPosition) > HysteresisDistance)
{
// Optional: stay in DefaultGroup for room-wide messages by not leaving it.
if (lastGroup != DefaultGroup)
{
PhotonNetwork.LeaveGroup(lastGroup);
}
PhotonNetwork.JoinGroup(group);
lastGroup = group;
lastSwitchPosition = transform.position;
}
}
static byte GroupIdForPosition(Vector3 pos)
{
int cellX = Mathf.FloorToInt(pos.x / CellSize);
int cellY = Mathf.FloorToInt(pos.z / CellSize); // adjust axis for your world orientation
int id = (cellX * 7919 + cellY) % 256;
return (byte)id;
}
void SendPositionUpdate(Vector3 pos)
{
if (!PhotonNetwork.InRoom) return;
var options = new RaiseEventOptions();
// SDK-dependent: set the interest-group target on options before raising.
// Example: options.TargetGroup = lastGroup; // verify in your SDK reference
PhotonNetwork.RaiseEvent(
eventCode: 1,
customContent: pos,
raiseEventOptions: options,
sendOptions: SendOptions.SendUnreliable
);
}
}
Risk: If you target a group that no client has joined, the event is silently dropped. If you mistype a group ID, there is no compile-time error because groups are numeric.
Trade-offs and Limits
- Group ceiling: Only 256 groups exist. A fine grid may force you to reuse IDs, which can leak updates to unintended cells.
- No automatic catch-up: Joining a group does not replay state for objects already inside it. You must implement a separate request–response, such as asking the cell owner or server for the current snapshot after
JoinGroup. - Server cost remains: The server still evaluates group membership and routes events. Interest Groups reduce client bandwidth, not server-side routing work.
- Edge flicker: Players near cell boundaries can oscillate between groups. The hysteresis distance above reduces this, but adds a small delay before relevance updates.
How to Verify Without Guessing
Do not rely on the dashboard alone; per-client incoming packet breakdown is not guaranteed to be exposed there. Instead, build a local test with twenty moving bots and log your SDK’s incoming traffic statistics over a fixed interval, first with global broadcasts and then with grid-based groups. Compare incoming bytes per second. Expect lower client traffic with grouping, but the exact margin depends on cell size, player density, and update frequency.
Closing Checklist
- Map world coordinates to byte group IDs (0–255).
- Call
JoinGroupandLeaveGrouponly after joining a room. - Add hysteresis or overlap to prevent boundary flicker.
- Implement a state-catch-up request for newly joined groups.
- Verify with client-side traffic logs, not assumptions.
If a subscription causes problems, the rollback is simple: call LeaveGroup for that group ID to stop receiving its events.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.