This tutorial covers two systems from the 3D dino runner: DinoPositionController (arrange multiple dinos evenly around a circle using trigonometry) and MapManager (chain map prefabs end-to-end to build a random course procedurally). Along the way you will understand Mathf.Cos/Sin, Mathf.Deg2Rad, GetComponent on prefabs at runtime, and the difference between Euler angles and Quaternions.
Game Structure
| Script | Role |
|---|---|
DinoPositionController |
Place dinos evenly on a circle |
Map |
Returns the Z length of one map segment |
MapManager |
Chains 5 random map prefabs end-to-end at startup |
DinoPositionController – Circular Arrangement
Full Code
using UnityEngine;
public class DinoPositionController : MonoBehaviour
{
public Transform raptors; // Parent Transform holding all dinos as children
public float radius = 1f; // Circle radius
public float ratio = 0.1f; // Angle gap multiplier
void Start()
{
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);
}
}
}
}
Key Concepts
1. Trigonometry for circular placement
A point on a circle of radius r at angle θ:
x = cos(θ) × r
z = sin(θ) × r
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);
- angleStep: Divides 360° by the number of dinos so they are spaced evenly.
ratiolets you tighten or widen the spacing. - Mathf.Deg2Rad: The constant
π / 180(≈ 0.01745). Multiply degrees by this to get radians. Unity's trig functions (Cos,Sin) require radians. - Mathf.Cos / Mathf.Sin: Return the cosine/sine of a radian angle. Multiplying by
radiusgives the point on the circle. - localPosition: Sets the position relative to the parent
raptorsTransform.
Example — 4 dinos, radius = 2, ratio = 1:
| i | angle | x | z |
|---|---|---|---|
| 0 | 0° | 2 | 0 |
| 1 | 90° | 0 | 2 |
| 2 | 180° | −2 | 0 |
| 3 | 270° | 0 | −2 |
2. GetChild – access children by index
raptors.GetChild(i).localPosition = new Vector3(x, 0f, z);
raptors.GetChild(i).gameObject.SetActive(false);
- transform.GetChild(index): Returns the child Transform at the given index (0-based, up to
childCount - 1). - SetActive(false): Deactivates the GameObject — invisible, no Update, but still in the hierarchy.
- Dinos beyond index 8 (9th and above) are deactivated to cap the displayed count at 9.
Map – Segment Length
Full Code
using UnityEngine;
public class Map : MonoBehaviour
{
public Vector3 mapSize;
public float GetMapSize()
{
return mapSize.z;
}
}
- Attached to every map prefab.
GetMapSize()returns the segment's Z length soMapManagercan chain them correctly. - Set
mapSize.zin the Inspector to match the actual Z size of each map prefab.
MapManager – Procedural Map Generation
Full Code
using UnityEngine;
public class MapManager : MonoBehaviour
{
public GameObject[] mapPrefabs;
void Start()
{
Vector3 mapPosition = Vector3.zero;
Transform generatedMapsParent = GameObject.Find("GeneratedMaps").transform;
for (int i = 0; i < 5; i++)
{
GameObject selectedMap = mapPrefabs[Random.Range(0, mapPrefabs.Length)];
mapPosition.y = selectedMap.transform.position.y;
if (i > 0)
{
mapPosition.z += selectedMap.GetComponent<Map>().GetMapSize() / 2f;
}
GameObject nowMap = Instantiate(selectedMap, mapPosition, Quaternion.identity, generatedMapsParent);
mapPosition.z += nowMap.GetComponent<Map>().GetMapSize() / 2f;
}
}
}
Key Concepts
1. Chaining map segments end-to-end
Each prefab's pivot is at its center, so placing the next map requires advancing by half the current segment plus half the next segment:
[Map A center] ←A/2→ [A end = B start] ←B/2→ [Map B center]
// Advance to center of the current map (skip on first iteration — no previous map)
mapPosition.z += selectedMap.GetComponent<Map>().GetMapSize() / 2f;
// Place the map
GameObject nowMap = Instantiate(selectedMap, mapPosition, Quaternion.identity, generatedMapsParent);
// Advance to where the next map's center will be
mapPosition.z += nowMap.GetComponent<Map>().GetMapSize() / 2f;
2. GetComponent on a prefab at runtime
selectedMap.GetComponent<Map>().GetMapSize()
- Calling
GetComponent<T>()on a prefab reference (not yet instantiated) reads data from the original asset. - This is safe in
Start— just be careful not to call methods that depend on scene state on un-instantiated prefabs.
3. Instantiate with a parent
GameObject nowMap = Instantiate(selectedMap, mapPosition, Quaternion.identity, generatedMapsParent);
- The fourth parameter sets the parent Transform for the new object immediately on creation, keeping the Hierarchy tidy.
4. GameObject.Find
Transform generatedMapsParent = GameObject.Find("GeneratedMaps").transform;
- GameObject.Find("name") searches the entire scene by name. Call it once in
Startand cache the result — repeated searches each frame are expensive. - Returns
nullif the object does not exist, so make sure a GameObject named"GeneratedMaps"is present in the scene.
Euler Angles vs Quaternions
Quaternion.identity appears in every Instantiate call above. Here is why Quaternions are used instead of Euler angles.
Euler Angles
Rotation expressed as three axis angles (degrees):
transform.eulerAngles = new Vector3(30f, 45f, 0f);
transform.Rotate(0f, 90f, 0f);
- Pros: Intuitive, easy to read in Inspector.
- Con – Gimbal Lock: When one axis reaches 90°, two axes align and you lose one degree of rotational freedom. For example, rotating Y to 90° makes X and Z identical, preventing rotation in one direction.
- Con – Order-dependent: Unity applies Euler rotations in ZXY order; the result changes depending on order.
Quaternions
Four-component mathematical structure (w, x, y, z) that represents rotation without gimbal lock:
Quaternion.identity // No rotation
Quaternion.Euler(30f, 45f, 0f) // Convert Euler to Quaternion
Quaternion.LookRotation(dir) // Rotate to face a direction
Quaternion.Slerp(a, b, t) // Smooth interpolation between two rotations
| Euler Angles | Quaternion | |
|---|---|---|
| Representation | (X°, Y°, Z°) | (w, x, y, z) |
| Readability | High | Low |
| Gimbal lock | Possible | None |
| Interpolation | Can snap | Smooth (Slerp) |
| Unity internal | Converted to Quaternion | Used directly |
Best practice: Enter rotation values as Euler via Quaternion.Euler(), and use Quaternion.Slerp() for smooth animated rotation.
// Smoothly rotate toward a target rotation
transform.rotation = Quaternion.Slerp(
transform.rotation,
Quaternion.Euler(0f, 90f, 0f),
Time.deltaTime * rotateSpeed
);
Scene Setup Summary
| Item | Recommendation |
|---|---|
| Dino parent | Empty GameObject with DinoPositionController; assign dino parent Transform |
| Map prefabs | Attach Map.cs to each prefab; set mapSize.z to match actual Z length |
| MapManager | Empty GameObject with MapManager; fill mapPrefabs array in Inspector |
| GeneratedMaps | Empty GameObject named exactly "GeneratedMaps" — parent of all spawned maps |