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 |