Sensor

@three-blocks/proAISensorPerception
new Sensor()

Sensor - Base class for AI perception sensors

Sensors observe the environment and write data to State and Memory. Intents then read this data to make decisions. Sensors run at configurable intervals to balance accuracy vs performance.


Quick Start: Creating a Custom Sensor


The body Parameter

The body parameter in _sense() is the physics body (KinematicBody). Key properties you can access:

_sense(body, state, memory, delta) {
  // Position (THREE.Vector3)
  const pos = body.transformer.position;
  console.log(pos.x, pos.y, pos.z);

  // Rotation (THREE.Quaternion)
  const rot = body.transformer.quaternion;

  // Forward direction
  const forward = new THREE.Vector3(0, 0, 1).applyQuaternion(rot);

  // Body index (for identifying this agent)
  const index = body._index;

  // Custom data you attached
  const team = body.userData?.team;
  const health = body.userData?.health;
}

The state Parameter (State API)

State stores instant perception data. Values are immediately available and tracked with timestamps for staleness detection.

// Writing to state (in _sense)
state.set('targetVisible', true);
state.set('targetPosition', { x: 10, y: 0, z: 5 });
state.set('targetDistance', 15.5);
state.setMultiple({
  nearbyEnemyCount: 3,
  closestEnemyDistance: 5.2,
});

// Reading from state (typically in Intent)
const visible = state.get('targetVisible', false);  // false = default
const pos = state.get('targetPosition');            // null if not set
const age = state.getAge('targetVisible');          // seconds since update
const exists = state.has('targetPosition');         // boolean

The memory Parameter (Memory API)

Memory stores time-decaying information. Values fade over time and return null after expiry. Use for historical context.

// Remembering (in _sense)
memory.remember('lastDamageDirection', direction, 10);  // 10 second decay
memory.remember('lastSeenPosition', targetPos, 30);     // 30 second decay
memory.remember('wasRecentlyAttacked', true, 5);        // 5 second decay

// Recalling (typically in Intent)
const dir = memory.recall('lastDamageDirection');       // null if decayed
const confidence = memory.getConfidence('lastSeenPosition'); // 0.0-1.0
const hasMemory = memory.has('wasRecentlyAttacked');    // boolean
memory.refresh('lastSeenPosition');                     // reset decay timer
memory.forget('lastDamageDirection');                   // force forget

Complete Examples

Example 1: Proximity Sensor

Detects nearby enemies within a radius.

import { Sensor } from '@three-blocks/pro';

class ProximitySensor extends Sensor {
  static type = 'ProximitySensor';

  constructor(config = {}) {
    super({ updatePeriod: 0.2, ...config });
    this._radius = config.radius ?? 15;
    this._targetTeam = config.targetTeam ?? 'enemy';
  }

  _sense(body, state, memory, delta) {
    const myPos = body.transformer.position;
    const myTeam = body.userData?.team ?? 'neutral';

    // Get all kinematic bodies from physics state
    const kinematicBodies = this._getKinematicBodies();
    const nearbyEnemies = [];

    for (const other of kinematicBodies) {
      if (other._index === body._index) continue; // skip self

      const otherTeam = other.userData?.team ?? 'neutral';
      if (otherTeam !== this._targetTeam) continue;

      const otherPos = other.transformer.position;
      const dx = otherPos.x - myPos.x;
      const dz = otherPos.z - myPos.z;
      const distance = Math.sqrt(dx * dx + dz * dz);

      if (distance <= this._radius) {
        nearbyEnemies.push({
          index: other._index,
          position: { x: otherPos.x, y: otherPos.y, z: otherPos.z },
          distance,
        });
      }
    }

    // Sort by distance
    nearbyEnemies.sort((a, b) => a.distance - b.distance);

    // Write to state
    state.set('nearbyEnemies', nearbyEnemies);
    state.set('nearbyEnemyCount', nearbyEnemies.length);
    state.set('closestEnemy', nearbyEnemies[0] ?? null);
    state.set('hasEnemiesNearby', nearbyEnemies.length > 0);
  }

  serialize() {
    return {
      ...super.serialize(),
      radius: this._radius,
      targetTeam: this._targetTeam,
    };
  }
}

Example 2: Sound Sensor

Detects sounds and remembers their locations.

import { Sensor } from '@three-blocks/pro';

class SoundSensor extends Sensor {
  static type = 'SoundSensor';

  constructor(config = {}) {
    super({ updatePeriod: 0.1, ...config });
    this._hearingRange = config.hearingRange ?? 30;
    this._pendingSounds = [];
  }

  // Call this from your game when a sound occurs
  reportSound(position, loudness, type) {
    this._pendingSounds.push({ position, loudness, type });
  }

  _sense(body, state, memory, delta) {
    const myPos = body.transformer.position;
    const heardSounds = [];

    for (const sound of this._pendingSounds) {
      const dx = sound.position.x - myPos.x;
      const dz = sound.position.z - myPos.z;
      const distance = Math.sqrt(dx * dx + dz * dz);

      // Loudness affects hearing range
      const effectiveRange = this._hearingRange * sound.loudness;

      if (distance <= effectiveRange) {
        heardSounds.push({
          position: sound.position,
          type: sound.type,
          distance,
        });

        // Remember sound location for investigation
        memory.remember('lastHeardSound', {
          position: sound.position,
          type: sound.type,
        }, 20); // 20 second memory
      }
    }

    this._pendingSounds = []; // Clear processed sounds

    state.set('heardSounds', heardSounds);
    state.set('heardSoundThisFrame', heardSounds.length > 0);
  }

  serialize() {
    return {
      ...super.serialize(),
      hearingRange: this._hearingRange,
    };
  }
}

Example 3: Ammo Sensor

Tracks ammunition status.

import { Sensor } from '@three-blocks/pro';

class AmmoSensor extends Sensor {
  static type = 'AmmoSensor';

  constructor(config = {}) {
    super({ updatePeriod: 0.1, ...config });
    this._lowAmmoThreshold = config.lowAmmoThreshold ?? 10;
  }

  _sense(body, state, memory, delta) {
    const ammo = body.userData?.ammo ?? 30;
    const maxAmmo = body.userData?.maxAmmo ?? 30;

    state.set('ammo', ammo);
    state.set('ammoPercent', ammo / maxAmmo);
    state.set('isLowAmmo', ammo <= this._lowAmmoThreshold);
    state.set('isOutOfAmmo', ammo <= 0);
  }

  serialize() {
    return {
      ...super.serialize(),
      lowAmmoThreshold: this._lowAmmoThreshold,
    };
  }
}

Configuration Options

Option Type Default Description
updatePeriod number 0.1 Seconds between _sense() calls (0.1 = 10 Hz)
enabled boolean true Whether sensor is active

Lifecycle

  1. Construction: constructor(config) - Initialize sensor with config
  2. Each Frame: update(body, state, memory, delta) - Called every frame
  3. Throttled: _sense(body, state, memory, delta) - Called when updatePeriod elapses
  4. Cleanup: dispose() - Called when sensor is removed
Example
import { Sensor } from '@three-blocks/pro';

class HealthSensor extends Sensor {
  // REQUIRED: Unique type identifier for serialization
  static type = 'HealthSensor';

  constructor(config = {}) {
    // Pass config to parent (sets updatePeriod, enabled)
    super({ updatePeriod: 0.5, ...config });

    // Store custom config
    this._lowHealthThreshold = config.lowHealthThreshold ?? 30;
  }

  // REQUIRED: Override _sense() to implement perception logic
  _sense(body, state, memory, delta) {
    // Read health from body (your game logic)
    const health = body.userData?.health ?? 100;

    // Write to state (instant values, read by intents)
    state.set('health', health);
    state.set('isLowHealth', health < this._lowHealthThreshold);

    // Write to memory (time-decaying, for historical context)
    if (health < this._lowHealthThreshold) {
      memory.remember('lastLowHealthTime', Date.now(), 60); // remember for 60s
    }
  }

  // REQUIRED: Override serialize() to include custom config
  serialize() {
    return {
      ...super.serialize(),
      lowHealthThreshold: this._lowHealthThreshold,
    };
  }
}

Properties

.type : string

Sensor type identifier. REQUIRED: Override in subclass.

This string is used for serialization and factory lookup. Must match the key used when registering the sensor factory.

.id : string

Unique ID for this sensor instance. Auto-generated, used for tracking and removal.

.type :

Get sensor type.

.enabled :

Get whether sensor is enabled.

.enabled :

Set whether sensor is enabled.

.updatePeriod :

Get update period.

.updatePeriod :

Set update period.

Methods

update#

update(body : Object, state : State, memory : Memory, delta : number)

Update the sensor. Called every frame by SensorManager. Throttles calls to _sense() based on updatePeriod.

Do not override this method. Override _sense() instead.

Parameters
bodyObject
The physics body (KinematicBody).
The perception state to write to.
The time-decaying memory to write to.
deltanumber
Time delta in seconds since last frame.

set#

set(config : Object) : Sensor

Configure sensor parameters at runtime.

Parameters
configObject
Configuration object.
  • updatePeriodoptionalnumber
    New update period.
  • enabledoptionalboolean
    New enabled state.
Returns
Sensor — This instance for chaining.

emit#

emit(eventType : string, params : Object)

Emit an event to the main thread.

Events are queued and dispatched to body.ai EventDispatcher on the main thread. Use this to notify user code of sensor state changes (e.g., target acquired, target lost).

Parameters
eventTypestring
Event type name (e.g., 'targetAcquired').
paramsoptionalObject
Event payload data.
Default is {}.

serialize#

serialize() : Object

Serialize sensor configuration for worker transfer.

REQUIRED: Override in subclass to include custom config. Always call super.serialize() and spread the result.

Returns
Object — Serialized configuration including type, id, and all config.

dispose#

dispose()

Clean up sensor resources. Override in subclass if you need to clean up event listeners, timers, or other resources.