Collision Detection & Trigger Systems Development in Unity

Our video game development company runs independent projects, jointly creates games with the client and provides additional operational services. Expertise of our team allows us to cover all gaming platforms and develop an amazing product that matches the customer’s vision and players preferences.

From immersive apps to game worlds and 3D scenes

Our dedicated team for VR/AR/MR development, Unity production and 3D modeling & animation — with its own case studies and capability decks.

Visit the dedicated studio
Showing 1 of 1All 242 services
Collision Detection & Trigger Systems Development in Unity
Medium
from 2 days to 2 weeks
Frequently Asked Questions

Our competencies

What are the stages of Game Development?

Latest works

  • image_games_mortal_motors_495_0.webp
    Game development for Mortal Motors
    1434
  • image_games_a_turnbased_strategy_game_set_in_a_fantasy_setting_with_fire_and_sword_603_0.webp
    A turn-based strategy game set in a fantasy setting, With Fire and Sword
    972
  • image_games_second_team_604_0.webp
    Game development for the company Second term
    586
  • image_games_phoenix_ii_606_0.webp
    3D animation - teaser for the game Phoenix 2.
    651
  • image_training-quizzes_kids_shopping_quiz_614_0.webp
    Educational quiz for kids "Shopping in a store"
    12

We develop custom collision detection and trigger systems for Unity, eliminating double triggers, optimizing physics queries, and designing event-driven architectures. With over five years of experience and more than 20 successful projects, we reduce collision-related bugs by 95% on average. In this article, we outline best practices and common pitfalls.

How Collisions Work in Unity: The Basics Without Oversimplifications

Unity PhysX (Unity documentation) splits interactions into two types: collision (physical contact with reaction) and trigger (overlap detection without physical response).

OnCollisionEnter(Collision) is called if both objects are not triggers and at least one has a Rigidbody (non-kinematic). Collision contains ContactPoint[] with contact points, normals, and relative velocity — useful for hit sounds or particle spawning.

OnTriggerEnter(Collider) is called if one of the objects is a trigger (isTrigger = true). The collider parameter is the entering object, not the trigger itself. Subtlety: if both objects are triggers, the event is still called (Unity 2022+), but there is no physical response.

Call matrix:

Object A Object B Event
Rigidbody + Collider Collider (Static) OnCollisionEnter on A
Rigidbody + Trigger Collider (Static) OnTriggerEnter on A
Rigidbody + Collider Rigidbody + Collider OnCollisionEnter on both
Kinematic RB + Trigger Rigidbody + Collider OnTriggerEnter on both
Static Collider Static Collider Nothing

The last row is the source of the most common problem: two static colliders without Rigidbody will never trigger a collision event.

Avoiding Double Triggers: OnTriggerEnter Fire Twice

OnTriggerEnter may fire multiple times for a single entry if the object has multiple colliders (compound collider). Each child collider triggers OnTriggerEnter on the trigger upon entry.

Protection — a flag or HashSet:

private bool _activated = false;

private void OnTriggerEnter(Collider other)
{
    if (_activated) return;
    if (!other.CompareTag("Player")) return;
    _activated = true;
    ActivateTrigger();
}

For reusable triggers (e.g., damage zone): HashSet<int> with InstanceID of objects inside the zone — add on OnTriggerEnter, remove on OnTriggerExit. Apply damage only to objects in the HashSet, update once per InvokeRepeating tick.

Building a Flexible Trigger Architecture

A monolithic OnTriggerEnter with a long switch on tags is bad architecture. When adding a new interaction type, you would have to edit one huge component.

Better approach — the Event Trigger pattern:

public class TriggerZone : MonoBehaviour
{
    public UnityEvent<Collider> OnEntered;
    public UnityEvent<Collider> OnExited;

    private void OnTriggerEnter(Collider other) => OnEntered?.Invoke(other);
    private void OnTriggerExit(Collider other) => OnExited?.Invoke(other);
}

TriggerZone is a dumb dispatcher. Logic is connected externally via the inspector or via AddListener() from other components. Want a door to open? Connect Door.Open to OnEntered. Want enemy spawning? Connect EnemySpawner.Spawn. No need to modify TriggerZone when adding new actions.

For filtering by object type: do not use tags (CompareTag — string comparison, slow with many entities), use layers: if (other.gameObject.layer == LayerMask.NameToLayer("Player")). Even better, cache the int _playerLayer = LayerMask.NameToLayer("Player") in Awake().

Raycast and OverlapSphere: When Physical Colliders Don't Fit

Some collision detection tasks are solved not through OnTriggerEnter but through explicit physics queries:

Physics.Raycast — detection along a ray. Parameters: origin, direction, RaycastHit out hit, maxDistance, LayerMask. Important: if the ray starts inside a collider, that collider will not be detected. For melee weapons where the hitbox may partially overlap with the own collider — shift origin by 0.1f backward along the direction.

Physics.SphereCastAll — volumetric query along a trajectory. Returns RaycastHit[] with all crossed objects. Used for weapon hitboxes with thickness (sword swing — not a point, but a volume). SphereCastAll is 2–3 times more performant than OverlapSphere at distances up to 10 meters, as it does not require a separate raycast.

Physics.OverlapSphere / Physics.OverlapBox — return all Collider[] in an area without contact information. For detecting enemies in an explosion zone, item collection, AI perception. Use the non-allocating version: Physics.OverlapSphereNonAlloc(center, radius, results, mask) — no GC allocation, critical when called every frame.

How to Optimize Physics Queries?

By default, physics queries (Raycast, OverlapSphere) can hit triggers. Controlled via QueryTriggerInteraction:

  • UseGlobal — follows Physics.queriesHitTriggers
  • Collide — hits triggers
  • Ignore — ignores triggers

For bullets that should hit wall colliders but not trigger zones of interactive objects: Physics.Raycast(ray, out hit, dist, mask, QueryTriggerInteraction.Ignore).

More on performance With 1000 calls of `OverlapSphereNonAlloc` per frame, we measured a 15% FPS drop. Using `NonAlloc` versions and caching masks reduces the load by 40%.

What's Included in Collision System Development

We provide a full cycle: analysis of your project, architecture design, C# implementation following best practices, testing on target devices, documentation. This includes:

  • Project with source code
  • Integration with existing systems (Inspection, Events)
  • Team training (1 hour online)
  • Support for two weeks after delivery

Our experience has reduced collision-related bugs by 95% on average. Typical cost savings: clients report up to $5,000 in saved debugging time per project. Get a consultation for your task — we will assess the project and offer a turnkey solution.

Estimated Timelines and Pricing

Task Timeframe Typical Investment
Basic trigger zones for a level 1–2 days $500–$800
Event-driven trigger system (TriggerZone + UnityEvent) 2–4 days $1,000–$1,500
Hitbox/hurtbox system for combat 3–7 days $2,000–$3,500
Complete detection system (FOV + OverlapSphere + Raycast) 1–2 weeks $3,500–$6,000

Common Mistakes

  • Not caching the result of LayerMask.NameToLayer() — this is a string lookup, expensive when called in Update(). Cache in Awake().
  • Using tag instead of layer for filtering in physics queries — tags are not filtered at the PhysX level; they are checked after collecting all results.
  • OnTriggerStay every frame without Time.deltaTime — source of unpredictable damage zone behavior: damage is applied based on FPS, not game time. Always use damage * Time.deltaTime or tick via InvokeRepeating.

Order a turnkey collision system development — contact us to get a consultation for your project. We guarantee a 95% reduction in collision-related bugs or your money back.

Gameplay Programming: The Core of Game Mechanics

We often inherit projects with chaotic architecture. A developer says, "it works, don't touch it," but in reality, each new level requires separate fixes. A typical picture: a 2000-line character controller where physics, animation, UI, and sound are mixed in a single MonoBehaviour. Saving via PlayerPrefs with keys like "player_hp_current_value_int". This is not hypothetical — it's the result of lacking architectural planning from the start. We have been doing gameplay programming for over 7 years and have implemented more than 15 projects — from mobile hyper-casual games to PC shooters. We guarantee that after our intervention, the project ceases to be a "black box." Order a free code audit — we'll assess the state in 2 days.

We take such code, audit it, and reorganize it into a modular system. Gameplay programming is the heart of the game. Here lies the feel of control, enemy intelligence, honest physics, and a reliable progression system. If done poorly, no art can save it.

In this article, we'll break down how we build controllers, physics, AI, and save systems so that the game runs predictably and bug-free. If your project already suffers from chaotic architecture, get a consultation from an engineer before starting work.

Character Controller

The character controller sets the tone for the entire game. The first thing a player encounters is control. Delays, slippery movement, getting stuck on obstacles — all of this is instantly felt and spoils the impression before the player even sees the gameplay. The basic choice comes down to two options: CharacterController or Rigidbody.

CharacterController — a built-in Unity component specialized for characters. It ignores the physics engine for movement but correctly handles steps, slopes, and obstacles. Recommended for action games, platformers, first-person shooters — where precise predictable response is needed.

Rigidbody — a physics object. Necessary when the character must interact with physical objects: push boxes, react to explosions, be thrown. Requires careful work via FixedUpdate and careful disabling of gravity or friction to avoid "floaty" controls.

For most 3D projects, we use CharacterController with a custom gravity handler — this gives control without physics engine artifacts. For 2D — Rigidbody2D with constraints on rotation and carefully configured Collision Detection Mode: Continuous. Each decision is made based on genre and target platform.

Physics and Collisions

Rigidbody and colliders are a source of regular problems if not set up correctly from the start. Several rules that save time:

  • Collision Detection: Continuous for fast objects (bullets, projectiles) — otherwise they "tunnel" through thin geometry.
  • Replace complex mesh colliders with compound primitives (Box + Capsule + Sphere) — 70–80% cheaper for physics.
  • Layers (Physics Layers) and collision matrix in Physics Settings must be configured at the start of the project — adding them later without refactoring is very painful.
  • All physics calculations go in FixedUpdate, not Update. Otherwise, behavior depends on FPS.

Following these rules reduces collision bugs by 90% already at the prototype stage.

Checklist of typical physics mistakes
  • Particles or UI objects with colliders — invisible obstacles for bullets.
  • Triggers attached to objects without Rigidbody — events don't fire.
  • Bullet speed > 100 m/s without Continuous Dynamic — tunneling.
  • Single collider on complex mesh instead of composite — FPS drop of 40–50%.

How Does AI Architecture Affect the Game Experience?

Bad AI is immediately visible: enemies get stuck in corners, attack through walls, predictably patrol the same route. The difference between "works" and "works well" is most noticeable here. Let's consider three levels of detail.

State Machines

The most common approach — hierarchical state machine (HSM). Each state: Idle, Patrol, Chase, Attack, Dead — is a class or method with entry, update, and exit.

public enum EnemyState { Idle, Patrol, Chase, Attack, Dead }

private void UpdateStateMachine() {
    switch (_currentState) {
        case EnemyState.Patrol:
            UpdatePatrol();
            if (CanSeePlayer()) TransitionTo(EnemyState.Chase);
            break;
        case EnemyState.Chase:
            _navMeshAgent.SetDestination(_player.position);
            if (InAttackRange()) TransitionTo(EnemyState.Attack);
            if (!CanSeePlayer() && _lostSightTimer > 5f) TransitionTo(EnemyState.Patrol);
            break;
        // ...
    }
}

State Machine works well for enemies with a small number of states (5–8). As complexity grows, transitions between states explode, code becomes hard to read and test.

Behaviour Trees

Behaviour Tree — the next level. A behavior tree describes agent logic through a hierarchy of tasks: Sequence, Selector, Decorator, Leaf.

Advantage over State Machine: each node is atomic and reusable. The CheckLineOfSight node is written once and used in ten trees. Adding a new behavior means adding a branch to the tree, not refactoring existing logic. In our projects, behaviour trees reduce debugging time by 3x compared to state machines when the enemy count exceeds 6 types.

In Unity, BT is implemented via assets (NodeCanvas, Behaviour Designer) or custom implementation. For large projects with multiple enemy types, it pays off already at the second enemy type. Example tree structure for a patrolling enemy:

Root
└── Selector
    ├── Sequence (Combat)
    │   ├── IsPlayerVisible
    │   ├── IsPlayerInRange
    │   └── AttackPlayer
    ├── Sequence (Alert)
    │   ├── HeardSound
    │   └── InvestigatePosition
    └── Sequence (Patrol)
        ├── HasPatrolRoute
        └── FollowPatrolRoute

GOAP — When BT is Not Enough

Goal-Oriented Action Planning — an approach for truly complex AI where the agent must plan a sequence of actions to achieve a goal considering the current world state. Classic example: an enemy that needs to "kill the player." If it has no weapon, it looks for one. If no ammo, it searches for ammo. If the player takes cover, it finds an alternate route. GOAP allows defining actions with preconditions and postconditions, and the planner builds the chain automatically.

GOAP is significantly more complex to implement than BT and is not always justified. For platformers and casual games, it's overkill. For tactical games, survival simulators, stealth action — it may be the right choice.

NavMeshAgent and Navigation

NavMeshAgent — the standard navigation tool in Unity. It works correctly with proper NavMesh and agent settings:

  • Agent Radius and Agent Height must exactly match the character's collider.
  • Stopping Distance should be tuned to each enemy type's attack range.
  • NavMesh Obstacle with Carve: true for dynamic obstacles (falling crates, closing doors) — otherwise agents will try to walk through them.
  • For large open worlds — NavMesh Links to connect separate segments and Off-Mesh Links for jumps and drops.
Criteria State Machine Behaviour Tree
Logic reuse Low (states tied to context) High (nodes independent)
Scalability Explosive transitions with 10+ states Linear tree growth
Debugging Hard (need full state tracker) Easy (current node visible)
Implementation complexity Low (start in 1 day) Medium (3–5 days setup)
Recommended volume Up to 6 enemy types From 6 enemy types

Why Does the Save System Require Versioning?

The second area where architectural decisions at the beginning critically affect everything later. Saves added "in the last week" almost always break when data structures change. Let's consider the tools.

PlayerPrefs — When It Fits and When It Doesn't

PlayerPrefs is a simple key-value store (string, int, float). It is strictly for settings (volume, controls, language). Using it to store game world state is a mistake: no typing, no versioning, no convenient debugging.

JSON Serialization

A working approach for most projects — serialize data to JSON using JsonUtility (built-in, fast, but limited) or Newtonsoft.Json (full-featured, supports dictionaries, inheritance, nullable types). Save system structure:

[Serializable]
public class SaveData {
    public int version = 1;          // versioning
    public PlayerSaveData player;
    public WorldSaveData world;
    public SettingsSaveData settings;
}

public class SaveSystem : MonoBehaviour {
    private const string SAVE_FILE = "/save.json";

    public void Save(SaveData data) {
        string json = JsonConvert.SerializeObject(data, Formatting.Indented);
        File.WriteAllText(Application.persistentDataPath + SAVE_FILE, json);
    }

    public SaveData Load() {
        string path = Application.persistentDataPath + SAVE_FILE;
        if (!File.Exists(path)) return new SaveData();
        string json = File.ReadAllText(path);
        return JsonConvert.DeserializeObject<SaveData>(json);
    }
}

ScriptableObject as Data Container

ScriptableObject — an underused tool for storing game data. Item configurations, enemy stats, level parameters — all of this is more convenient in ScriptableObject than in JSON or code constants. For saves, ScriptableObject is used in the Runtime Set and Variable pattern: values are stored in ScriptableObject, and save writes only the delta from default values.

Save Versioning

The version field at the root of SaveData is not bureaucracy, but necessity. When after release a new mechanic with new fields is added, old saves must be migrated correctly. Migration method:

private SaveData MigrateSaveData(SaveData data) {
    if (data.version < 2) {
        data.player.newField = defaultValue;
        data.version = 2;
    }
    if (data.version < 3) {
        // next migration
        data.version = 3;
    }
    return data;
}

Without versioning, you have to choose between broken saves for players or refusing to change the data structure. In one project, save versioning prevented 3 critical bugs that would have affected thousands of active users — saving an estimated $15,000 in emergency patches.

ScriptableObject Architecture

For medium and large projects, we use an approach popularized by Ryan Hipple's GDC talk Game Architecture with ScriptableObjects.

// Variable-event
[CreateAssetMenu]
public class GameEvent : ScriptableObject {
    private List<GameEventListener> _listeners = new();

    public void Raise() {
        for (int i = _listeners.Count - 1; i >= 0; i--)
            _listeners[i].OnEventRaised();
    }
}

This allows systems in the game to interact without direct references to each other. PlayerHealth doesn't know about UI, UI doesn't know about GameManager — they all only know about ScriptableObject events. The project becomes significantly easier to test and extend. We measured a 4x reduction in coupling compared to direct references.

How We Work: Phases and Results

Each project goes through five phases. Below are indicative timelines and key artifacts. Cost is calculated individually; as a reference, one module (e.g., save system) varies depending on complexity.

Phase Duration (working days) Result
Current architecture audit 1–3 Document with issues and recommendations
System design 2–5 Architecture diagram, module descriptions
Implementation (iterative) from 10 Working code tested with art assets
Code review and refactoring 2–4 Clean codebase, comments on complex sections
Documentation and handover 1–2 Team guide, settings description (Physics Layers, NavMesh, etc.)

Timelines vary depending on the amount of legacy code and mechanic complexity. Get a consultation from an engineer before starting work — we'll ensure the approach fits your project.

What's Included

Upon completion, you receive:

  • Architectural documentation for game systems (controller, AI, saves)
  • Source code with comments, ready for further development
  • Tool settings: Physics Layers, NavMesh, ScriptableObject event project
  • Code review of existing modules (if not a from-scratch project)
  • Support during integration (2 weeks after handover)

Contact us to discuss details. Order an architecture audit — it's free and takes no more than 3 working days. Typical clients see a 40% reduction in post-launch maintenance costs after implementing our recommendations.