오픈월드 게임에서 수백~수천 명의 NPC를 동시에 움직이게 하는 건 오랫동안 게임 개발자들의 숙제였다. 언리얼 엔진의 전통적인 접근은 각 NPC를 AActor 서브클래스로 만들고 UCharacterMovementComponent와 AIController를 붙이는 방식이다. NPC 20명이라면 무리가 없다. 2000명이 되면 CPU 틱 연산과 메모리 할당이 폭발적으로 늘어난다.
Unreal Fest 2025에서 에픽게임즈는 Mass Entity와 ZoneGraph를 결합한 대규모 군중 시뮬레이션 사례를 여러 세션에 걸쳐 공개했다. The Matrix Awakens 데모에서 처음 모습을 드러낸 이 아키텍처는 UE 5.2 이후 프로덕션 레디 수준에 도달했고, 실제 상업 타이틀 도입 사례도 등장하고 있다.
이 글에서는 Mass Entity의 핵심 개념을 정리하고, ZoneGraph로 이동 경로를 잡는 실전 C++ 코드를 살펴본다.
Mass Entity란 무엇인가
Mass Entity는 언리얼 엔진 5에 탑재된 ECS(Entity Component System) 구현체다. 데이터를 연속 메모리(Archetype)에 배치해 캐시 효율을 극대화하고, Processor에서 병렬 처리한다. Unity DOTS의 IJobEntity와 설계 철학이 유사하다.
핵심 구성 요소 세 가지:
- Fragment: 데이터 컨테이너.
FMassFragment를 상속하는 C++ 구조체. - Processor: 로직 단위.
UMassProcessor를 상속, 매 틱마다 Fragment를 일괄 처리. - Archetype: 동일한 Fragment 조합을 가진 엔티티 묶음. 메모리 레이아웃이 동일해 SIMD 최적화에 유리.
AActor와 결정적으로 다른 점은 틱이 없다는 것이다. 각 엔티티는 독립적인 BeginPlay/Tick/EndPlay 생명주기를 갖지 않는다. 대신 Processor가 동일 Fragment를 가진 모든 엔티티를 청크 단위로 한꺼번에 처리한다.
Fragment와 Processor 작성
이동 예제로 구조를 익혀보자. 먼저 이동 목적지와 속도를 담는 Fragment:
// MoveTargetFragment.h
#pragma once
#include "MassCommonFragments.h"
#include "MoveTargetFragment.generated.h"
USTRUCT()
struct FMoveTargetFragment : public FMassFragment
{
GENERATED_BODY()
FVector TargetLocation = FVector::ZeroVector;
float Speed = 200.f;
};
이 Fragment를 처리하는 Processor:
// SimpleMoveProcessor.h
#pragma once
#include "MassProcessor.h"
#include "SimpleMoveProcessor.generated.h"
UCLASS()
class USimpleMoveProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
USimpleMoveProcessor();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context) override;
private:
FMassEntityQuery EntityQuery;
};
// SimpleMoveProcessor.cpp
#include "SimpleMoveProcessor.h"
#include "MoveTargetFragment.h"
#include "MassTransformFragments.h"
USimpleMoveProcessor::USimpleMoveProcessor()
{
ExecutionFlags = (int32)EProcessorExecutionFlags::AllNetModes;
ProcessingPhase = EMassProcessingPhase::PrePhysics;
}
void USimpleMoveProcessor::ConfigureQueries()
{
EntityQuery.AddRequirement<FMoveTargetFragment>(EMassFragmentAccess::ReadOnly);
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
}
void USimpleMoveProcessor::Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
const float DeltaTime = Context.GetDeltaTimeSeconds();
EntityQuery.ForEachEntityChunk(EntityManager, Context,
[DeltaTime](FMassExecutionContext& Ctx)
{
auto MoveTargets = Ctx.GetFragmentView<FMoveTargetFragment>();
auto Transforms = Ctx.GetMutableFragmentView<FTransformFragment>();
for (int32 i = 0; i < Ctx.GetNumEntities(); ++i)
{
const FVector Dir = (MoveTargets[i].TargetLocation
- Transforms[i].GetTransform().GetLocation()).GetSafeNormal();
const FVector Delta = Dir * MoveTargets[i].Speed * DeltaTime;
Transforms[i].GetMutableTransform().AddToTranslation(Delta);
}
});
}
ForEachEntityChunk 내부는 동일 Archetype 청크 단위로 루프를 돌며, 언리얼이 자동으로 TaskGraph에 병렬 디스패치한다. 엔티티 수가 많을수록 멀티코어 효율이 올라가는 구조다.
ZoneGraph로 이동 경로 구성하기
수천 명의 NPC가 실제 거리를 따라 자연스럽게 이동하려면 내비게이션이 필요하다. 그러나 NavMesh는 에이전트 수가 늘수록 경로 쿼리 비용이 급격히 증가한다. Mass Entity 기반 군중에는 ZoneGraph와 결합하는 방식이 권장된다.
ZoneGraph는 레벨에 미리 도로와 통로 레인을 정의하는 경량 그래프다. NPC는 개별 경로 탐색 대신 레인을 예약하고 따라가기 때문에, 천 명이 넘어도 NavMesh를 직접 쿼리하지 않는다.
활성화 순서:
1. 프로젝트 설정 > Plugins > MassGameplay, ZoneGraph, MassCrowd 활성화
2. 레벨에 ZoneGraphData 액터를 배치하고 레인 스플라인을 배치한다
3. UMassCrowdSubsystem이 자동으로 ZoneGraph와 연동해 레인 할당을 처리한다
// 에이전트를 ZoneGraph 레인에 등록 (스폰 시 1회 호출)
#include "ZoneGraphSubsystem.h"
#include "MassCrowdFragments.h"
#include "MassTransformFragments.h"
void RegisterAgentToLane(FMassEntityManager& EntityManager,
FMassEntityHandle Entity,
const UWorld* World)
{
UZoneGraphSubsystem* ZGS = World->GetSubsystem<UZoneGraphSubsystem>();
if (!ZGS) return;
const FVector AgentLocation = EntityManager
.GetFragmentDataChecked<FTransformFragment>(Entity)
.GetTransform().GetLocation();
FZoneGraphLaneLocation LaneLocation;
if (ZGS->FindNearestLane(AgentLocation, 500.f, LaneLocation))
{
FMassCrowdLaneTrackingFragment& Tracking =
EntityManager.GetFragmentDataChecked<FMassCrowdLaneTrackingFragment>(Entity);
Tracking.LaneHandle = LaneLocation.LaneHandle;
}
}
레인 등록은 스폰 시점에 한 번만 수행하고, 이후 MassCrowd Processor가 레인 이동을 일괄 처리한다.
성능과 도입 시 고려사항
Unreal Fest 2025 세션에서 공개된 벤치마크에 따르면, 동일 NPC 2000명 기준으로 AActor 방식 대비 CPU 틱 비용 60~80% 절감 사례가 보고됐다. 다만 몇 가지 조건이 있다.
비주얼 표현: Mass Entity 자체는 렌더링을 담당하지 않는다. ISMComponent(Instanced Static Mesh) 또는 커스텀 렌더러를 별도로 연결해야 한다. 복잡한 스켈레탈 메시는 Skeletal Mesh Batching이 필요하며 아직 완성도가 낮아 간소화된 LOD 애니메이션을 쓰는 것이 현실적이다.
AI 복잡도: 단순 이동과 군중 시뮬레이션에는 탁월하지만, 복잡한 행동 트리가 필요한 주요 NPC는 여전히 AActor + AIController가 적합하다. 주요 NPC는 AActor, 배경 군중은 Mass Entity로 가져가는 하이브리드 구조가 가장 일반적이다.
디버깅: MassDebugger 뷰포트 플러그인을 활성화하면 Fragment 상태를 실시간 시각화할 수 있다. Processors 탭에서 각 Processor 실행 시간을 확인해 병목 지점을 짚어낼 수 있다.
결론
Mass Entity + ZoneGraph 조합은 오픈월드 NPC 대규모 처리 문제에 대한 언리얼 엔진의 정식 해법으로 자리잡고 있다. AActor 기반을 전면 교체하는 게 아니라, 군중처럼 동질적이고 대량의 엔티티를 처리하는 별도 레이어로 병행 도입하는 접근이 현실적이다.
UE 5.4 이후 MassCrowd 모듈 안정성이 크게 향상됐고, Unreal Fest 2025에서 실제 상업 타이틀 사례도 공개됐다. 프로토타입 단계부터 Mass Entity 파이프라인을 실험해두면, 이후 스케일업 시 리팩터링 비용을 크게 줄일 수 있다.
'게임 산업 트렌드' 카테고리의 다른 글
| 2026 GPU 드리븐 렌더링 트렌드, Unity 6 GPU Resident Drawer와 RenderMeshIndirect로 드로우콜 만 단위 줄이기 (0) | 2026.06.27 |
|---|---|
| 2026 멀티플레이어 트렌드: 서버 권위로의 회귀, Unity로 구현하는 클라이언트 예측과 재조정 (1) | 2026.06.20 |
| GDC 2026 게임 트렌드: 언리얼 엔진 NNE로 구현하는 온디바이스 AI 캐릭터 제어 (1) | 2026.06.06 |
| 2026 GDC 이후 실무 체크리스트: Nanite Foliage와 MegaLights가 바꾼 라이팅 워크플로우 (1) | 2026.05.30 |
| Work Graphs가 컴퓨트 셰이더를 대체한다는 말, 2026년에 진짜로 시작됐다 (GDC 2026 정리) (1) | 2026.05.23 |