3D Dino Run – Collision Detection and Arithmetic System

Detect door collisions using Physics.OverlapSphere and complete the game system by dynamically adjusting raptor count with arithmetic operations

This tutorial completes the core gameplay of 3D Dino Run. We add sphere-based collision detection (Physics.OverlapSphere) to DinoController to catch the exact moment the dinosaur passes through a door, and implement the four arithmetic methods in DinoPositionController to dynamically adjust the raptor count. We also cover OnDrawGizmos to visualize the collision sphere in the editor.


Game Structure

Script Method Role
DinoController DoorCheck() Detects door collision with a sphere, delegates arithmetic
DinoController OnDrawGizmos() Visualizes the collision sphere in the editor
DinoPositionController SetDoorCalc() Calls the correct arithmetic method based on door type
DinoPositionController PlusRaptor() Adds raptors (addition)
DinoPositionController MinusRaptor() Removes raptors (subtraction)
DinoPositionController TimesRaptor() Multiplies raptors
DinoPositionController DivisionRaptor() Divides raptors

DinoController – Adding DoorCheck

Full Code

This is the completed version of the movement code from Tutorial 49, with DoorCheck and OnDrawGizmos added.

using UnityEngine;

public class DinoController : MonoBehaviour
{
    public float zMoveSpeed;
    public float xMoveSpeed;

    public DinoPositionController dinoPositionController;
    public Vector3 sphereCenter;
    public float sphereRadius = 0.5f;

    void Update()
    {
        transform.position += Vector3.forward * Time.deltaTime * zMoveSpeed;

        if (Input.GetKey(KeyCode.LeftArrow))
            transform.Translate(-xMoveSpeed * Time.deltaTime, 0, 0);
        if (Input.GetKey(KeyCode.RightArrow))
            transform.Translate(xMoveSpeed * Time.deltaTime, 0, 0);

        DoorCheck();
    }

    void DoorCheck()
    {
        Collider[] hitColliders = Physics.OverlapSphere(transform.position + sphereCenter, sphereRadius);
        foreach (Collider doors in hitColliders)
        {
            int doorNum = doors.gameObject.GetComponent<SelectDoors>().GetDoorNumber(transform.position.x);
            DoorType doorType = doors.gameObject.GetComponent<SelectDoors>().GetDoorType(transform.position.x);
            doors.gameObject.GetComponent<BoxCollider>().enabled = false;
            dinoPositionController.SetDoorCalc(doorType, doorNum);
        }
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position + sphereCenter, sphereRadius);
    }
}

Key Concepts – DinoController

1. Physics.OverlapSphere – Sphere-Based Collision Detection

Collider[] hitColliders = Physics.OverlapSphere(transform.position + sphereCenter, sphereRadius);

Physics.OverlapSphere returns an array of every Collider whose bounds overlap the sphere defined by the given center and radius. It is commonly used to detect nearby enemies within a blast radius, or to check whether any objects are within a certain range.

Physics.OverlapSphere(center, radius)
                         ↑        ↑
              transform.position  sphereRadius
              + sphereCenter

Parameter breakdown:

Parameter Type Description
1st (center) Vector3 Center of the sphere. transform.position + sphereCenter applies an offset from the object's origin
2nd (radius) float Radius of the sphere. All Colliders within this range are returned
3rd (layerMask) int (optional) Limit the search to specific layers. Omit to check all layers

Return value: A Collider[] array. Returns an empty array (length 0) if nothing is found.

Why use sphereCenter? If the dinosaur's pivot point is at its feet, you may want to offset the sphere to the torso or slightly in front. Setting sphereCenter to something like (0, 0.5, 1) in the Inspector moves the sphere forward and up from the object's origin.


2. Iterating Over Colliders with foreach

foreach (Collider doors in hitColliders)
{
    // ...
}

Physics.OverlapSphere returns all colliders in range as an array. The foreach loop processes each one in order.

// Equivalent with a for loop
for (int i = 0; i < hitColliders.Length; i++)
{
    Collider doors = hitColliders[i];
    // ...
}

In practice only one door is detected at a time, but using foreach makes the code safe even if multiple colliders are returned simultaneously.


3. GetComponent Chaining – Accessing Components from a Collider

int doorNum = doors.gameObject.GetComponent<SelectDoors>().GetDoorNumber(transform.position.x);
DoorType doorType = doors.gameObject.GetComponent<SelectDoors>().GetDoorType(transform.position.x);
doors.gameObject.GetComponent<BoxCollider>().enabled = false;

This is a common pattern for accessing other components on the same GameObject that owns the Collider.

Collider (doors)
    └── .gameObject                        → the GameObject this Collider belongs to
         └── .GetComponent<SelectDoors>()  → the SelectDoors component on the same object
              └── .GetDoorNumber(...)       → call the method

doors.gameObject retrieves the GameObject the Collider is attached to. Since Collider and SelectDoors are on the same object, this is how we reach SelectDoors.

For better performance, you can cache the result if you call GetComponent on the same object multiple times:

// Optimized: cache GetComponent result in a variable
SelectDoors selectDoors = doors.gameObject.GetComponent<SelectDoors>();
int doorNum = selectDoors.GetDoorNumber(transform.position.x);
DoorType doorType = selectDoors.GetDoorType(transform.position.x);

4. BoxCollider.enabled = false – Preventing Duplicate Collisions

doors.gameObject.GetComponent<BoxCollider>().enabled = false;

The door's BoxCollider is disabled the moment the dinosaur first touches it. Without this, DoorCheck would fire every frame while the dinosaur passes through the door, causing the arithmetic operation to be applied dozens of times in a single pass.

Situation Result
BoxCollider not disabled Collision detected every frame during pass-through → operation applied repeatedly
BoxCollider.enabled = false Disabled on first contact → operation applied exactly once
// Comparing three approaches
doors.gameObject.GetComponent<BoxCollider>().enabled = false;  // Disable only the Collider (object stays visible)
Destroy(doors.gameObject.GetComponent<BoxCollider>());         // Delete the Collider component
Destroy(doors.gameObject);                                     // Delete the entire object

Using enabled = false keeps the door visible while stopping further collision detection — exactly what we want.


5. OnDrawGizmos – Visualizing the Collision Range in the Editor

void OnDrawGizmos()
{
    Gizmos.color = Color.red;
    Gizmos.DrawWireSphere(transform.position + sphereCenter, sphereRadius);
}

OnDrawGizmos is a special Unity method that draws editor-only visual aids (gizmos) in the Scene view. Gizmos are not included in game builds — they are only visible in the Unity editor.

  • Gizmos.color: Sets the color for any gizmos drawn after this line.
  • Gizmos.DrawWireSphere(center, radius): Draws a wireframe sphere at the given position.

Thanks to this code, selecting the dinosaur object in the Scene view shows a red wireframe sphere, letting you visually confirm the collision range and fine-tune sphereCenter and sphereRadius without guesswork.

// Commonly used Gizmos methods
Gizmos.DrawWireSphere(pos, radius);  // Wireframe sphere
Gizmos.DrawSphere(pos, radius);      // Solid sphere
Gizmos.DrawWireCube(pos, size);      // Wireframe box
Gizmos.DrawLine(from, to);          // Line
Gizmos.DrawRay(origin, direction);   // Ray

OnDrawGizmosSelected: Unlike OnDrawGizmos, this only draws when the object is selected in the editor. Useful when the scene has many objects and you only want to see gizmos for the currently selected one.


DinoPositionController – Adding SetDoorCalc

Full Code

This is the completed version of Tutorial 50's SetDinoPosition code, with SetDoorCalc and the four arithmetic methods added.

using UnityEngine;

public class DinoPositionController : MonoBehaviour
{
    public Transform raptors;
    public float radius = 1f;
    public float ratio = 0.1f;

    public GameObject raptorPrefab;

    void Start()
    {
        SetDinoPosition();
    }

    public void SetDoorCalc(DoorType doorType, int doorNumber)
    {
        if (doorType.Equals(DoorType.Plus)) PlusRaptor(doorNumber);
        else if (doorType.Equals(DoorType.Minus)) MinusRaptor(doorNumber);
        else if (doorType.Equals(DoorType.Times)) TimesRaptor(doorNumber);
        else if (doorType.Equals(DoorType.Division)) DivisionRaptor(doorNumber);
        SetDinoPosition();
    }

    private void SetDinoPosition()
    {
        for (int i = 0; i < raptors.childCount; i++)
        {
            if (i > 8)
            {
                raptors.GetChild(i).gameObject.SetActive(false);
                continue;
            }
            if (raptors.childCount < 10)
            {
                float angleStep = 360f / (raptors.childCount * ratio);
                float angle = i * angleStep;
                float angleRad = angle * Mathf.Deg2Rad;
                float x = Mathf.Cos(angleRad) * radius;
                float z = Mathf.Sin(angleRad) * radius;
                raptors.GetChild(i).localPosition = new Vector3(x, 0f, z);
            }
        }
    }

    private void PlusRaptor(int number)
    {
        for (int i = 0; i < number; i++)
            Instantiate(raptorPrefab, raptors);
    }

    private void MinusRaptor(int number)
    {
        if (number > raptors.childCount) number = raptors.childCount;
        int raptorNum = raptors.childCount;
        for (int i = raptorNum - 1; i >= (raptorNum - number); i--)
            Destroy(raptors.GetChild(i).gameObject);
    }

    private void TimesRaptor(int number)
    {
        int currentCount = raptors.childCount;
        int toAdd = currentCount * number - currentCount;
        for (int i = 0; i < toAdd; i++)
            Instantiate(raptorPrefab, raptors);
    }

    private void DivisionRaptor(int number)
    {
        int currentCount = raptors.childCount;
        int toRemain = currentCount / number;
        if (toRemain < 0) toRemain = 0;
        int toRemove = currentCount - toRemain;
        for (int i = currentCount - 1; i >= (currentCount - toRemove); i--)
            Destroy(raptors.GetChild(i).gameObject);
    }
}

Key Concepts – DinoPositionController

6. SetDoorCalc – Branching by Operation Type

public void SetDoorCalc(DoorType doorType, int doorNumber)
{
    if (doorType.Equals(DoorType.Plus)) PlusRaptor(doorNumber);
    else if (doorType.Equals(DoorType.Minus)) MinusRaptor(doorNumber);
    else if (doorType.Equals(DoorType.Times)) TimesRaptor(doorNumber);
    else if (doorType.Equals(DoorType.Division)) DivisionRaptor(doorNumber);
    SetDinoPosition();
}

DinoController passes the doorType and doorNumber to this method, which calls the appropriate arithmetic method. After the raptor count changes, SetDinoPosition() is called again to recalculate the circular arrangement for the new count.


7. PlusRaptor – Adding Raptors with Instantiate

private void PlusRaptor(int number)
{
    for (int i = 0; i < number; i++)
        Instantiate(raptorPrefab, raptors);
}

Instantiate(prefab, parent) creates a copy of the prefab and sets the given Transform as its parent.

// Instantiate overloads
Instantiate(prefab);                                   // At world origin, no parent
Instantiate(prefab, position, rotation);               // Set position and rotation, no parent
Instantiate(prefab, parent);                           // Set parent, placed at parent's local origin
Instantiate(prefab, position, rotation, parent);       // Set position, rotation, and parent

Passing raptors as the parent means each newly created raptor becomes a child of raptors, immediately increasing raptors.childCount. The subsequent SetDinoPosition() call then uses the updated count to recalculate the circular layout.

Example: 3 raptors currently, pass through a +2 door → PlusRaptor(2) → Instantiate twice → 5 raptors.


8. MinusRaptor – Why Delete from the Back

private void MinusRaptor(int number)
{
    if (number > raptors.childCount) number = raptors.childCount;
    int raptorNum = raptors.childCount;
    for (int i = raptorNum - 1; i >= (raptorNum - number); i--)
        Destroy(raptors.GetChild(i).gameObject);
}

Guard clause: If number exceeds raptors.childCount, it is clamped to childCount. For example, if only 2 raptors remain and the player hits a -5 door, the count drops to 0 rather than going negative.

Why delete from the back (decrementing i from raptorNum - 1)?

Child indices: [0] [1] [2] [3] [4]   (5 raptors)

Deleting from the front:
  Delete [0] → children re-index to [0][1][2][3]
  Delete [0] → children re-index to [0][1][2]
  → Index shifts can cause unintended objects to be deleted

Deleting from the back:
  Delete [4] → [0][1][2][3] (front indices unchanged)
  Delete [3] → [0][1][2]    (front indices unchanged)
  → Safely removes exactly the desired number of raptors
// Example: delete 2 of 5 raptors
int raptorNum = 5;
// i = 4 → destroy GetChild(4)
// i = 3 → destroy GetChild(3)
// i = 3 >= (5 - 2) = 3, so loop ends

Destroy executes at the end of the current frame, meaning childCount does not decrease immediately during the loop. Iterating from the back ensures index stability throughout.


9. TimesRaptor – Multiplying the Raptor Count

private void TimesRaptor(int number)
{
    int currentCount = raptors.childCount;
    int toAdd = currentCount * number - currentCount;
    for (int i = 0; i < toAdd; i++)
        Instantiate(raptorPrefab, raptors);
}

Formula: raptors to add = current count × multiplier − current count

3 raptors currently, pass through ×2 door:
Target = 3 × 2 = 6 raptors
To add = 6 − 3 = 3 Instantiate calls
Current Multiplier Target To Add
3 2 6 3
5 3 15 10
4 1 4 0 (no change)

Why save currentCount before the loop? Every call to Instantiate increases raptors.childCount. If the loop condition used raptors.childCount directly, it would keep growing and the loop would never end. Capturing the initial value in a variable fixes the target.


10. DivisionRaptor – Integer Division and Edge Cases

private void DivisionRaptor(int number)
{
    int currentCount = raptors.childCount;
    int toRemain = currentCount / number;
    if (toRemain < 0) toRemain = 0;
    int toRemove = currentCount - toRemain;
    for (int i = currentCount - 1; i >= (currentCount - toRemove); i--)
        Destroy(raptors.GetChild(i).gameObject);
}

Integer division: In C#, dividing two int values truncates any decimal portion.

int result = 5 / 2;  // 2  (not 2.5)
int result = 7 / 3;  // 2  (not 2.33...)
5 raptors currently, pass through ÷2 door:
Remaining = 5 / 2 = 2  (integer division)
To remove = 5 − 2 = 3 raptors
Current Divisor Remaining (int div) To Remove
6 2 3 3
5 2 2 3
7 3 2 5
4 4 1 3

if (toRemain < 0) toRemain = 0: In theory, dividing two positive integers can never produce a negative result, but this guard clause is an example of defensive programming — it ensures the value can never go below zero regardless of future changes.

Deletion is performed from the back for the same index-stability reasons explained in MinusRaptor.


Complete Game Flow

[Game Start]
    ↓
DinoController.Update() – every frame
    ├── Z-axis auto-advance (Vector3.forward)
    ├── Arrow key X-axis movement (Transform.Translate)
    └── DoorCheck() called
          ↓
    Physics.OverlapSphere searches for nearby Colliders
          ↓
    Door Collider detected:
    ├── SelectDoors.GetDoorType(xPos)   → obtain DoorType
    ├── SelectDoors.GetDoorNumber(xPos) → obtain number
    ├── BoxCollider.enabled = false     → prevent duplicates
    └── DinoPositionController.SetDoorCalc(type, num) called
              ↓
        Branch by DoorType:
        ├── Plus     → PlusRaptor(n)     → Instantiate n raptors
        ├── Minus    → MinusRaptor(n)    → Destroy n raptors (from back)
        ├── Times    → TimesRaptor(n)    → Instantiate (current×n − current) raptors
        └── Division → DivisionRaptor(n) → Destroy (current − current/n) raptors
              ↓
        SetDinoPosition() – recalculate circular layout for new count

Unity Scene Setup Summary

Item Recommended Setup
Dinosaur object Attach DinoController; assign DinoPositionController to dinoPositionController
sphereCenter Set the collision sphere offset in the Inspector (e.g. (0, 0.5, 0.5))
sphereRadius Adjust to match door width (default 0.5)
Raptor parent object Attach DinoPositionController; assign the raptor parent Transform to raptors
raptorPrefab Assign the raptor prefab in the Inspector
Door object Attach SelectDoors + BoxCollider (consider setting a dedicated Layer)

Summary

Concept Code Purpose
Sphere collision detection Physics.OverlapSphere(pos, radius) Returns all Colliders within a given radius
Prevent duplicate collisions BoxCollider.enabled = false Blocks repeated arithmetic during a single pass-through
Gizmo visualization OnDrawGizmos + Gizmos.DrawWireSphere View collision range in the editor
Adding raptors Instantiate(prefab, parent) Spawns a prefab copy as a child
Removing raptors Destroy(GetChild(i).gameObject) from back Maintains index stability during deletion
Integer division int / int Truncates decimals for raptor count calculation