Files
skills/docs/gaming/games/game-server/src/enhanced/interest-management/interest-manager/interest-manager.md
2026-05-29 08:20:56 +01:00

4.3 KiB
Raw Blame History

interest-manager

(no submodules)

Purpose

InterestManager implements area-of-interest (AOI) culling for open-world multiplayer games: it tracks zones, entities, and players in memory and returns the bounded slice of entities each player can see, ranked by priority. The class is the standard mechanism for shrinking broadcast scope from O(world) to O(perceived) updates.

It is dependency-injectable: a Clock source provides timestamps and an OpenTelemetry Meter produces visibility-calculation counters and duration histograms. Configuration is validated with Zod; invalid input raises GAME_SERVER.INVALID_INTEREST_CONFIG. Unknown players passed to updatePlayerPosition raise GAME_SERVER.INTEREST_PLAYER_NOT_REGISTERED.

Features

  • Three zone shapes: circular, rectangular (optionally 3D), custom (predicate-based).
  • Per-player viewDistance override (defaults to config.defaultViewDistance).
  • Priority-weighted visibility ranking via maxPriority - distance × decay + sharedZones × 0.5 with output clamped into [0, maxPriority].
  • getVisibleEntities slices to maxEntitiesPerPlayer.
  • calculateVisibilityChanges emits enter/exit deltas only when the set actually changes (returns null on no-op).
  • Typed event registry: entity:added/removed, player:subscribed/unsubscribed, visibility:changed.
  • OTel: game_server.interest.visibility_calc_total (counter), game_server.interest.visibility_calc_duration_ms (histogram).

Usage: Basic

import { InterestManager } from "@suaveplan/game-server/enhanced/interest-management/interest-manager";

const aoi = new InterestManager({ defaultViewDistance: 50 });
aoi.registerPlayer("p1", { x: 0, y: 0 });
aoi.updateEntity("npc", { x: 10, y: 0 }, { kind: "merchant" });
const visible = aoi.getVisibleEntities("p1");

Usage: Advanced

import { InterestManager } from "@suaveplan/game-server/enhanced/interest-management/interest-manager";

const aoi = new InterestManager<{ name: string }>(
    {
        defaultViewDistance: 80,
        maxEntitiesPerPlayer: 100,
        priorityDecayRate: 0.05,
        maxPriority: 20,
    },
    { clock: () => testNow }
);

aoi.registerZone({
    id: "downtown",
    shape: "rectangular",
    center: { x: 0, y: 0, z: 0 },
    width: 200,
    height: 200,
    depth: 30,
    priority: 5,
});

aoi.registerPlayer("p1", { x: 1, y: 1, z: 0 });
aoi.subscribeToZone("p1", "downtown");

const unsub = aoi.on("visibility:changed", (change) => {
    syncEnterExitToClient(change.playerId, change.entered, change.exited);
});

// In the server tick:
aoi.updateEntity("merchant", { x: 5, y: 5, z: 0 }, { name: "Sera" });
aoi.calculateVisibilityChanges("p1");

unsub();

API Reference

  • new InterestManager(config, deps?) — validates config with InterestManagerConfigSchema; throws GAME_SERVER.INVALID_INTEREST_CONFIG on failure.
  • registerZone(zone) / unregisterZone(zoneId); throws GAME_SERVER.INTEREST_REGION_INVALID on empty zone.id.
  • updateEntity(id, position, data?, type?) / removeEntity(id).
  • registerPlayer(id, position, viewDistance?) / updatePlayerPosition(id, position) / unregisterPlayer(id).
  • subscribeToZone(playerId, zoneId) / unsubscribeFromZone(playerId, zoneId).
  • getVisibleEntities(playerId) / calculateVisibilityChanges(playerId).
  • isInZone(zoneId, point) / getZonesAtPoint(point) / getZone / getAllZones / getEntity / getStats / dispose.
  • on(event, listener) → unsubscribe; off(event, listener).

Implementation Notes

Geometry primitives (vectorDistance, zoneContains, calcPriority) live in interest-manager.internal.ts so the main class is dominated by lifecycle, dispatch, and metrics emission. getVisibleEntities is the hot path; it sorts the candidate set in descending priority and slices to maxEntitiesPerPlayer so the result is always bounded. The shared-zones walk only checks zones the player explicitly subscribed to, so an entity in an unsubscribed zone is treated as if the zone were absent. Visibility deltas are derived against the previous result rather than a snapshot of the world to support incremental synchronisation. dispose clears every internal collection so the manager can be safely garbage-collected after shutdown.