The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.
Two dimensions use a separate physics API and configuration
A flat-looking mesh is not enough to choose the 2D physics world. Collider2D and Rigidbody2D participate in that world, while Collider and Rigidbody belong to the separate 3D system. The callback signature is part of the choice: OnTriggerEnter2D accepts Collider2D. Sensor2DProbe deliberately uses a static trigger sensor and a dynamic actor body to make the minimum relationship visible. Gravity is disabled for the controlled top-down crossing, and the relevant Physics 2D layer pair is enabled. Starting from this explicit scene avoids trying to repair a missing 2D message by adding unrelated 3D settings or renaming a method alone.
Resolve actor identity from the attached body
The callback first checks isActiveAndEnabled because a disabled MonoBehaviour can still receive a physics trigger message. It next tests the incoming collider GameObject’s layer against actorLayers. For compound bodies, the attachedRigidbody identifies the body owning that child collider, so its GameObject becomes the runtime actor identity. Without a body on the other side, the collider’s own object is the fallback. HashSet.Add returns true only on the first occurrence of that identity, and only that branch logs a new count. Notice that the mask is checked on the collider object, so assigning the layer only to a parent does not excuse incorrectly configured collider children.
private void OnTriggerEnter2D(Collider2D other)
{
if (!isActiveAndEnabled) return;
if ((actorLayers.value & (1 << other.gameObject.layer)) == 0) return;
Rigidbody2D body = other.attachedRigidbody;
GameObject actor = body != null ? body.gameObject : other.gameObject;
if (countedActors.Add(actor.GetInstanceID()))
Debug.Log("Unique 2D actors: " + countedActors.Count, this);
}
}Separate duplicate policy from contact generation
The portable source supplies actor-a repeatedly and actor-b once. A string set retains two distinct identifiers, and the later Add for actor-b returns False. This establishes the counting policy for supplied identities, including the fact that repeated entries do not increase membership. It says nothing about how many Collider2D objects generated callbacks, whether the physics layer pair was enabled, or whether a body was simulated. The ordinary set also has no Unity destroyed-object semantics. Use it to reason about what counts as one gameplay actor, then establish the mapping from real collider to actor in the separately configured scene.
A lifetime count is different from current occupancy
This set lives for the component instance and is not cleared on exit or on re-enable. A person walking out and returning is intentionally still a previously counted actor. If the requirement is the number of actors currently inside, this algorithm is insufficient: multiple collider enter and exit events require per-actor overlap counts, and destruction or disabling needs a cleanup policy. Runtime GetInstanceID values are useful for this session’s identity but are not persistent save identifiers. Keep those requirements distinct rather than adding OnTriggerExit2D with a simple Remove and assuming compound-body occupancy is solved.
Practice: cross with one collider, then a compound body
Begin with one dynamic actor and one sensor and verify a single count in a fresh Play session. Add a second child Collider2D under the same actor body, set its layer explicitly, and cross again. The actor should still contribute one unique body identity even if several collider messages arrive. Next create a truly separate Rigidbody2D actor and observe the count increase. As an independent setup test, replace a required 2D collider with its 3D namesake in a copied scene and predict why that is not the same contact configuration. Restore the baseline instead of retaining a mixture of two physics systems by accident.
- Hint: inspect attachedRigidbody and collider GameObject layers separately.
- Hint: use a controlled physics-aware movement path rather than treating an arbitrary Transform teleport as a contact guarantee.
Practice: choose lifetime membership or overlap accounting
Extend the plain ledger to distinguish first-ever visits from current occupancy. Supply two enter records for one actor and two matching exit records, then decide when occupancy becomes zero. A dictionary of positive overlap counts can represent that policy, but it must reject unmatched exits or document their treatment. Keep that model free of Unity types so the boundary checks remain ordinary arithmetic. Then write down what an engine adapter would need to supply when an actor is destroyed while overlapping, because a pure ledger cannot discover that event on its own. This second exercise changes the requirement deliberately rather than silently reinterpreting the original unique-visitor count.
- Hint: a set records presence, while a compound-collider occupancy model may need a count.
- Hint: author expected records before choosing callbacks and cleanup hooks.
Keep the physics setup and counting rule separately reviewable
The final scene review should name both Collider2D types, the participating Rigidbody2D, body simulation state, trigger flag and Physics 2D layer pair. The rule review should state body-based identity, once-per-instance counting and the disabled-behaviour guard. Each list explains a different class of failure. The article uses Unity6000.0 references and a real component script, but no local Editor execution is claimed. Desktop execution established only the separate string-ledger output. When this sensor becomes a doorway, quest detector or occupancy zone, revise the gameplay contract first and then verify the scene adapter that feeds it.
Unity Editor example
Set up in Unity 6
- In a 2D scene, create a Sensor object with BoxCollider2D, set Is Trigger on, and attach Sensor2DProbe.cs. Do not substitute a 3D BoxCollider.
- Create an Actor with Rigidbody2D set to Dynamic, Gravity Scale zero, and a nontrigger CircleCollider2D. Use Z zero for both objects and enable Simulated on the body.
- Create an Actor2D layer, assign it to every actor collider GameObject, include it in actorLayers, and enable the relevant pair in Project Settings > Physics 2D > Layer Collision Matrix.
- Enter Play mode and move the dynamic actor through the sensor using a Rigidbody2D-aware mover or a controlled initial velocity. Verify one enter log; repeat with a second actor.
- For the compound test, put two Collider2D children under the same Rigidbody2D actor, give both children Actor2D, and pass through again in a fresh Play session. Disable only Sensor2DProbe as a separate guard test.
Sensor2DProbe.cs
Download Unity script ↗using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(BoxCollider2D))]
public sealed class Sensor2DProbe : MonoBehaviour
{
[SerializeField] private LayerMask actorLayers = ~0;
private readonly HashSet<int> countedActors = new HashSet<int>();
private void OnTriggerEnter2D(Collider2D other)
{
if (!isActiveAndEnabled) return;
if ((actorLayers.value & (1 << other.gameObject.layer)) == 0) return;
Rigidbody2D body = other.attachedRigidbody;
GameObject actor = body != null ? body.gameObject : other.gameObject;
if (countedActors.Add(actor.GetInstanceID()))
Debug.Log("Unique 2D actors: " + countedActors.Count, this);
}
}
What to observe in Unity
Configured 2D trigger contacts count each attached actor body once. Re-entry and extra child colliders do not increase that runtime identity count; disabling the script prevents its own counting policy.
The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
var counted = new HashSet<string>(StringComparer.Ordinal);
foreach (var suppliedActor in new[] { "actor-a", "actor-a", "actor-b", "actor-a" }) counted.Add(suppliedActor);
Console.WriteLine(counted.Count);
Console.WriteLine(counted.Add("actor-b"));
C# practice output
2 False
A 2D trigger requires 2D components and a body; a HashSet does not create contacts.