군집시스템(using Cursor)

강성원·2025년 10월 15일

TIL 경험하고 배운 것

목록 보기
82/84
post-thumbnail

AI 사용법을 가볍게 익힐겸 커서를 사용하여 작은 프로젝트를 시작했다.

[프로젝트명] Boids_with_AI

TL;DR: Cursor를 이용해 Boid/BoidsManager를 생성하고, 설계서 기반으로 코드 검수·수정하여 동작하는 데모를 만들었다.
(Unity 버전: 6000.2.7f2)

가벼운 배경

군집시스템은 각 오브젝트가 오브젝트 무리에 대하여 분리, 정렬, 응집 로직을 진행한다.
실제 조류 군집의 움직임과 유사하게 구현하는 것이 군집시스템의 목적이다.

목표

  • 간단한 Boids 구현(분리/정렬/응집)
  • AI( Cursor )에게 설계서 제공 → 코드 생성 → 검수

원본 설계서 (프롬프트)

초안으로 작성한 시스템 설계서는 아래와 같다.

[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] 사용 
- 코드를 한 번에 실행 가능한 완성본 코드를 작성해

사용한 도구

  • Unity: 6000.2.7f2
  • Cursor (Agent)

생성된 코드

  • 핵심 파일: Assets/Scripts/AI/Boid.cs, Assets/Scripts/AI/BoidsManager.cs
  • 전체 코드는 GitHub 링크 참조.

핵심 스니펫

// 분리 규칙
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);
        }
    }
}

검수 및 수정 포인트

  • Boid를 범위 내에 랜덤하게 생성만 하는 역할의 BoidSpawner.cs를 추가했음

데모

  • 모두의 방향이 같아서 다양한 모습이 연출되지는 않는다.

벤치마크

  • 나중에!

배운 점 / 다음 계획

  • 설계서 쓰는 시간이 너무 많이 걸렸다.
    그냥 하드코딩하고 품질개선을 커서한테 맡기는게 나아보일 정도이다.
    기획, 설계 부분을 어떻게 하면 시간단축시킬지 고민해봐야한다.

  • 프롬프트는 구조 정의를 시키고 코드생성 따로 시키면 AI에 부담도 적고 결과물의 퀄리티가 올라간다.

  • 다음 계획:
    다양한 움직임 관찰을 위한 Boid 생성 시 방향 랜덤 적용,
    최적화를 위한 spatial partition 고안,
    장애물 회피 기능,
    한 공간에 맴돌도록 하는 기능에 대한 정의와 기획

profile
개발은삼순이발

0개의 댓글