Skip to content

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 correct GoapKeyType
  • [ ] 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

What's Next