3D 공룡 달리기 - 원형 배치와 절차적 맵 생성

삼각함수로 오브젝트를 원형 배치하고, 맵 프리팹을 이어 붙여 절차적으로 생성하는 방법과 오일러 회전과 쿼터니언의 차이를 학습합니다

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. ratio lets 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 radius gives the point on the circle.
  • localPosition: Sets the position relative to the parent raptors Transform.

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 so MapManager can chain them correctly.
  • Set mapSize.z in 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 Start and cache the result — repeated searches each frame are expensive.
  • Returns null if 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