first commit
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# interest-manager
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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
|
||||
|
||||
```ts
|
||||
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
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user