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

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

여러 공룡을 원형으로 균등하게 배치하는 DinoPositionController, 맵 프리팹을 이어 붙여 랜덤 코스를 자동 생성하는 MapManager를 학습합니다. 삼각함수(Mathf.Cos, Mathf.Sin)를 이용한 원형 배치, GetComponent로 프리팹 정보를 런타임에 읽는 방법, 그리고 오일러 회전과 쿼터니언의 차이를 다룹니다.


게임 구조

스크립트 역할
DinoPositionController 여러 공룡을 원 위에 균등 배치
Map 맵 세그먼트의 Z 길이를 반환(return)
MapManager 맵 프리팹을 이어 붙여 5개 구간 자동 생성

DinoPositionController - 원형 배치

전체 코드

using UnityEngine;

public class DinoPositionController : MonoBehaviour
{
    public Transform raptors;   // 공룡들이 자식으로 붙어 있는 부모 Transform
    public float radius = 1f;   // 원의 반지름
    public float ratio = 0.1f;  // 각도 간격 조절 비율

    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);
            }
        }
    }
}

핵심 개념 설명

1. 삼각함수(function)를 이용한 원형 배치

원 위의 한 점은 각도 θ(세타)를 이용해 다음과 같이 표현합니다:

x = cos(θ) × 반지름
z = sin(θ) × 반지름
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: 공룡 수로 360도를 나누어 각 공룡 사이의 각도 간격을 구합니다. ratio로 간격을 조절할 수 있습니다.
  • Mathf.Deg2Rad: 도(degree)를 라디안(radian)으로 변환하는 상수(π / 180, 약 0.01745). Unity의 삼각함수(function)는 라디안을 입력받으므로 반드시 변환이 필요합니다.
  • Mathf.Cos / Mathf.Sin: 각도의 코사인·사인 값을 반환(return)합니다. 반지름을 곱하면 원 위의 좌표가 됩니다.
  • localPosition: 부모 오브젝트(raptors) 기준 상대 좌표로 위치를 설정합니다.

예: 공룡이 4마리이고 radius = 2, ratio = 1이면:

i angle x z
0 2 0
1 90° 0 2
2 180° -2 0
3 270° 0 -2

2. GetChild - 자식 오브젝트 접근

raptors.GetChild(i).localPosition = new Vector3(x, 0f, z);
raptors.GetChild(i).gameObject.SetActive(false);
  • transform.GetChild(index): 인덱스로 자식 Transform을 가져옵니다. 0부터 시작하며 childCount - 1까지 접근 가능합니다.
  • SetActive(false): GameObject를 비활성화합니다. 씬에는 존재하지만 보이지 않고 Update도 호출되지 않습니다.
  • 이 코드에서는 9번 인덱스(10번째) 이상의 공룡은 비활성화해 화면에 표시하는 최대 수를 제한합니다.

Map - 맵 세그먼트 정보

전체 코드

using UnityEngine;

public class Map : MonoBehaviour
{
    public Vector3 mapSize;

    public float GetMapSize()
    {
        return mapSize.z;
    }
}
  • 역할: 각 맵 프리팹이 Z축 방향으로 얼마나 긴지(mapSize.z)를 반환(return)합니다.
  • MapManager가 맵을 이어 붙일 때 이 값을 읽어 다음 맵의 시작 위치를 계산합니다.
  • 맵 프리팹마다 Map 컴포넌트를 부착하고 Inspector에서 mapSize.z를 실제 크기에 맞게 설정합니다.

MapManager - 절차적 맵 생성

전체 코드

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;
            }

            // 맵 생성 (GeneratedMaps의 자식으로)
            GameObject nowMap = Instantiate(selectedMap, mapPosition, Quaternion.identity, generatedMapsParent);

            // 다음 맵 시작 위치를 위해 나머지 절반 전진
            mapPosition.z += nowMap.GetComponent<Map>().GetMapSize() / 2f;
        }
    }
}

핵심 개념 설명

1. 맵 이어 붙이기 알고리즘

각 맵 프리팹의 원점(pivot)이 중앙에 있다고 가정합니다:

[맵A 중앙] ← mapA.z/2 → [맵A 끝 = 맵B 시작] ← mapB.z/2 → [맵B 중앙]
// 이전 맵의 절반: 이전 맵의 끝 위치로 이동
mapPosition.z += selectedMap.GetComponent<Map>().GetMapSize() / 2f;

// 현재 맵 생성
GameObject nowMap = Instantiate(selectedMap, mapPosition, Quaternion.identity, generatedMapsParent);

// 현재 맵의 절반: 다음 맵의 시작 위치로 이동
mapPosition.z += nowMap.GetComponent<Map>().GetMapSize() / 2f;
  • 프리팹의 pivot이 중앙이므로, 이전 맵 중앙에서 "이전 맵의 절반 + 현재 맵의 절반" 만큼 이동하면 현재 맵의 중앙이 됩니다.
  • 첫 번째 맵(i == 0)은 이전 맵이 없으므로 이 단계를 건너뜁니다.

2. GetComponent - 런타임 컴포넌트 접근

selectedMap.GetComponent<Map>().GetMapSize()
  • GetComponent<T>(): 해당 GameObject에 붙어 있는 컴포넌트 T를 가져옵니다.
  • 프리팹에서 GetComponent를 호출하면 씬에 생성되기 전의 원본 데이터를 읽을 수 있습니다.
  • nowMap.GetComponent<Map>()은 생성된 인스턴스(instance)에서 읽습니다. 프리팹과 인스턴스(instance) 크기가 동일하므로 동일한 결과를 반환(return)합니다.

3. Instantiate - 부모 지정

GameObject nowMap = Instantiate(selectedMap, mapPosition, Quaternion.identity, generatedMapsParent);
  • 4번째 매개변수(parent): 생성된 오브젝트를 지정한 Transform의 자식으로 설정합니다. 씬 Hierarchy를 깔끔하게 정리할 때 유용합니다.
  • Quaternion.identity: 회전 없이 기본 방향으로 생성합니다.

4. GameObject.Find - 이름으로 오브젝트 검색

Transform generatedMapsParent = GameObject.Find("GeneratedMaps").transform;
  • GameObject.Find("이름"): 씬 전체에서 해당 이름의 오브젝트를 검색합니다.
  • 검색 비용이 있어 StartAwake에서 한 번만 호출하고 캐싱해 사용하는 것이 좋습니다.
  • 오브젝트가 없으면 null을 반환(return)하므로, 씬에 "GeneratedMaps" 이름의 빈 GameObject가 반드시 있어야 합니다.

오일러 회전과 쿼터니언

MapManagerQuaternion.identity를 이해하려면 두 회전 표현 방식의 차이를 알아야 합니다.

오일러 회전 (Euler Angles)

오브젝트의 회전을 X, Y, Z 축의 각도(도)로 표현합니다.

// 오일러 회전으로 오브젝트 회전
transform.eulerAngles = new Vector3(30f, 45f, 0f);  // X: 30도, Y: 45도
transform.Rotate(0f, 90f, 0f);  // Y축으로 90도 회전
  • 장점: 직관적이고 이해하기 쉽습니다. Inspector에서 숫자로 확인 가능합니다.
  • 단점 - 짐벌락(Gimbal Lock): 특정 각도에서 두 축이 겹쳐 자유도가 하나 사라지는 현상입니다.

짐벌락 예시:

Y축을 90도로 회전하면 → X축과 Z축이 일치
이후 X축과 Z축 회전이 동일하게 동작 → 특정 방향 회전 불가
  • 단점 - 회전 순서 의존: Unity는 ZXY 순서로 오일러 회전을 적용하므로, 순서에 따라 결과가 달라집니다.

쿼터니언 (Quaternion)

4개의 값(w, x, y, z)으로 회전을 표현하는 수학적 구조입니다.

// 쿼터니언 사용
Quaternion.identity            // 회전 없음 (0도)
Quaternion.Euler(30f, 45f, 0f) // 오일러 각도를 쿼터니언으로 변환
Quaternion.LookRotation(dir)   // 특정 방향을 바라보는 회전
Quaternion.Slerp(a, b, t)      // 두 회전 사이를 부드럽게 보간
비교 오일러 회전 쿼터니언
표현 방식 (X도, Y도, Z도) (w, x, y, z)
직관성 높음 낮음
짐벌락 발생 가능 없음
보간 부자연스러울 수 있음 Slerp으로 자연스럽게
Unity 내부 처리 쿼터니언으로 변환 후 처리 직접 처리

실무 권장: 회전을 직접 설정할 때는 Quaternion.Euler()로 오일러 값을 입력하고, 보간/부드러운 회전에는 Quaternion.Slerp()를 사용합니다.

// 오브젝트를 부드럽게 회전시키기
transform.rotation = Quaternion.Slerp(
    transform.rotation,           // 현재 회전
    Quaternion.Euler(0f, 90f, 0f), // 목표 회전
    Time.deltaTime * rotateSpeed   // 보간 비율
);

Unity 씬 설정 요약

항목 권장 설정
공룡 부모 오브젝트 빈 GameObject에 DinoPositionController 부착, raptors에 공룡 부모 할당
맵 프리팹 각 프리팹에 Map.cs 부착 후 mapSize.z를 실제 Z 길이로 설정
MapManager 빈 GameObject에 부착, mapPrefabs 배열(array)에 맵 프리팹 등록
GeneratedMaps 빈 GameObject, 이름을 "GeneratedMaps"으로 설정 (생성된 맵의 부모)