LYSC STUDIO

LIST
Cover

언리얼 엔진 5(UE5) Mass Entity 프레임워크를 활용한 10만 단위 대규모 군중 시뮬레이션 구축 가이드

·UE5 Engine

인디 게임에서 대규모 전투나 살아 숨 쉬는 도시를 구현하기란 쉽지 않은 과제입니다. UE5의 차세대 데이터 지향형 프레임워크인 Mass Entity를 통해 퍼포먼스 저하 없이 대규모 군중 시뮬레이션을 구현하는 방법과 실무적인 최적화 노하우를 깊이 있게 알아봅니다.

도입 및 개요

언리얼 엔진 5(UE5)가 도입되면서 게임 개발자들은 기존 객체 지향 프로그래밍(OOP) 기반의 한계를 뛰어넘어, 데이터 지향 설계(Data-Oriented Design)를 기반으로 한 Mass Entity 프레임워크를 사용할 수 있게 되었습니다. 특히 인디 게임 개발 과정에서 제한된 인력과 자원으로 AAA급 게임에 버금가는 스케일, 예를 들어 수만 명의 NPC가 살아 움직이는 대규모 도시 씬이나 대규모 좀비 웨이브 전투를 구현하려면 기존의 UObject나 AActor 기반의 접근 방식으로는 메모리와 CPU 캐시 미스(Cache Miss) 문제로 인해 극심한 프레임 드랍을 겪을 수밖에 없습니다.

Mass Entity의 핵심은 ECS(Entity Component System) 아키텍처를 언리얼 엔진의 생태계에 맞게 변형하여 도입한 것입니다. Entity는 단순한 ID에 불과하며, Component(Mass Entity에서는 Fragment라고 부름)는 순수하게 데이터만을 담고 있습니다. 그리고 System(Mass Processor)이 이러한 데이터를 일괄적으로 처리합니다. 이 방식은 데이터가 메모리에 연속적으로 배치되도록 강제하므로, CPU 캐시 활용도를 극대화하여 처리 속도를 비약적으로 상승시킵니다. 구체적인 사례로, 평범한 AActor 기반으로 1,000마리의 좀비 AI를 Tick 함수로 처리하면 즉시 프레임이 30FPS 밑으로 떨어지지만, Mass Entity를 활용하면 10만 마리의 객체를 60FPS로 무리 없이 시뮬레이션할 수 있습니다.

Mass Entity를 프로젝트에 도입하기 위해서는 먼저 플러그인 설정에서 MassGameplay 플러그인을 활성화해야 합니다. 이후 MassSpawner를 레벨에 배치하고, 어떤 형태의 Entity를 생성할지 정의하는 MassEntityConfig를 설정합니다. 여기에는 Trait(특성)이라는 개념이 사용되는데, 예를 들어 이동을 담당하는 Movement Trait, 시각적 표현을 담당하는 Visualization Trait, 충돌 및 회피를 다루는 Avoidance Trait 등을 조합하여 하나의 완전한 NPC 군중 개체를 정의하게 됩니다. 특히 시각화를 담당하는 부분은 Instanced Static Mesh(ISM)나 Hierarchical Instanced Static Mesh(HISM)를 기반으로 동작하여 수천 개의 메시를 한 번의 드로우 콜(Draw Call)로 렌더링함으로써 렌더 스레드의 부하를 극적으로 줄입니다.

구체적인 최적화 사례를 살펴보겠습니다. 한 인디 게임 개발팀은 중세 시대 공성전 게임을 개발하며 5,000명의 병사가 성벽을 향해 돌격하는 씬을 구상했습니다. 처음에는 언리얼 엔진의 Character 무브먼트 컴포넌트를 사용했으나, 병사들이 서로 부딪치며 발생하는 물리 연산과 애니메이션 업데이트 부하로 인해 게임이 멈추는 수준에 이르렀습니다. 이들은 Mass Entity 프레임워크로 시스템을 전면 개편했습니다. 병사들의 현재 위치, 목표 위치, 이동 속도를 Fragment 데이터로 분리하고, MassProcessor를 상속받은 커스텀 프로세서에서 다중 스레드(Multithreading)를 통해 모든 병사의 이동 벡터를 한 번에 계산했습니다. 애니메이션의 경우 State Machine을 사용하는 대신, Vertex Animation Texture(VAT) 기법을 Mass Visualization에 결합하여 GPU에서 직접 애니메이션 프레임을 보간하도록 처리했습니다. 그 결과, 5,000명의 개체가 렌더링 및 물리 충돌까지 포함하여 CPU 스레드 타임 2ms 내에 처리가 완료되는 놀라운 최적화를 이루어냈습니다.

핵심 분석

하지만 Mass Entity를 활용할 때 주의할 점도 있습니다. 데이터 지향 설계는 기존의 상속 기반 로직이나 블루프린트를 통한 시각적 스크립팅에 익숙한 개발자에게는 패러다임의 전환을 요구합니다. 모든 상태를 데이터로 정의하고 조건 분기를 최소화해야 성능 이점을 얻을 수 있습니다. 또한 디버깅이 까다로울 수 있는데, Gameplay Debugger를 적극 활용하여 Mass 디버깅 기능을 켜고 각 Entity가 소유한 Fragment의 현재 상태를 실시간으로 모니터링하는 습관을 들여야 합니다. 결론적으로 Mass Entity 프레임워크는 초기 진입 장벽이 다소 존재하지만, 이를 극복하고 나면 인디 게임의 한계라 여겨졌던 스케일의 제약을 완벽하게 부수고 완전히 새로운 게임 플레이 경험을 창출할 수 있는 강력한 무기가 됩니다.

Implementation C++ / UE5
UCLASS()
class UMassCustomMovementProcessor : public UMassProcessor
{
    GENERATED_BODY()
public:
    UMassCustomMovementProcessor();
protected:
    virtual void ConfigureQueries() override;
    virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;
private:
    FMassEntityQuery MovementQuery;
};

UMassCustomMovementProcessor::UMassCustomMovementProcessor()
{
    bAutoRegisterWithProcessingPhases = true;
    ExecutionFlags = (int32)EProcessorExecutionFlags::All;
    ExecutionOrder.ExecuteBefore.Add(UE::Mass::ProcessorGroupNames::Movement);
}

void UMassCustomMovementProcessor::ConfigureQueries()
{
    MovementQuery.AddRequirement(EMassFragmentAccess::ReadWrite);
    MovementQuery.AddRequirement(EMassFragmentAccess::ReadOnly);
}

void UMassCustomMovementProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
    MovementQuery.ForEachEntityChunk(EntityManager, Context, [this](FMassExecutionContext& Context)
    {
        const int32 NumEntities = Context.GetNumEntities();
        const float DeltaTime = Context.GetDeltaTimeSeconds();
        TArrayView Transforms = Context.GetMutableFragmentView();
        TConstArrayView Velocities = Context.GetFragmentView();
        
        for (int32 i = 0; i < NumEntities; ++i)
        {
            Transforms[i].GetMutableTransform().AddToTranslation(Velocities[i].Velocity * DeltaTime);
        }
    });
}
Mass Entity는 초기 학습 곡선이 가파르지만, 데이터 지향 프로그래밍의 강력함을 이해하고 성공적으로 도입한다면 인디 게임의 스케일을 AAA급으로 끌어올릴 수 있는 혁신적인 도구입니다. 꾸준한 프로파일링과 설계 최적화를 통해 자신만의 웅장한 세계를 구현해 보시기 바랍니다.