Interest Area

Overview

Area of Interest is a bandwidth optimization that limits which objects a client receives state updates for. Instead of replicating every object to every client, AOI uses interest keys to categorize objects and player subscriptions to control visibility. Clients only receive updates for objects whose interest key matches one of their subscribed keys.

Interest Key Model

Unlike grid-based AOI systems, Fusion 3.0 uses a key-based model. Each object is assigned an interest key (a uint64_t), and each player subscribes to a set of keys. The server matches object keys against player subscriptions to determine which objects to replicate.

Entity Key / Subscription Result
Object A key: 42 Received by Player 1 and Player 2
Object B key: 99 Received by Player 1 only
Object C global Received by everyone
Player 1 subscribed: 42, 99 Receives A, B, C
Player 2 subscribed: 42 Receives A, C

InterestKeyType

Each object's interest key has a type that determines how it is handled:

C++

enum class InterestKeyType : uint8_t {
    Global = 0,   // Replicated to all clients regardless of subscriptions
    Area   = 1,   // Matched against area key subscriptions
    User   = 2    // Matched against user key subscriptions
};
Type Behavior Use Case
Global Sent to all clients. No subscription needed. Game managers, scoreboards, UI state
Area Matched against area subscriptions. Subscriptions are replaced as a set via SetAreaKeys(). Spatial regions, level zones
User Matched against user subscriptions. Added and removed individually. Teams, parties, private channels

Setting Object Interest Keys

Each object declares its interest key via one of three static methods on Client. Interest keys can only be set on root objects — called on an ObjectChild, these methods log a warning and do nothing (children follow their root's interest).

Global Interest

C++

static void Client::SetGlobalInterestKey(Object* obj);

Makes the object visible to all clients by storing key 0. This is also the default for objects that have never been assigned a key.

Area Interest

C++

static void Client::SetAreaInterestKey(Object* obj, uint64_t key);

Assigns an area-type interest key. Only clients subscribed to this key (via SetAreaKeys()) receive updates. The key value is application-defined -- it could represent a spatial region, a zone ID or any grouping concept.

User Interest

C++

static void Client::SetUserInterestKey(Object* obj, uint64_t key);

Assigns a user-type interest key. Only clients subscribed to this key (via AddUserKey()) receive updates.

Key Encoding

The stored 64-bit key steals bit 0 to distinguish area from user keys: an area key is stored as (key << 1) | 1, a user key as key << 1, and 0 means global. Two consequences:

  • The top bit of your application key is lost in the shift — usable key values are 63 bits, not 64.
  • SetUserInterestKey(obj, 0) encodes to 0 and is therefore identical to SetGlobalInterestKey(obj) — user key 0 cannot be used as a real subscription key.

The matching SetAreaKeys / AddUserKey subscription methods apply the same encoding, so application code always works with the un-shifted key value on both sides.

Clearing

C++

static void Client::ClearInterestKey(Object* obj);

Stores key 0 and clears the assigned-key flag, reverting the object to global visibility.

Querying

C++

static bool Client::HasSetInterestKey(Object* obj);
static InterestKeyType Client::GetInterestKeyType(Object* obj);

GetInterestKeyType derives the type from the stored key: 0 is Global, an odd value is Area, an even non-zero value is User.

ObjectTail Storage

The interest key is stored in the ObjectTail at the end of the Words buffer:

C++

#pragma pack(push, 4)
struct ObjectTail {
    uint32_t Reserved[8];
    int32_t  RequiredObjectsCount;
    uint64_t InterestKey;       // 8 bytes (2 words)
    int32_t  Destroyed;
    int32_t  RoomSendRate;
    PlayerId PredictingPlayer;
    uint32_t RejectedSequence;
    uint32_t InputSequence;
    uint32_t InputTime;
    int32_t  Dummy;
};
#pragma pack(pop)

The SetGlobalInterestKey, SetAreaInterestKey and SetUserInterestKey methods write to this field. Do not write to the interest key directly -- always use the Client methods, which also track the key type and registration state.

Player Subscriptions

A client's key subscriptions are held in an InterestKeySet, obtained from Client:

C++

InterestKeySet& Client::GetLocalInterestKeys();
InterestKeySet& Client::GetPlayerInterestKeys(PlayerId player);

In Shared-Authority mode each client manages its own subscriptions through GetLocalInterestKeys(). In Client-Server mode the simulation server decides interest for every player and manages each player's set through GetPlayerInterestKeys(player).

Each set holds two independent kinds of keys -- area keys and user keys -- mutated through the methods below.

Area Keys

C++

void InterestKeySet::SetAreaKeys(const std::vector<std::tuple<uint64_t, uint8_t>>& keys);

Sets the complete list of area key subscriptions. Each entry is a (key, sendRate) tuple:

  • key -- the interest key value to subscribe to
  • sendRate -- desired send rate for objects with this key (lower = faster updates)

Area keys are replaced as a set — each SetAreaKeys() call removes all current area subscriptions and installs the given ones (user keys are untouched). Between calls the subscriptions persist; the SDK never clears them on its own. Call it when the region set actually changes (e.g., the player crosses into a new zone) — calling it every frame is unnecessary and, with identical keys, still re-marks the set as dirty.

C++

// Subscribe to region 42 at full rate, and neighbor region 43 at half rate
std::vector<std::tuple<uint64_t, uint8_t>> areaKeys = {
    {42, 1},   // Full rate
    {43, 2},   // Half rate
};
client->GetLocalInterestKeys().SetAreaKeys(areaKeys);

When multiple area keys match the same object, the minimum send rate (fastest) is used.

User Keys

C++

void InterestKeySet::AddUserKey(uint64_t key, uint8_t sendRate = 0);
void InterestKeySet::RemoveUserKey(uint64_t key);

User keys are added and removed individually -- they remain active until explicitly removed and survive SetAreaKeys() calls. This is designed for stable subscriptions like team channels or party membership.

C++

// Subscribe to team channel
client->GetLocalInterestKeys().AddUserKey(teamId, 1);

// Later: leave team
client->GetLocalInterestKeys().RemoveUserKey(teamId);

Clearing Subscriptions

C++

void InterestKeySet::Clear();          // Clear all keys (area and user)
void InterestKeySet::ClearAreaKeys();  // Clear only area keys
void InterestKeySet::ClearUserKeys();  // Clear only user keys

Querying Subscriptions

C++

std::vector<std::tuple<uint64_t, uint8_t>> InterestKeySet::GetAllAreaKeys() const;
std::vector<std::tuple<uint64_t, uint8_t>> InterestKeySet::GetAllUserKeys() const;

The raw subscription map is available as the public InterestKeySet::Keys member (std::map<uint64_t, uint8_t>). It holds the encoded keys (see Key Encoding); the GetAll*Keys accessors return the decoded application values.

Interest Enter/Exit Callbacks

When an object enters or exits a client's interest set, the SDK fires:

C++

Broadcaster<void(ObjectRoot*)> OnInterestEnter;
Broadcaster<void(ObjectRoot*)> OnInterestExit;

OnInterestEnter

Fires when an object becomes visible to the local client. The transition is decided by the server: each received object update carries an in-interest-set flag, and the SDK fires OnInterestEnter when the flag turns on for an object that was previously out of interest — whether because the client's subscriptions changed or because the object's key changed.

For a newly created object, the enter callback on its create update fires only in one case: owner modes MasterClient and above, on the master client itself. For all other objects, expect the regular creation callbacks (OnObjectReady) instead of an enter event.

On enter, the server performs a snap -- it sends the full object state immediately rather than waiting for the next delta update. This ensures the client sees the object in its current state without interpolation artifacts.

OnInterestExit

Fires when an object leaves the client's interest set. The client stops receiving updates for this object. The engine integration should typically:

  • Hide or despawn the engine entity.
  • Stop reading from the object's Words buffer (data will become stale).

C++

subs += client->OnInterestEnter.Subscribe([](FusionCore::ObjectRoot* obj) {
    // Object became visible -- instantiate or show engine entity
    spawn_or_show(obj);
});

subs += client->OnInterestExit.Subscribe([](FusionCore::ObjectRoot* obj) {
    // Object left interest -- hide or despawn engine entity
    hide_or_despawn(obj);
});

Multiple Area Key Merging

When a client subscribes to multiple area keys and an object matches more than one subscription, the server uses the minimum send rate (fastest updates) among all matching keys.

Subscription Object Key Effective Send Rate
Area key 42, rate = 4 42 4
Area key 43, rate = 1 43 1
Both 42 and 43 matches both min(4, 1) = 1

Complete Example

C++

// === Object side: assign interest keys ===

// Player avatar: area-based interest
client->SetAreaInterestKey(playerObj, regionId);

// Scoreboard: global (visible to all)
client->SetGlobalInterestKey(scoreboardObj);

// Team chat relay: user key based
client->SetUserInterestKey(teamRelayObj, teamId);


// === Player side: manage subscriptions ===

// Subscribe to nearby regions (call again when the region set changes)
auto nearbyRegions = calculate_nearby_regions(playerPosition);
std::vector<std::tuple<uint64_t, uint8_t>> areaKeys;
for (auto& [regionId, distance] : nearbyRegions) {
    uint8_t rate = distance == 0 ? 1 : 4;  // Close = fast, far = slow
    areaKeys.push_back({regionId, rate});
}
client->GetLocalInterestKeys().SetAreaKeys(areaKeys);

// Subscribe to team channel (once, persistent)
client->GetLocalInterestKeys().AddUserKey(myTeamId, 1);


// === React to enter/exit ===

subs += client->OnInterestEnter.Subscribe([](FusionCore::ObjectRoot* obj) {
    instantiate_visual(obj);
});

subs += client->OnInterestExit.Subscribe([](FusionCore::ObjectRoot* obj) {
    destroy_visual(obj);
});

Interaction with Ownership

AOI and Ownership are independent systems:

  • An owner always receives updates for their own objects regardless of AOI.
  • When an object leaves a non-owner's interest, the client stops receiving updates. Re-entering triggers a snap.
  • Ownership transfer (SetWantOwner) works regardless of AOI -- a client can request ownership of an object outside its interest region.

Common Mistakes

Mistake Symptom
Calling SetAreaKeys every frame with unchanged keys Wasted work — subscriptions persist until replaced
Using area keys for persistent subscriptions Keys replaced on next SetAreaKeys call
Setting an interest key on an ObjectChild Warning logged, call ignored — keys belong to the root
Using user key 0 or keys with bit 63 set Key 0 means global; the top bit is lost in the encoding
Not handling OnInterestExit Stale engine entities with outdated state
Setting interest key without player subscriptions Object invisible to non-owning clients
  • {VersionPath}/manual/ownership -- Interaction between AOI and ownership
  • {VersionPath}/manual/architecture -- Object hierarchy and data model
  • {VersionPath}/manual/time -- When interest callbacks fire in the frame loop
Back to top