수학 달리기 게임에는 맵에 배치된 적 랩터가 있습니다. 적은 처음에 가만히 서 있다가(Idle) 근처에 플레이어 랩터가 오면 달려가(Run) 잡습니다. 이 동작을 enum 기반 상태 머신으로 구현합니다. 여러 적이 동시에 같은 랩터를 쫓는 것을 Invoke로 방지하고, Vector3.MoveTowards로 부드럽게 이동합니다.
게임 구조
| 스크립트 | 역할 |
|---|---|
Raptor |
플레이어 랩터 - 타겟 지정 여부 관리 |
Enermy |
적 AI - 탐지·추격·포획 상태 머신 |
EnermyRaptors |
적 그룹 생성기 - 피보나치 나선으로 적 배치 |
Raptor - 타겟 관리
using UnityEngine;
public class Raptor : MonoBehaviour
{
private bool isTarget; // 이미 적에게 타겟으로 지정됐는지
public void SetTarget()
{
isTarget = true;
}
public bool IsTarget()
{
return isTarget;
}
}
Raptor는 단순한 데이터 클래스(class)입니다. isTarget 하나로 "이미 다른 적이 쫓고 있는지"를 기록해 중복 타겟팅을 방지합니다.
Enermy - 전체 코드
using System.Collections;
using UnityEngine;
public class Enermy : MonoBehaviour
{
enum State { Idle, Run }
public float moveSpeed;
public float detectRadius;
private State state;
private Transform targetRaptor;
[SerializeField] private bool isTargetOn;
void Start()
{
GetComponent<Animator>().speed = 0f; // 처음엔 정지 애니메이션
}
void Update()
{
SetState();
}
private void SetState()
{
switch (state)
{
case State.Idle: DetectDino(); break;
case State.Run: GoToDino(); break;
}
}
private void DetectDino()
{
if (isTargetOn) return; // 이미 타겟 지정됨
Collider[] hitColliders = Physics.OverlapSphere(transform.position, detectRadius);
foreach (Collider col in hitColliders)
{
Raptor raptor = col.GetComponent<Raptor>();
if (raptor != null && !raptor.IsTarget())
{
Invoke("SetTargetDino", 0.1f); // 0.1초 지연 후 타겟 확정
targetRaptor = raptor.transform;
break;
}
}
}
private void SetTargetDino()
{
if (targetRaptor != null && !targetRaptor.GetComponent<Raptor>().IsTarget())
{
targetRaptor.GetComponent<Raptor>().SetTarget();
isTargetOn = true;
StartGotoDino();
}
}
private void StartGotoDino()
{
state = State.Run;
GetComponent<Animator>().speed = 1f; // 달리기 애니메이션 시작
}
private void GoToDino()
{
if (targetRaptor == null) return;
transform.position = Vector3.MoveTowards(
transform.position,
targetRaptor.position,
moveSpeed * Time.deltaTime
);
if (Vector3.Distance(transform.position, targetRaptor.position) < 0.1f)
{
SoundManager.instance.DinoDieSoundPlay();
Destroy(targetRaptor.gameObject); // 랩터 제거
Destroy(this.gameObject); // 자신도 제거
}
}
}
EnermyRaptors - 적 그룹 생성
using UnityEngine;
public class EnermyRaptors : MonoBehaviour
{
public GameObject enermyRaptorPrefab;
public int enermyRaptorNumber;
public Transform enermyRaptorsParent;
public float initialRadius = 0f;
public float radiusGrowth = 0.12f;
public float angleIncrement = 137.5f;
void Start()
{
CreateEnemyRaptors();
this.gameObject.transform.GetChild(0).gameObject.SetActive(true);
}
private void CreateEnemyRaptors()
{
for (int i = 0; i < enermyRaptorNumber; i++)
{
float currentRadius = initialRadius + (radiusGrowth * i);
float angle = i * angleIncrement;
float x = Mathf.Cos(angle * Mathf.Deg2Rad) * currentRadius;
float z = Mathf.Sin(angle * Mathf.Deg2Rad) * currentRadius;
GameObject enemyRaptor = Instantiate(
enermyRaptorPrefab,
enermyRaptorsParent
);
enemyRaptor.transform.localPosition = new Vector3(x, 0, z);
}
}
}
핵심 개념 설명
1. enum 상태 머신
enum State { Idle, Run }
private State state; // 현재 상태
private void SetState()
{
switch (state)
{
case State.Idle: DetectDino(); break;
case State.Run: GoToDino(); break;
}
}
상태 머신(State Machine) 은 오브젝트가 특정 상태 중 하나에만 있을 수 있고, 조건에 따라 상태가 전환되는 패턴입니다.
[Idle] → (랩터 감지) → [Run] → (포획) → (소멸)
Idle상태:DetectDino()를 매 프레임 호출해 주변 랩터를 탐색합니다.Run상태:GoToDino()를 매 프레임 호출해 타겟을 향해 이동합니다.- 상태 전환은
StartGotoDino()에서state = State.Run으로 변경합니다.
switch문이 if-else보다 상태 머신에 적합한 이유: 상태가 늘어나도 각 케이스를 독립적으로 추가하면 됩니다.
2. Invoke - 지연 호출
Invoke("SetTargetDino", 0.1f); // 0.1초 후 SetTargetDino() 호출
Invoke(메서드명, 지연시간)은 지정한 초 후에 메서드(method)를 한 번 호출합니다.
왜 0.1초 지연이 필요한가?
여러 적이 동시에 같은 랩터를 탐지할 수 있습니다. 만약 타겟 지정이 즉각 처리된다면 이렇게 됩니다:
적 A: 랩터1 감지 → 즉시 SetTarget() → isTarget=true
적 B: 랩터1 감지 (같은 프레임) → IsTarget()=false 읽음 (아직 업데이트 전) → 중복 타겟!
0.1초 지연으로 타겟이 확정되는 시점을 분리합니다:
적 A: 0.1초 예약 → SetTargetDino() → SetTarget()
적 B: DetectDino()에서 isTargetOn=true 확인 → return (중복 방지)
isTargetOn: 이 적이 타겟 예약을 했는지 (DetectDino에서 중복 예약 방지)raptor.IsTarget(): 해당 랩터가 이미 다른 적에게 타겟됐는지 (SetTargetDino에서 확인)
3. Vector3.MoveTowards - 일정 속도로 이동
transform.position = Vector3.MoveTowards(
transform.position, // 현재 위치
targetRaptor.position, // 목표 위치
moveSpeed * Time.deltaTime // 이번 프레임 최대 이동 거리
);
Vector3.MoveTowards는 현재 위치에서 목표 위치 방향으로 최대 maxDistance만큼 이동한 위치를 반환(return)합니다. 목표에 정확히 도달하면 목표 위치를 반환(return)해 오버슈팅이 없습니다.
Vector3.Lerp와 비교:
| 방법 | 속도 | 특징 |
|---|---|---|
MoveTowards |
일정 | 끝까지 같은 속도, 오버슈팅 없음 |
Lerp |
점점 느려짐 | 처음엔 빠르고 가까워질수록 느려짐 |
추격 AI에는 MoveTowards가 더 적합합니다.
4. Vector3.Distance - 거리 측정
if (Vector3.Distance(transform.position, targetRaptor.position) < 0.1f)
{
// 포획 성공
}
Vector3.Distance(a, b)는 두 점 사이의 거리를 반환(return)합니다. 0.1 미만이면 "충분히 가까움"으로 판단해 포획 처리합니다.
// 내부 계산:
// distance = Mathf.Sqrt((b.x-a.x)² + (b.y-a.y)² + (b.z-a.z)²)
자주 호출하는 경우 Mathf.Sqrt가 느릴 수 있어 sqrMagnitude를 쓰기도 합니다:
// 최적화 버전 (제곱끼리 비교)
if ((transform.position - targetRaptor.position).sqrMagnitude < 0.01f)
이 프로젝트처럼 간단한 게임에서는 가독성이 좋은 Vector3.Distance를 사용합니다.
5. Animator.speed - 애니메이션 재생 속도
GetComponent<Animator>().speed = 0f; // 정지 (Start)
GetComponent<Animator>().speed = 1f; // 정상 재생 (Run 상태 진입 시)
Animator.speed를 0으로 설정하면 애니메이션이 현재 프레임에 멈춥니다. 1로 복원하면 정상 재생됩니다. SetActive(false)를 쓰지 않고 애니메이션만 정지시킬 때 유용합니다.
6. null 체크 - targetRaptor 삭제 후 안전성
private void GoToDino()
{
if (targetRaptor == null) return; // 이미 삭제됐으면 즉시 반환
// ...
}
타겟 랩터가 다른 요인(다른 적, 뺄셈 문 등)으로 먼저 Destroy된 경우, targetRaptor는 null이 됩니다. null인 Transform에 접근하면 NullReferenceException이 발생하므로 반드시 null 체크가 필요합니다.
Unity 씬 설정
| 항목 | 설정 |
|---|---|
| 적 랩터 프리팹 | Enermy + Animator + Collider 부착 |
detectRadius |
2~3 정도로 시작 |
moveSpeed |
2~4 정도 (플레이어 속도보다 약간 느리게) |
EnermyRaptors |
맵 세그먼트에 배치, 부모 Transform 할당 |
| 플레이어 랩터 프리팹 | Raptor 컴포넌트 부착 |