
AI 사용법을 가볍게 익힐겸 커서를 사용하여 작은 프로젝트를 시작했다.
TL;DR: Cursor를 이용해 Boid/BoidsManager를 생성하고, 설계서 기반으로 코드 검수·수정하여 동작하는 데모를 만들었다.
(Unity 버전: 6000.2.7f2)
군집시스템은 각 오브젝트가 오브젝트 무리에 대하여 분리, 정렬, 응집 로직을 진행한다.
실제 조류 군집의 움직임과 유사하게 구현하는 것이 군집시스템의 목적이다.
초안으로 작성한 시스템 설계서는 아래와 같다.
[Boid 클래스 설계]
- 목적: 3가지의 규칙을 적용하여 군집 이동을 구현 (세 규칙: 분리, 정렬, 응집)
- 변수(멤버):
- BoidsManager
- neighbors: List<Boid>
- perceptionRadius: 3.0f
- maxSpeed: 5.0f
- maxForce: 0.5f
- CurrentDirection: Vector3.zero
- 움직임 규칙:
- 분리: 감지된 이웃들로부터 벗어난다.
- (n번째 이웃 - 나) 방향의 벡터들을 다 합쳐서 벗어날 벡터를 계산한다.
- 정렬: 이웃들의 진행 방향의 평균 방향으로 진행한다.
- 이웃들의 forward를 다 합치고 평균을 내고 정규화한다.
- 응집: 모든 이웃 사이의 중간점을 찾고 중간점을 향해 이동
- 모든 이웃들의 위치를 더하고 평균 좌표를 계산함. (평균 좌표 - 나) 방향을 구한다.
- Update 흐름:
1. 각 규칙마다 값을 계산한다.
- 각 규칙마다 값을 구하는 메서드가 존재하고, 메서드는 계산한 방향 값을 반환한다.
2. 계산된 벡터 값을 합침.
3. 벡터 값을 방향 적용한다.
- SLerp를 사용하여 CurrentDirection에서 계산된 방향으로 자연히 변경되도록 한다.
- Time.deltaTime 사용
- SLerp를 통해 계산된 방향을 CurrentDirection에 대입한다.
4. 구한 방향 값을 사용해서 오브젝트 방향 변경
- transform.rotation = Quaternion.LookRotation(direction);
5. 구한 방향으로 오브젝트 위치 이동
- transform.position += transform.forward * speed * Time.deltaTime;
- Update외 주기적인 흐름
- 감지 범위에 들어오는 Boid는 이웃 리스트에 넣는다.
- 범위를 벗어나면 이웃 리스트에서 뺀다.
- 최적화는 미루고 O(n^2) 형태로 진행
- BoidsManager 클래스 설계
- 목적: Boid컴포넌트가 달린 오브젝트들을 생산, 관리하고 Boid의 각 규칙에 적용되는 가중치를 조절한다.
- 싱글톤 형태를 가진다.
- 싱글톤에 필요한 로직을 갖춤
- 변수(멤버):
- Boids: List<Boid>
- BoidPrefab
- SeperationWeight: 1.0f
- AlignWeight: 1.0f
- CohesionWeight: 1.0f
- CreateBoid 메서드
- 프리팹을 인스턴스화 시키고 List에 추가한다.
- 부가
- 각 가중치는 런타임 중에 BoidsManager 컴포넌트가 달린 오브젝트의 인스펙터에서 조절 가능함. (가시성 관련 세팅 요구함.)
- 각 가중치는 각 Boid에서 접근해서 읽을 수 있음.
- 등록된 Boid 오브젝트는 삭제는 나중에 추가할 것이니 일단 무시하고 진행.
- 분리규칙 적용할 때, 이웃 하나 하나의 거리가 중요할까?
일단은 단순하게 가자.
================================================
[디버그]
- PerceptionRadius를 Gizmo로 시각화. 반투명하게.
[제한]
- 각 Boid가 주변 이웃 찾는 것은 O(n^2)이므로 Boid 생성 제한. 우선 100부터.
설계서를 주고 바로 코드 작성을 요청하면 AI가 처리하는 일이 많아져 원치 않는 결과가 나온다. 그래서 우선 아래처럼 설계서대로 틀만 잡도록 했다.
이 설계서를 기반으로 Boid.cs, BoidsManager.cs 각각 어떤 함수와 필드를 가질지 설계.
함수 이름과 역할만 명시. 코드 본문은 작성하지 마.
클래스와 멤버 변수, 메서드의 틀이 잡힌다. 그 다음 자세한 지시사항을 아래와 같이 적어서 요청했다.
위 구조를 그대로 사용해서 Unity C# 코드 전체를 작성해줘.
- Boid.cs는 Assets/Scripts/AI에 있고, BoidsManager.cs는 Assets/Scripts에 이미 존재해. 그 파일 내용 바꾸면 돼
- Boid.cs, BoidsManager.cs 두 개의 스크립트로 나눠서 작성
- Unity 컴포넌트로 동작하도록 MonoBehaviour 상속
- 변수는 public으로 하지 말고 [SerializeField]를 사용
- 주석은 최소한의 기능 설명만
- Update, Start 등 Unity 함수 이름은 그대로 유지
- 필요 시 Vector3 연산은 Unity의 기본 메서드(Vector3.Normalize 등)를 사용
- 구조는 설계서 내용을 그대로 따르되, 명확히 함수 단위로 분리
- 각 Boid의 이동 규칙(분리, 정렬, 응집)은 개별 메서드로 만들어 반환값은 Vector3로
- BoidsManager는 싱글톤으로 관리
- Inspector에서 가중치를 조정할 수 있게 public property나 [SerializeField] 사용
- 코드를 한 번에 실행 가능한 완성본 코드를 작성해
Assets/Scripts/AI/Boid.cs, Assets/Scripts/AI/BoidsManager.cs// 분리 규칙
private Vector3 ComputeSeparation()
{
if (neighbors.Count == 0) return Vector3.zero;
Vector3 sum = Vector3.zero;
Vector3 myPos = transform.position;
for (int i = 0; i < neighbors.Count; i++)
{
Vector3 away = myPos - neighbors[i].transform.position;
sum += away;
}
if (sum != Vector3.zero)
{
sum = sum.normalized * maxSpeed - currentDirection * maxSpeed;
if (sum.magnitude > maxForce)
{
sum = sum.normalized * maxForce;
}
}
return sum;
}
// 정렬 규칙
private Vector3 ComputeAlignment()
{
if (neighbors.Count == 0) return Vector3.zero;
Vector3 sum = Vector3.zero;
for (int i = 0; i < neighbors.Count; i++)
{
sum += neighbors[i].transform.forward;
}
Vector3 avg = sum / neighbors.Count;
if (avg != Vector3.zero)
{
avg = avg.normalized * maxSpeed - currentDirection * maxSpeed;
if (avg.magnitude > maxForce)
{
avg = avg.normalized * maxForce;
}
}
return avg;
}
// 응집 규칙
private Vector3 ComputeCohesion()
{
if (neighbors.Count == 0) return Vector3.zero;
Vector3 sum = Vector3.zero;
for (int i = 0; i < neighbors.Count; i++)
{
sum += neighbors[i].transform.position;
}
Vector3 center = sum / neighbors.Count;
Vector3 desiredDir = (center - transform.position);
if (desiredDir != Vector3.zero)
{
desiredDir = desiredDir.normalized * maxSpeed - currentDirection * maxSpeed;
if (desiredDir.magnitude > maxForce)
{
desiredDir = desiredDir.normalized * maxForce;
}
}
return desiredDir;
}
// 매 프레임마다 가까운 오브젝트를 이웃으로 인식
// 최적화 필요
public void RefreshNeighbors(IReadOnlyList<Boid> allBoids)
{
neighbors.Clear();
Vector3 myPos = transform.position;
for (int i = 0; i < allBoids.Count; i++)
{
var other = allBoids[i];
if (other == null || other == this) continue;
float dist = Vector3.Distance(myPos, other.transform.position);
if (dist <= perceptionRadius)
{
neighbors.Add(other);
}
}
}
BoidSpawner.cs를 추가했음
설계서 쓰는 시간이 너무 많이 걸렸다.
그냥 하드코딩하고 품질개선을 커서한테 맡기는게 나아보일 정도이다.
기획, 설계 부분을 어떻게 하면 시간단축시킬지 고민해봐야한다.
프롬프트는 구조 정의를 시키고 코드생성 따로 시키면 AI에 부담도 적고 결과물의 퀄리티가 올라간다.
다음 계획:
다양한 움직임 관찰을 위한 Boid 생성 시 방향 랜덤 적용,
최적화를 위한 spatial partition 고안,
장애물 회피 기능,
한 공간에 맴돌도록 하는 기능에 대한 정의와 기획