Custom Sensors¶
The built-in sensors (Optical, Auditory, Sphere, Waypoint) cover common use cases, but you'll often need sensors tailored to your game. This guide walks through building one from scratch.
When to Create a Custom Sensor¶
- You need detection logic the built-in sensors don't provide (e.g., threat scoring, multi-target tracking, inventory checks).
- You want to combine multiple detection methods into a single sensor.
- You need to read from game-specific systems (quest state, faction reputation, resource counts).
Step 1: Define the Settings Class¶
Extend GoapSensorLocalSettings with any per-agent configuration your sensor needs:
using System;
using RGS.GOAP.Core;
[Serializable]
public class MySensorSettings : GoapSensorLocalSettings
{
public float Radius = 10f;
public LayerMask TargetLayers;
public string[] TargetTags;
}
If your sensor needs no extra settings, you can use GoapSensorLocalSettings directly.
Step 2: Create the Sensor Class¶
using System;
using UnityEngine;
using RGS.GOAP.Core;
[CreateAssetMenu(fileName = "New My Sensor", menuName = "RGS/GOAP/Sensors/My Sensor")]
public class MySensorSO : GoapSensorSO
{
// Output name constants — must match what GetOutputs() returns
public const string OUT_TARGET_POS = "TargetPosition";
public const string OUT_FOUND = "Found";
public float DefaultRadius = 10f;
public LayerMask DefaultTargetLayers;
public override Type GetSettingsType() => typeof(MySensorSettings);
public override SensorOutputConfig[] GetOutputs() => new[]
{
new SensorOutputConfig(OUT_TARGET_POS, GoapKeyType.Vector3),
new SensorOutputConfig(OUT_FOUND, GoapKeyType.Boolean),
};
public override void OnUpdate(GameObject agentGO, GoapSensorLocalSettings baseSettings,
GoapBlackboard blackboard)
{
var settings = baseSettings as MySensorSettings;
if (settings == null) return;
float radius = settings.Radius > 0 ? settings.Radius : DefaultRadius;
Vector3 origin = agentGO.transform.position;
// Detection logic
var colliders = Physics.OverlapSphere(origin, radius, settings.TargetLayers);
GameObject bestTarget = null;
float bestDist = float.MaxValue;
foreach (var col in colliders)
{
if (!SensorUtils.IsValidTarget(col.gameObject, settings.TargetLayers,
settings.TargetTags))
continue;
float dist = Vector3.SqrMagnitude(col.transform.position - origin);
if (dist < bestDist)
{
bestDist = dist;
bestTarget = col.gameObject;
}
}
// Write results using helpers (auto-routes temporal keys)
WriteBool(settings, blackboard, OUT_FOUND, bestTarget != null);
if (bestTarget != null)
{
// Use the target parameter for entity tracking in recollection
WriteVector3(settings, blackboard, OUT_TARGET_POS,
bestTarget.transform.position, bestTarget);
}
}
public override void DrawGizmos(GameObject agentGO, GoapSensorLocalSettings baseSettings)
{
var settings = baseSettings as MySensorSettings;
float radius = settings?.Radius > 0 ? settings.Radius : DefaultRadius;
Gizmos.color = new Color(0f, 1f, 0f, 0.3f);
Gizmos.DrawWireSphere(agentGO.transform.position, radius);
}
}
Key Points¶
Output Declaration¶
Every sensor must implement GetOutputs() to declare its outputs with names and types. The GOAP Hub uses this to create output mapping slots that you wire to blackboard keys.
public override SensorOutputConfig[] GetOutputs() => new[]
{
new SensorOutputConfig("TargetPosition", GoapKeyType.Vector3),
new SensorOutputConfig("Found", GoapKeyType.Boolean),
};
Write Helpers and Temporal Routing¶
Always use the typed write helpers (WriteBool, WriteFloat, WriteVector3, etc.) instead of writing directly to the blackboard. These helpers automatically check if a key is managed by the RecollectionSystem:
- Non-temporal key: writes directly to the blackboard.
- Temporal key: calls
ReportStimulus()instead, enabling confidence decay and multi-entity tracking.
Warning
Always use WriteVector3 with the target parameter when writing entity positions. Without the target, temporal key routing won't track the entity for confidence decay.
DrawGizmos¶
Implement DrawGizmos() to visualize your sensor in the Scene view. This is invaluable for debugging detection ranges, angles, and coverage.
GetSettingsType¶
Override GetSettingsType() to return your custom settings class. This tells the GOAP Hub what inspector to show for per-agent sensor configuration.
Sensor Timing¶
- Sensors fire at their
UpdateInterval(default 0.5s), not every frame. - First tick is staggered using a deterministic offset based on the agent's InstanceID.
- Design all code that reads sensor-driven keys to tolerate stale data — values may be up to one full interval old.
Checklist¶
Before shipping a custom sensor:
- [ ]
GetOutputs()declares all outputs with correctGoapKeyType - [ ] Uses typed write helpers (
WriteBool,WriteVector3, etc.) - [ ] Uses
WriteVector3(settings, bb, name, value, target)for entity tracking - [ ] No expensive work outside of
OnUpdate(beliefs must stay cheap) - [ ]
DrawGizmos()implemented for scene debugging - [ ]
GetSettingsType()returns the correct settings class
See Also
- Blackboard — Regular Keys vs Temporal Keys — Understanding when to use
WriteVector3with entity tracking - Guard Post Demo — Spatial Memory — See temporal sensors in a complete example
- Performance Tuning — Tuning sensor intervals for large agent counts
- Glossary — Quick definitions for Sensor, Temporal Key, ValidityKey, and related terms
What's Next¶
- Sensor Controller — How the framework manages sensor lifecycle and timing.
- Blackboard — The key-value store sensors write to.
- Override System — Tune sensor settings per-agent.