MassProcessor 기초
ECS의 System에 해당하는 MassProcessor의 구조와 작성 방법을 학습합니다
Processor의 역할
데이터를 변환하는 상태 없는 로직 단위
UMassProcessor는 ECS의 System에 해당합니다. Fragment 데이터를 읽고 변환하는 상태 없는(Stateless) 로직 클래스입니다. 매 틱마다 자동으로 실행되며, Query를 통해 처리할 엔티티를 선택합니다.
Processor는 데이터를 소유하지 않습니다. Fragment가 데이터를, Processor가 로직을 담당합니다. 이 분리가 Mass Entity의 확장성과 병렬 처리의 기반입니다.
// MyMovementProcessor.h
UCLASS()
class UMyMovementProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
UMyMovementProcessor();
protected:
// Query 설정: 어떤 Fragment를 사용할지 정의
virtual void ConfigureQueries() override;
// 매 틱 실행되는 메인 로직
virtual void Execute(
FMassEntityManager& EntityManager,
FMassExecutionContext& Context) override;
private:
FMassEntityQuery MovementQuery;
};
// MyMovementProcessor.cpp
UMyMovementProcessor::UMyMovementProcessor()
{
// 자동으로 처리 단계에 등록
bAutoRegisterWithProcessingPhases = true;
// 물리 시뮬레이션 전에 실행
ProcessingPhase = EMassProcessingPhase::PrePhysics;
// 실행 환경 설정
ExecutionFlags = (int32)(
EProcessorExecutionFlags::Client |
EProcessorExecutionFlags::Standalone);
}
void UMyMovementProcessor::ConfigureQueries()
{
// Transform을 읽고 쓸 수 있게, Velocity는 읽기만
MovementQuery.AddRequirement<FTransformFragment>(
EMassFragmentAccess::ReadWrite);
MovementQuery.AddRequirement<FVelocityFragment>(
EMassFragmentAccess::ReadOnly);
// 이 Processor에 Query 등록
MovementQuery.RegisterWithProcessor(*this);
}
void UMyMovementProcessor::Execute(
FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
MovementQuery.ForEachEntityChunk(EntityManager, Context,
[](FMassExecutionContext& Ctx)
{
auto Transforms = Ctx.GetMutableFragmentView<FTransformFragment>();
const auto Velocities = Ctx.GetFragmentView<FVelocityFragment>();
const float DT = Ctx.GetDeltaTimeSeconds();
for (int32 i = 0; i < Ctx.GetNumEntities(); ++i)
{
Transforms[i].Transform.AddToTranslation(
Velocities[i].LinearVelocity * DT);
}
});
}
처리 단계 (Processing Phase)
6개의 처리 단계로 구성된 실행 파이프라인
Mass Entity는 매 틱마다 6개의 처리 단계(Processing Phase)를 순서대로 실행합니다. 각 Processor는 하나의 단계에 등록됩니다.
| 단계 | EMassProcessingPhase | 용도 |
|---|---|---|
| 1 | PrePhysics | 물리 전 준비: 이동, 입력 처리, AI 결정 |
| 2 | StartPhysics | 물리 시작: 물리 시뮬레이션에 데이터 전달 |
| 3 | DuringPhysics | 물리 실행 중: 물리와 무관한 처리 |
| 4 | EndPhysics | 물리 종료: 물리 결과 수집 |
| 5 | PostPhysics | 물리 후 처리: 충돌 응답, 위치 보정 |
| 6 | FrameEnd | 프레임 마무리: 시각화 업데이트, 정리 |
// AI 판단 → PrePhysics
UAIDecisionProcessor::UAIDecisionProcessor()
{
ProcessingPhase = EMassProcessingPhase::PrePhysics;
}
// 이동 적용 → PrePhysics (AI 이후)
UMovementProcessor::UMovementProcessor()
{
ProcessingPhase = EMassProcessingPhase::PrePhysics;
}
// 충돌 후 보정 → PostPhysics
UCollisionResponseProcessor::UCollisionResponseProcessor()
{
ProcessingPhase = EMassProcessingPhase::PostPhysics;
}
// 시각화 업데이트 → FrameEnd
UVisualizationProcessor::UVisualizationProcessor()
{
ProcessingPhase = EMassProcessingPhase::FrameEnd;
}
ExecutionFlags와 실행 제어
서버, 클라이언트, 스탠드얼론 환경별 실행 제어
ExecutionFlags는 Processor가 어떤 네트워크 환경에서 실행될지를 결정합니다.
| 플래그 | 설명 |
|---|---|
Server |
데디케이트 서버에서만 실행 |
Client |
클라이언트에서만 실행 |
Standalone |
스탠드얼론(싱글플레이) 모드에서 실행 |
// 클라이언트와 스탠드얼론에서 실행 (시각화 등)
ExecutionFlags = (int32)(
EProcessorExecutionFlags::Client |
EProcessorExecutionFlags::Standalone);
// 서버와 스탠드얼론에서 실행 (게임 로직)
ExecutionFlags = (int32)(
EProcessorExecutionFlags::Server |
EProcessorExecutionFlags::Standalone);
// 모든 환경에서 실행
ExecutionFlags = (int32)(
EProcessorExecutionFlags::Server |
EProcessorExecutionFlags::Client |
EProcessorExecutionFlags::Standalone);
게임 스레드 실행 강제
UMyProcessor::UMyProcessor()
{
// true: 반드시 게임 스레드에서 실행
// (UObject 접근, 월드 쿼리 등이 필요할 때)
bRequiresGameThreadExecution = true;
// false: 워커 스레드에서 병렬 실행 가능 (기본값)
// bRequiresGameThreadExecution = false;
}
bRequiresGameThreadExecution = true는 성능에 부정적입니다. UObject 접근, 월드 쿼리, 라인 트레이스 등 게임 스레드가 꼭 필요한 경우에만 사용하세요. 대부분의 순수 데이터 처리 Processor는 false로 두어 병렬 실행을 허용합니다.
Observer Processor
이벤트 기반으로 동작하는 특수 Processor
UMassObserverProcessor는 매 틱 실행되는 일반 Processor와 달리, 특정 Fragment/Tag가 추가되거나 제거될 때만 트리거됩니다. 초기화나 정리 로직에 적합합니다.
UCLASS()
class UDeathObserver : public UMassObserverProcessor
{
GENERATED_BODY()
public:
UDeathObserver();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(
FMassEntityManager& EntityManager,
FMassExecutionContext& Context) override;
FMassEntityQuery DeathQuery;
};
UDeathObserver::UDeathObserver()
{
// FIsDeadTag가 추가될 때 트리거
ObservedType = FIsDeadTag::StaticStruct();
Operation = EMassObservedOperation::Add;
ExecutionFlags = (int32)(
EProcessorExecutionFlags::Server |
EProcessorExecutionFlags::Standalone);
}
void UDeathObserver::ConfigureQueries()
{
DeathQuery.AddRequirement<FTransformFragment>(
EMassFragmentAccess::ReadOnly);
DeathQuery.AddTagRequirement<FIsDeadTag>(
EMassFragmentPresence::All);
DeathQuery.RegisterWithProcessor(*this);
}
void UDeathObserver::Execute(
FMassEntityManager& EntityManager,
FMassExecutionContext& Context)
{
DeathQuery.ForEachEntityChunk(EntityManager, Context,
[](FMassExecutionContext& Ctx)
{
auto Transforms = Ctx.GetFragmentView<FTransformFragment>();
auto Entities = Ctx.GetEntities();
for (int32 i = 0; i < Ctx.GetNumEntities(); ++i)
{
// 사망 처리: 파티클 스폰, 점수 계산 등
UE_LOG(LogTemp, Log,
TEXT("Entity died at %s"),
*Transforms[i].Transform.GetLocation().ToString());
// 엔티티 파괴 예약
Ctx.Defer().DestroyEntity(Entities[i]);
}
});
}
Add 트리거: 초기화, 시각 이펙트 스폰, 사운드 재생
Remove 트리거: 리소스 정리, 메모리 해제, 통계 업데이트
핵심 요약
- UMassProcessor는 상태 없는 로직 클래스로, ConfigureQueries()와 Execute()를 오버라이드한다
- 6개의 ProcessingPhase가 매 틱 순서대로 실행된다: PrePhysics ~ FrameEnd
- ExecutionFlags로 Server/Client/Standalone 환경별 실행을 제어한다
bRequiresGameThreadExecution은 UObject 접근이 필요할 때만 true로 설정한다- UMassObserverProcessor는 Fragment/Tag 추가/제거 이벤트에 반응하는 특수 Processor이다
- Processor는 데이터를 소유하지 않으며, Query를 통해 필요한 Fragment에만 접근한다
도전 과제
배운 내용을 직접 실습해보세요
UMassProcessor를 상속받아 이동 Processor를 만드세요. ConfigureQueries에서 FTransformFragment(ReadWrite)와 FVelocityFragment(ReadOnly)를 요구하고, Execute에서 Transform += Velocity * DeltaTime을 계산하세요.
UMassObserverProcessor를 상속받아 특정 Fragment 추가/제거를 감지하는 Processor를 만드세요. FDeadTag가 추가될 때 트리거되어 사망 처리 로직을 실행하도록 구현하세요.
bRequiresGameThreadExecution=false로 설정하여 멀티스레드에서 실행되는 Processor를 작성하세요. Thread-safe한 데이터 접근 패턴을 확인하고, 동일 Processor의 싱글/멀티스레드 성능을 비교하세요.