Sensor
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
- Construction:
constructor(config)- Initialize sensor with config - Each Frame:
update(body, state, memory, delta)- Called every frame - Throttled:
_sense(body, state, memory, delta)- Called when updatePeriod elapses - 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
set#
set(config : Object) : SensorConfigure sensor parameters at runtime.
Parameters
configObjectupdatePeriodoptionalnumberNew update period.enabledoptionalbooleanNew 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
eventTypestringparamsoptionalObjectDefault is
{}.serialize#
serialize() : ObjectSerialize 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.