Unity PAT 프로젝트 #6 포위형 AI (Formation/Flocking)

SourStar15·2025년 11월 18일

PAT-Project

목록 보기
6/10
post-thumbnail

AI의 포위형 추격

이 시스템은 단순히 AI가 플레이어의 위치를 추격하는 것이 아닌 이전에 만들어 두었던 Blackboard를 이용해 AI끼리 정보를 공유하며 포위를 하며 추격하는 시스템입니다.

시스템 구조

  1. AIBlackboard (공유 메모리 시스템)
  2. AI_Script (개별 AI 행동)

1단계 : AI 등록 및 각도 할당

// AIBlackboard.cs
private Dictionary<Transform, float> assingedAngles = new Dictionary<Transform, float>();

public void RegisterAI(Transform ai)
{
    if (!aiAgents.Contains(ai))
    {
        aiAgents.Add(ai);
        ReassignFormationAngles(); 
    }
}
private void ReassignFormationAngles()
{
    assingedAngles.Clear();
    if (aiAgents.Count == 0) return;

    // AI 개수에 따라 균등하게 각도 분배
    float angleStep = 360f / aiAgents.Count;

    for (int i = 0; i < aiAgents.Count; i++)
    {
        float angle = (angleStep * i) + angleOffset;
        assingedAngles[aiAgents[i]] = angle;
    }
}

가장 중요한 AI의 수에 따른 각도 할당입니다.
왜 AI의 수에 따라 각도를 할당 해야 할까요? 제가 구현한 포위형 추격은 플레이어의 기준으로 원형을 만들어 원형을 기준으로 포위를 하는 식으로 구현해 뒀습니다.

예를 들어
AI 3개 : 360도 / 3 = 120도 씩
AI 1 = 0도
AI 2 = 120도
AI 3 = 240도

AI 4개 : 360 / 4 = 90도 씩
AI 1 = 0도 (동쪽)
AI 2 = 90도 (북쪽)
AI 3 = 180도 (서쪽)
AI 4 = 270도 (남쪽)

이런식으로 구현이 되기 때문에 AI의 수에 따라 각도를 할당해 누구하나 빠짐없이 포위를 할 수 있게 만들어 두었습니다.

  • AI 개수가 변해도 자동으로 재계산
  • 각 AI는 고유한 각도를 할당받습니다.
  • Dictionary로 AI마다 각도를 저장합니다.

2단계 : 포메이션 위치 계산

// AIBlackboard.cs
public Vector3 GetFormationPosition(Transform self, Vector3 centerPos, float radius)
{
    if (!assingedAngles.ContainsKey(self))
        return centerPos;

    float angle = assingedAngles[self];

    //원형 좌표계 변환 (극좌표 → 직교좌표)
    Vector3 offset = new Vector3(
        Mathf.Cos(angle * Mathf.Deg2Rad),  // X축 (좌우)
        0,                                   // Y축 (높이 고정)
        Mathf.Sin(angle * Mathf.Deg2Rad)   // Z축 (앞뒤)
    ) * radius;

    Vector3 formationPos = centerPos + offset;

    // NavMesh 위의 유효한 위치로 조정
    UnityEngine.AI.NavMeshHit hit;
    if(UnityEngine.AI.NavMesh.SamplePosition(formationPos, out hit, 2f, UnityEngine.AI.NavMesh.AllAreas))
    {
        return hit.position;
    }

    return formationPos;

극좌표(반지름, 각도)를 직교좌표(x,y,z)값으로 변환을 하였습니다.
X = radius x cos(angle)
z = radius x sin(angle)
y = 0 (지면 고정)
예시로 반지름 10cm, 각도 90이라면
X = 10 x cos(90) = 0
z = 10 x sin(90) = 10
-> 위치는 (0, 0, 10)으로 됩니다.

  • 플레이어 중심으로 원형 배치
  • NavMesh 검증으로 이동 가능한 위치만 사용
  • AI마다 고유한 위치를 가짐

3단계 : 거리별 행동 전환(히스테리시스)

// Ai_Script.cs
private enum ChaseMode { WideFormation, NarrowFormation, DirectChase }
private ChaseMode currentChaseMode = ChaseMode.WideFormation;
public float modeTransitionBuffer = 1.5f;  //핵심

switch (currentChaseMode)
{
    case ChaseMode.WideFormation:
        // 10.5m 이하에서 좁은 포위로 전환
        if(distanceToPlayer <= formationDistance - modeTransitionBuffer)
        {
            currentChaseMode = ChaseMode.NarrowFormation;
            MoveToFormation(6f);
        }
        else
        {
            MoveToFormation(12f);
        }
        break;
    
    case ChaseMode.NarrowFormation:
        // 13.5m 초과에서 넓은 포위로
        if(distanceToPlayer > formationDistance + modeTransitionBuffer)
        {
            currentChaseMode = ChaseMode.WideFormation;
            MoveToFormation(12f);
        }
        // 6.5m 이하에서 직접 추격으로
        else if(distanceToPlayer <= directChaseDistance - modeTransitionBuffer)
        {
            currentChaseMode = ChaseMode.DirectChase;
            DirectChase();
        }
        else
        {
            MoveToFormation(6f);
        }
        break;
    
    case ChaseMode.DirectChase:
        // 9.5m 초과에서 좁은 포위로
        if(distanceToPlayer > directChaseDistance + modeTransitionBuffer)
        {
            currentChaseMode = ChaseMode.NarrowFormation;
            MoveToFormation(6f);
        }
        else
        {
            DirectChase();
        }
        break;

히스테리시스 원리는 간단히 위치에 따른 AI의 행동 변화 입니다. 물론 AI의 우선 순위는 플레이어를 추격하는 것 이며, 추격하는 과정에서 포위를 해야 되는 것이기 때문에 플레이어와 가까이 있다면 포위가 아닌 단순 추격을 하는게 당연합니다.

NarrowFormation 상태

  • 6.5m 이하: 직접 추격으로 전환
  • 6.5 ~ 9.5m : 포위 유지 (안정 구간)
  • 9.5m 초과 : 넓은 포위로 전환

DirectChase 상태

  • 9.5m 이하 : 직접 추격 유지 (안정 구간)
  • 9.5m 초과 : 포위로 전환

4단계 Flocking(군집 행동)

// AIBlackboard.cs
public Vector3 GetFlockingDir(Transform self)
{
    Vector3 separation = Vector3.zero;  // 분리
    Vector3 alignment = Vector3.zero;   // 정렬
    Vector3 cohesion = Vector3.zero;    // 응집
    int neighborCount = 0;

    foreach (var agent in aiAgents)
    {
        if (agent == null || agent == self) continue;
        
        float distance = Vector3.Distance(agent.position, self.position);

        if (distance < neighborRadius && distance > 0.01f)
        {
            //Separation (분리) 너무 가까우면 밀어냄
            Vector3 awayDir = (self.position - agent.position).normalized;
            float separationStrength = 1f / (distance * distance);  // 역제곱 법칙
            separation += awayDir * separationStrength;

            //Alignment (정렬) 이웃의 방향과 맞춤
            if (agent.GetComponent<Ai_Script>() != null)
            {
                alignment += agent.forward;
            }

            //Cohesion (응집) 그룹 중심으로 이동
            cohesion += agent.position;

            neighborCount++;
        }
    }

    // 평균 계산
    if (neighborCount > 0)
    {
        alignment = (alignment / neighborCount).normalized;
        cohesion = ((cohesion / neighborCount) - self.position).normalized;
    }

    //가중치 적용
    Vector3 flockingDir =
        separation * separationWeight +  // 1.5 (가장 중요)
        alignment * alignmentWeight +    // 1.0
        cohesion * cohesionWeight;       // 1.0

    return flockingDir.normalized;

Flocking 시스템의 가장 중요한 점은 크레이그 레이놀즈의 Boids알 고리즘을 이용한 것 입니다.
Separation (분리) - 너무 가까운 개체는 피한다
Alignment (정렬) - 이웃의 방향에 맞춘다
Cohesion (응집) - 이웃들과 뭉친다.
이 세가지 개념을 통해 Flocking기능을 구현하였습니다만, 이 세 가지 힘의 균형이 가장 중요하기 때문에 블로그를 쓰는 지금도 계속해서 수정하면서 개발중 입니다.

5단계 : 포메이션 이동 통합

//Ai_Script.cs
void MoveToFormation(float radius)
{
    if (AIBlackboard.Instance == null || !AIBlackboard.Instance.playerDetect)
        return;

    //1. 목표 위치 계산
    Vector3 formationPos = AIBlackboard.Instance.GetFormationPosition(
        transform,
        player.position,  // 플레이어 중심
        radius            // 포위 반경
    );

    currentFormationTarget = formationPos;
    float distToFormation = Vector3.Distance(transform.position, formationPos);

    if (distToFormation > formationReachThreshold)  // 2m 초과
    {
        //2. 이동 중: Flocking 효과 적용
        isInFormation = false;

        Vector3 flockingDir = AIBlackboard.Instance.GetFlockingDir(transform);
        Vector3 toFormation = (formationPos - transform.position).normalized;

        //3. 포메이션 70% + Flocking 30% 혼합
        Vector3 finalDir = (toFormation * 0.7f + flockingDir * 0.3f).normalized;

        agent.SetDestination(formationPos);

        // 이동 방향으로 회전
        if (finalDir != Vector3.zero)
        {
            Quaternion targetRot = Quaternion.LookRotation(finalDir);
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, Time.deltaTime * 5f);
        }
    }
    else
    {
        //도착 플레이어 주시
        isInFormation = true;

        Vector3 lookDir = (player.position - transform.position).normalized;
        lookDir.y = 0;
        if (lookDir != Vector3.zero)
        {
            Quaternion targetRot = Quaternion.LookRotation(lookDir);
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, Time.deltaTime * 5f);
        }

        agent.SetDestination(formationPos);  // 위치 유지
    }
}

지금 까지 만들어둔 기능인 Formation 과 Flocking 기능을 통합한 코드입니다.
중간 코드를 보시면 toFormation 70% + Flocking 30%로 혼합을 했습니다.
포메이션의 비중을 높히고 군집의 기능의 비중을 낮춘다면 목표 지향적이지만 AI끼리 부딛힐 수 있습니다.
반대로 한다면 충돌은 잘 피하지만 플레이어를 쫓아가 포위하는 목표 도달이 느릴 것 입니다.

6단계 : 포메이션 지속 갱신

// Ai_Script.cs - Update() 내부
if (isInFormation)
{
    formationUpdateTimer += Time.deltaTime;
    if (formationUpdateTimer >= 0.2f)  //0.2초마다
    {
        formationUpdateTimer = 0f;

        // 현재 거리에 맞는 반경으로 재계산
        float currentRadius = distanceToPlayer > formationDistance ? 12f : 6f;
        Vector3 newFormationPos = AIBlackboard.Instance.GetFormationPosition(
            transform, 
            player.position,  //항상 최신 위치
            currentRadius
        );

        // 새 위치가 멀리 떨어져있으면 다시 이동
        if (Vector3.Distance(transform.position, newFormationPos) > formationReachThreshold)
        {
            isInFormation = false;  //이동 모드로 전환
            agent.SetDestination(newFormationPos);
        }
    }
}

갱신이 필요한 이유는 Ai가 플레이어를 인식한 뒤 해당 포메이션으로 이동한 뒤 도착하게 되면 멈추게 됩니다. 그런 과정에서 플레이어가 이동을 한다면 플레이어의 위치는 달라지지만 Ai는 포메이션으로 도착을 하게 된 상황이 되므로 주변에 멈춰있습니다. 물론 Update() 내부에 이루어지는 과정이기에 드라마틱하게 멈춰있거나 하진 않지만 AI가 절절거릴 수 있습니다.

현재는 0.2초로 갱신을 진행하였고 이 부분도 계속해서 최적화가 필요한 부분입니다.
도달 후에도 계속 갱신을 해야 되기 때문에 플레이어의 움직임을 최종적으로 추적할 수 있습니다.

전체 동작 흐름

  1. 게임 시작
  2. AI들이 Blackvoard에 자동 등록
  3. 각 AI에게 고유 각도 할당
  4. 순찰 모드
  5. 플레이어 발견 (시야 또는 거리)
  6. Blackboard에 플레이어 위치 공유
  7. 다른 AI들도 정보 수신 -> 추격 시작
  8. 거리별 모드 전환
    • 12m 초과 : WideFormation : AI들이 넓게 퍼져 포위
    • 8 ~ 12m : NarrowFormation : 포위망 좁히기, 더 가까운 거리에서 압박
    • 8m 이하 : DirectChase : 포메이션 무시, 플레이어 바로 쫓아감
  9. 플레이어 시야 벗어남
  10. 5초간 추격 유지(마지막으로 바라본 플레이어 위치에서)
  11. 추격 포기 -> 순찰 모드 복귀

식으로 흘러가게 됩니다.

이후 수정부분

사실 지금까지 과정중 완벽한건 없습니다. 아직 AI가 플레이어를 쫓아오다가 플레이어가 멈추고 있으면 몇몇 AI가 정지상태가 되는 버그들도 있기 때문에 계속해서 수정해야 됩니다.
수정하는 부분이 있으면 그 부분도 추가로 블로그에 작성하도록 하겠습니다. ^^

profile
말하는 감자

0개의 댓글