Flocking 알고리즘으로 군중과 조류의 자연스러운 이동 만들기
Flocking의 핵심 규칙인 분리, 정렬, 응집을 이용해 다수 에이전트가 자연스럽게 이동하도록 구현한다. Unity C# 예제와 성능 최적화 기준을 함께 정리한다.

Flocking이란
Flocking은 Craig Reynolds의 Boids 모델에서 널리 알려진 집단 이동 알고리즘이다. 각 개체는 전체 무리의 경로를 미리 받지 않는다. 대신 가까운 이웃의 위치와 속도를 관찰하고 간단한 규칙을 합성해 다음 이동 방향을 결정한다.
이 방식은 새 떼, 물고기 떼, 좀비 군중처럼 개별 행동은 단순하지만 집단으로 보면 유기적인 움직임을 표현할 때 유용하다. 경로 탐색을 완전히 대체하는 알고리즘은 아니며 목적지 이동이나 장애물 회피와 함께 사용하는 것이 일반적이다.
세 가지 기본 규칙
각 에이전트의 위치를 , 속도를 , 탐색 반경 안에 있는 이웃 집합을 라고 하자. Flocking은 보통 아래 세 조향력을 가중합해 계산한다.
분리: 너무 가까운 이웃에서 멀어진다
분리(Separation)는 개체끼리 겹치거나 한 지점에 뭉치는 현상을 줄인다. 가까울수록 더 큰 반발력을 주면 자연스럽다.
거리의 제곱으로 나누면 아주 가까운 이웃의 영향이 강해진다. 다만 힘이 지나치게 커지지 않도록 최대값을 제한하는 편이 안전하다.
정렬: 이웃과 비슷한 방향으로 움직인다
정렬(Alignment)은 주변 이웃의 평균 속도에 자신의 속도를 맞추려는 힘이다. 새 떼가 한 방향으로 선회하는 느낌을 만드는 데 중요한 역할을 한다.
응집: 무리의 중심으로 향한다
응집(Cohesion)은 이웃의 평균 위치, 즉 지역 중심을 향하게 한다. 이 힘이 없으면 분리와 정렬만으로는 무리가 여러 조각으로 흩어지기 쉽다.
세 힘을 합친 최종 조향력은 다음처럼 표현할 수 있다.
가중치 , , 는 장면의 성격에 따라 조정한다. 군중은 분리 비중을 높여 개인 공간을 확보하고 조류는 정렬과 응집을 조금 더 높이면 밀집된 비행 무리가 만들어진다.

Unity C# 기본 구현
아래 예제는 작은 규모의 무리에서 이해하기 쉬운 전체 탐색 방식이다. 각 에이전트는 매 프레임 다른 모든 에이전트를 확인하고 반경 안의 이웃으로부터 세 가지 힘을 계산한다.
using System.Collections.Generic;
using UnityEngine;
public class FlockAgent : MonoBehaviour
{
public static readonly List<FlockAgent> All = new List<FlockAgent>();
[Header("Movement")]
[SerializeField] float maxSpeed = 5f;
[SerializeField] float maxForce = 8f;
[SerializeField] float neighborRadius = 3f;
[Header("Weights")]
[SerializeField] float separationWeight = 2.2f;
[SerializeField] float alignmentWeight = 1.0f;
[SerializeField] float cohesionWeight = 1.0f;
Vector3 velocity;
void OnEnable()
{
All.Add(this);
velocity = transform.forward * Random.Range(1f, maxSpeed);
}
void OnDisable()
{
All.Remove(this);
}
void Update()
{
Vector3 separation = Vector3.zero;
Vector3 averageVelocity = Vector3.zero;
Vector3 averagePosition = Vector3.zero;
int neighborCount = 0;
foreach (FlockAgent other in All)
{
if (other == this)
continue;
Vector3 offset = transform.position - other.transform.position;
float distanceSqr = offset.sqrMagnitude;
float radiusSqr = neighborRadius * neighborRadius;
if (distanceSqr <= 0.0001f || distanceSqr > radiusSqr)
continue;
separation += offset / distanceSqr;
averageVelocity += other.velocity;
averagePosition += other.transform.position;
neighborCount++;
}
Vector3 steering = separation * separationWeight;
if (neighborCount > 0)
{
averageVelocity /= neighborCount;
averagePosition /= neighborCount;
steering += (averageVelocity - velocity) * alignmentWeight;
steering += (averagePosition - transform.position) * cohesionWeight;
}
steering = Vector3.ClampMagnitude(steering, maxForce);
velocity = Vector3.ClampMagnitude(velocity + steering * Time.deltaTime, maxSpeed);
transform.position += velocity * Time.deltaTime;
if (velocity.sqrMagnitude > 0.0001f)
transform.forward = velocity.normalized;
}
}
빈 GameObject에 이 스크립트를 붙인 프리팹을 여러 개 생성하면 기본적인 무리 움직임을 확인할 수 있다. 에이전트가 처음부터 같은 방향과 속도를 가지면 움직임이 단조로우므로 시작 속도에 작은 무작위성을 주는 편이 좋다.
이 예제는 각 에이전트가 Update에서 곧바로 상태를 바꾸므로 같은 프레임 안에서도 실행 순서에 따라 이웃 데이터가 섞일 수 있다. 재현성이 중요하거나 개체 수가 늘어나면 한 단계에서 모든 에이전트의 상태를 먼저 스냅샷으로 읽고, 다음 단계에서 새 상태를 적용하는 관리자 구조로 바꾼다.
목적지와 장애물 회피를 함께 적용하기
Flocking만 사용하면 무리는 현재 주변 상태에만 반응하며 특정 장소로 이동하지 못한다. 목표 지점을 향하는 힘을 추가하면 무리 전체에 방향성을 줄 수 있다.
Vector3 Seek(Vector3 target)
{
Vector3 desiredVelocity = (target - transform.position).normalized * maxSpeed;
return desiredVelocity - velocity;
}
계산한 힘에 다음처럼 더한다.
steering += Seek(targetPosition) * targetWeight;
장애물 회피도 같은 방식으로 조향력 하나를 추가한다. 진행 방향으로 SphereCast나 Raycast를 쏜 뒤 충돌 예상 지점의 법선 방향으로 힘을 주면 된다. 다만 회피력이 너무 강하면 개체가 무리에서 이탈하고 너무 약하면 장애물에 부딪힌다. 목표 추적, 회피, Flocking의 상대적 우선순위를 장면에 맞춰 조절해야 한다.
성능: 모든 개체를 서로 검사하지 않기
위 예제는 에이전트 수가 일 때 대략 의 이웃 검사 비용이 든다. 수십 개체에서는 충분할 수 있지만 수백~수천 개체에서는 병목이 될 가능성이 크다.
가장 먼저 적용하기 좋은 방법은 공간 분할이다. 월드를 셀로 나누고 각 에이전트를 자신의 셀에 등록한 뒤 현재 셀과 인접 셀만 검사한다. 탐색 반경과 셀 크기를 비슷하게 잡으면 멀리 있는 개체를 불필요하게 검사하지 않아도 된다.
flowchart LR
A[에이전트 위치] --> B[공간 셀에 등록]
B --> C[현재 셀과 인접 셀 조회]
C --> D[반경 안 이웃만 선별]
D --> E[분리·정렬·응집 계산]
E --> F[속도와 위치 갱신]
추가로 다음 기준도 효과적이다.
- 거리 비교에는
Vector3.Distance대신 제곱 거리인sqrMagnitude를 사용한다. - 화면 밖의 멀리 있는 무리는 갱신 주기를 낮추거나 단순 애니메이션으로 대체한다.
- 많은 에이전트를 다룬다면 Unity Jobs와 Burst, ECS를 검토한다.
- 렌더링 비용도 별도로 측정한다. 알고리즘 계산보다 메시 수와 드로우 콜이 먼저 병목이 될 수 있다.
튜닝 순서와 자주 생기는 문제
처음에는 한 규칙씩 켜며 효과를 확인하는 것이 좋다. 분리를 먼저 조정해 충돌을 막고 응집으로 무리가 유지되는지 확인한 다음, 정렬로 진행 방향의 통일감을 더한다.
에이전트가 계속 떨린다면 최대 조향력이나 가중치가 너무 높을 수 있다. 힘과 속도를 제한하고 각 프레임에서 계산한 결과를 바로 다른 에이전트가 참조하지 않도록 위치와 속도의 읽기 단계와 쓰기 단계를 분리하면 안정성이 좋아진다.
무리가 한 덩어리로 굳어 보인다면 탐색 반경을 줄이거나 응집 가중치를 낮춘다. 반대로 무리가 쉽게 분열하면 응집 또는 정렬 가중치를 올리고 분리 반경과 분리 힘이 과도하지 않은지 확인한다.
Flocking은 단순한 규칙의 조합이지만 목표 추적과 회피 그리고 성능 전략을 함께 설계하면 게임 속 군중과 조류에 설득력 있는 집단 행동을 부여할 수 있다.



