여러 공룡을 원형으로 균등하게 배치하는 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 | 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("이름"): 씬 전체에서 해당 이름의 오브젝트를 검색합니다.
- 검색 비용이 있어
Start나Awake에서 한 번만 호출하고 캐싱해 사용하는 것이 좋습니다. - 오브젝트가 없으면
null을 반환(return)하므로, 씬에"GeneratedMaps"이름의 빈 GameObject가 반드시 있어야 합니다.
오일러 회전과 쿼터니언
MapManager의 Quaternion.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"으로 설정 (생성된 맵의 부모) |