Mass Framework in Unreal Engine
Visualization with Mass Framework in Unreal Engine
<- 일본 MassFramework 발표 (CEDEC2022)
매스 플러그인 개요
용어 : ECS = EntityComponentSystem. 기존에 존재하는 아키텍처. 데이터드리븐 방식 엔진에 사용. 언리얼은 모든 객체가 AActor 파생이라 액터 수가 많아질수록 엄청난 병목이 생긴다는 단점. 기본 ECS와 매스ECS에 용어차이가 있음.
EQS = EnvironmentQuerySystem. AI들에 사용. 매스에서도 동일한 용어.
매스엔티티 - 프레임워크, 베이직 서브시스템, 스케줄링, 컴포지션 툴(Trait. 트레잇은 프래그먼트와 프로세서를 제공하기 위한 이름)
데이터중심. 기본 구조는 Fragment. 아토믹 데이터 조각. ex)Transform, Velocity, LODIndex. 프래그먼트 컬렉션으로 그룹화, 컬렉션 인스턴스는 엔티티라고 함. 엔티티는 프래그먼트 구성만으로 포함. 논리가 없는 데이터 전용 요소.
동일한 구성의 엔티티 집합은 아키타입이라 함. 아키타입은 메모리 청크에 저장(검색 빠르게하기위해.)
서브시스템은 매스엔티티 프레임워크를 나눈것. 월드서브시스템이므로 월드수명 종속. 매스엔티티 매니저는 엔티티 아키타입 생성 및 호스팅. 프래그먼트 추가,제거 등 작업 인터페이스 역할. 아키타입간에 엔티티 이동 등. 다른 서브시스템은 MassCommandBuffer 이용해서 비동기적으로 호출 가능
엔티티 탬플릿은 에디터에서 생성된 MassEntityConfig 에셋의 데이터에서 생성. 이 에셋은 엔티티 생성하는 도중에 엔티티에 새로운 Trait 세트 선언 가능. 부모에셋을 가질 수 있고 여기서 특성을 상속 가능.
Trait은 프래그먼트,프로세서의 총칭. 엔티티에 여러 Trait 인스턴스 추가 가능. Trait의 예시는 회피, LookAt, StateTree 등.
매스게임플레이 - 매스엔티티 플러그인을 확장. 월드 내 리프레젠테이션, 스포닝, 무브먼트, 리플리케이션, LOD매커니즘, 스테이트트리
서브시스템
Mass Representation(표현)
Mass Representation SubSystem은 매스엔티티의 여러 비주얼 요소를 매니징.
LOD에 따라
- HighRes Actor
- LowRes Actor
- ISM
- No Representation
이중 ISM이 가장 저렴. 버텍스 애니메이션 사용해서 이동과 애니메이션 처리.
Mass Representation 서브시스템은 각 표현 타입 전환을 핸들링하고, MassActorSpawner, MassLOD 서브시스템과 직접적으로 작동함. 또한 이 서브시스템은 스폰된 액터를 자동으로 재활용, 풀링 처리함.
매스 스포너 서브시스템은 매스스포너 오브젝트와 프로시저럴 호출을 통해 엔티티 스폰 및 관리함. 매스스포너 서브시스템은 사용가능한 엔티티 탬플릿에 대한 정보를 호스팅하는 매스엔티티 탬플릿 레지스트리 인스턴스를 소유함.
이 서브시스템을 사용하려면 매스스포너 액터를 생성해 레벨에 배치. 매스엔티티 데피니션 에셋을 추가해 어떤 액터 타입을 스폰할지 지정, Mass Entity Distribution Instance Generator를 추가하여 해당 액터를 어디에 스폰할지 지정 가능.
매스LOD 서브시스템은 각 매스엔티티에 필요한 LOD를 계산. High/Med/Low/Off 네가지 LOD 출력. 각 LOD레벨에 대해 거리, 최대 수를 구성 가능. 이 서브시스템의 클라이언트는 세가지 시스템
- Mass (Representation/Visualization) LOD -> 시각적 LOD 처리용. 디스턴스컬링, 프러스텀컬링 처리. 엔티티가 프러스텀 내 여부와 관계없이 다른 LOD 디스턴스 제공 가능.
- MassSimulationLOD -> 모든 엔티티 계산의 부하 분산. 모든 엔티티를 동일 LOD를 가지는 청크로 그룹화. 쿼리에 필터 사용시 유용. 모든 계산에 대해 가변 주기 업데이트 옵션도 있음.
- MassReplicationLOD -> 네트워크
매스 스테이트트리 서브시스템은 스테이트트리 시스템을 매스엔티티와 통합. 각 엔티티에 대한 스테이트 트리 구성, 다른 매스시스템 시그널에 따라 각 엔티티의 스테이트트리 업데이트. 해당 로직은 엔티티에 대한 데이터를 구성, 설정에만 사용, 이후 동작은 비헤이비어에 맞게 알아서 작동.
매스 무브먼트 서브시스템은 매스 에이전트를 위한 간단한 이동 모델 정의. 프래그먼트와 프로세서는 다른 트레잇이 속도나 힘을 직접 수정할 수 있도록 설정됨. 이 값들은 매스 에이전트가 사용하는 최종 이동 값과 결합됨.
ex) 에이전트 조종시 시스템은 초기 스티어링 포스 설정, 회피 기능을 통해 이동중에 충돌 회피를 위해 스티어링 포스를 변경 가능
매스 스마트 오브젝트 서브시스템. 스마트오브젝트 시스템을 매스엔티티와 통합. 스마트 오브젝트 쿼리 수행, 매스엔티티 에이전트에서 간단한 비헤이비어 실행에 필요한 트레잇, 프래그먼트, 프로세서 제공
매스AI - AI피처 제공을 위해 매스게임플레이 플러그인을 확장. 네비게이션, 스마트오브젝트, AIBehaviors, Animation, 존그래프
매스크라우드 -> 시티샘플 프로젝트의 크라우드와 트래픽 비헤이비어를 위해 작성됨. 매스AI를 확장
매스 어보이던스 (실험적) - 매스엔티티와 통합된 힘 기반 시스템. UMassMovingAvoidanceProcessor 로 입력 처리, FMassForceFragment로 스티어링 포스 출력. 이 출력은 엔티티를 다른 엔티티에서 멀어지게 하는데 사용.
인풋
FMassForceFragment - 엔티티를 다른 엔티티로부터 멀어지게 하기 위해 엔티티에 적용되는 힘
FMassNavigationEdgesFragment - 엔티티가 회피하는데 사용되는 모서리(벽) 목록 제공
FMassMoveTargetFragment - 엔티티의 타겟 목적지. 앞으로의 방향과 현재 상태 (정지, 이동 등)과 같은 추가 정보 포함
FDataFragment_Transform - 엔티티의 위치
FMassVelocityFragment - 엔티티의 속도
FDataFragment_AgentRadius - 엔티티의 반경
아웃풋
FMassForceFragment The final steering force applied to the Entity.
디테일한 정보와 디버깅 방법은
매쉬 / 매스에서의 시각화
매스 프레임워크는 스켈메시와 스태틱메시와 함께 작동. 따라서 메시 자체와 시각적/기술적 차이는 없음. 스태틱 메시가 있는 인스턴스화된 엔티티와 네가 원하는 메시가 있는 표준 액터(매스에 사용되는 MassAgent 컴포넌트용으로 확장됨)가 있음. 차이점은 씬에 존재하는 일반적인 형태와 동작 제어임.
- 일반 액터 : 매스 구현이 없음. 자체 논리가 있음.
- 매스 컨트롤드 액터 : BP_Actor이고 매스 스포너나 파생클래스에서 스폰됨. 각 멤버에 대한 BP_Actor 개별 표현이 있음. BP_Actor는 자체 기능이 없고 어태치된 매스에이전트 컴포넌트에 의해 조종되고, 특히 EntityConfig파일 (DataAsset)을 통해 제어됨
씬의 표준 액터이기 때문에 MassAgent와 다른 컴포넌트가 붙어있을 뿐이며, 스켈레탈 메시나 스태틱 메시를 기반으로 할 수도 있다.
- 매스 엔티티 : 매스스포너(또는 파생클래스)를 통해 스폰된 오브젝트. 하나의 오브젝트(MassVisualizer 액터(MassRepresentationSubsystemVisualizer)) 안에 위치함. 그러나 여전히 상당히 독립적으로 움직일 수 있음.
일반적으로 가장 가까운 캐릭터는 완전히 리깅되고 애니메이션 되는 메타휴먼 캐릭터 (매스컨트롤드액터), 그리고 좀 더 먼곳은 메타휴먼에서 생성된 사용자 지정 버텍스 애니메이티드 스태틱메시 (매스엔티티)
요약 : 매스액터 - 독립적 BP_Character로 표현 , 매스 엔티티는 매스 비주얼라이저로 표현. 매스엔티티와 매스액터는 카메라와 거리에 따라 스위칭. 플레이어나 사물에 블로킹 비헤이비어를 넣으면 알아서 멈추고 회피해감.
- 나이아가라 매스와의 차이 : 나이아가라에는 AI가 없음. 간단한 보이드 구현으로 군집 패턴을 구현하는 정도. 반면 매스AI는 StateTree로 로직 구현이 가능.
AI
전체 씬의 동작은 MassEntityConfig 에셋을 통해 구성됨.
해당 에셋을 기반으로 매스스포너가 메스 엔티티들과 매스컨트롤드 액터(매스 에이전트)를 씬에 스폰
매스에이전트는 특정 존그래프와 스테이트트리를 기반으로 동작. 스마트오브젝트와 상호작용도 가능.
ECS
일반 ECS 용어와 다름. 유니티가 ECS 프레임워크에 대한 특허를 가져서 그럼
norm ECS Mass ECS User's perspective ECS explanation
Entity Entity Entities = Indexes of data
Component Fragment Components = the Data itself
System Processor Systems = Behaviors definition
이외에도 Trait, Tag, 서브시스템 등의 용어가 다름.
데이터지향 설계 아키텍처이므로
Mass Framework in Unreal Engine 스키마 참고할것.
Trait 컴포넌트와 유사개념(로직 없음) <- Fragment,Tag 멤버 데이터와 유사.
Fragment 집합인 아키타입이 있고 엔티티는 단순히 아키타입에서의 ID만을 가진다.
쿼리는 아키타입의 일부를 사용해서 필터링 후 엔티티를 프로세서로 던진다.
매스는 엔티티라 불리는 개별 에이전트를 관리. 엔티티는 가벼운 버전의 액터라 볼 수 있음.
엔티티는 Trait의 컬렉션을 가짐. Trait은 가벼운 버전의 Component임.
엔티티나 Trait 모두 함수성을 추가할 방법이 없으므로 엔티티-액터 트레잇-컴포넌트와의 비교가 완벽하지는 않지만 구조 이해에는 도움이 됨.
엔티티는 신원만 정의, 트레잇은 데이터만 정의.
엔티티는 트레잇 리스트로 정의되며 이 리스트를 아키타입이라 함.
실제 데이터는 Fragment에 저장되고 이것은 작은 버전의 UStruct임. Fragments는 주어진 기능의 상태를 정의함(컴포넌트의 데이터와 같은 개념)
각 Fragmented된 상태의 실제 기능, 평가 및 변경은 processors에서 구현됨.(컴포넌트에 UStruct로 데이터가 저장되고 프로세서에서 동작하는 개념)
프로세서는 조작할 수 있는 데이터를 사용하는 엔티티를 필터링하고, 같은 유형의 엔티티 조각을 한꺼번에 묶어 제공하므로 같은 종류의 데이터를 순차적이고 연속적인 블록으로 조작할 수 있어 캐시 누락이 발생할 가능성이 적다.
프래그먼트는 계산에 사용되는 아토믹 데이터 요소(Struct). 위치,속도,LODIndex 등의 TArray로 볼 수 있음.
프래그먼트는 컬렉션으로 그룹화 되며 이런 컬렉션 인스턴스는 ID와 연관됨. 이 컬렉션의 인스턴스를 엔티티라 함.
FMassFragment
단일 엔티티를 위한 UStruct임. Mass Entity Config의 Trait에 넣을 수 있음.
FMassSharedFragment
여러 엔티티간 데이터 공유용. LOD 리플리케이션 등의 용도. 집합을 묶어서 동일 처리할때 좋을듯(10명 내외의 무리를 동일한 선상에서 핸들링, 작은 이벤트를 재생하거나 동일 몽타주를 재생하거나..)
커스텀 프래그먼트를 첨부하려면 MassEntityConfig의 AssortedFragments 리스트에 넣으면 됨.
점유 경로 프래그먼트.
빌트인 목록은 아래와 같음
Transform Fragment MassGameplay (MassCommonFragments.h)
Mass Actor Fragment MassGameplay (MassActorSubsystem.h)
Fragment to save the actor pointer of a mass entity if it exist
Agent Radius Fragment MassGameplay (MassCommonFragments.h)
Other fragments used in the source code
FMassViewerInfoFragment (MassGameplay)
FMassRepresentationFragment (MassGameplay)
FMassRepresentationLODFragment (MassGameplay)
FMassVisualizationChunkFragment (MassGameplay)
FMassRepresentationSubsystemSharedFragment (MassGameplay)
FMassVelocityFragment (MassGameplay)
FMassActorFragment (MassGameplay)
Trait은 컴포넌트와 유사개념. UCLASS임. 태그, 프래그먼트를 멤버로 가짐.
Mass에서의 Trait은 특정 기능을 제공하는 Fragment와 Processor의 총칭.
Trait의 추가나, Trait의 멤버 Fragment 추가는 MassEntityConfigDataAsset에서 가능하다.
기본 트레잇
MassCrowd의 시각화
Visualization with Mass Framework in Unreal Engine 를 참고할것.
- 크라우드 시각화
Traits->index[0]에 CrowdVisualization이 있음.
CrowdVisualization-> StaticMeshInstanceDesc->Meshes[0]->Mesh = 스태틱메시 Simplified.
CrowdVisualization-> HighResTemplateActor = BP_CrowdCharacter 고해상
CrowdVisualization-> LowResTemplateActor = BP_CrowdCharacter 저해상
CrowdVisualization-> LODParams (디스턴스 등 세팅)
(MassCrowdAgentConfig_MetaHuman은 다른 Trait과 연관된 MassCrowdAgentConfig를 확장한다는 점에 유의)
이 중, LODParams의 High/Med/Low/Off의 경우 아래와 같이 정의됨. 여러군데에서 씀.
UMassDistanceVisualizationTrait::UMassDistanceVisualizationTrait()
{
RepresentationSubsystemClass = UMassRepresentationSubsystem::StaticClass();
Params.RepresentationActorManagementClass = UMassRepresentationActorManagement::StaticClass();
Params.LODRepresentation[EMassLOD::High] = EMassRepresentationType::HighResSpawnedActor;
Params.LODRepresentation[EMassLOD::Medium] = EMassRepresentationType::LowResSpawnedActor;
Params.LODRepresentation[EMassLOD::Low] = EMassRepresentationType::StaticMeshInstance;
Params.LODRepresentation[EMassLOD::Off] = EMassRepresentationType::None;
LODParams.LODDistance[EMassLOD::High] = 0.f;
LODParams.LODDistance[EMassLOD::Medium] = 1000.f;
LODParams.LODDistance[EMassLOD::Low] = 2500.f;
LODParams.LODDistance[EMassLOD::Off] = 10000.f;
LODParams.BufferHysteresisOnDistancePercentage = 10.0f;
bAllowServerSideVisualization = false;
}
UMassCrowdVisualizationTrait::UMassCrowdVisualizationTrait()
{
// Override the subsystem to support parallelization of the crowd
RepresentationSubsystemClass = UMassCrowdRepresentationSubsystem::StaticClass();
Params.RepresentationActorManagementClass = UMassCrowdRepresentationActorManagement::StaticClass();
Params.LODRepresentation[EMassLOD::High] = EMassRepresentationType::HighResSpawnedActor;
Params.LODRepresentation[EMassLOD::Medium] = EMassRepresentationType::LowResSpawnedActor;
Params.LODRepresentation[EMassLOD::Low] = EMassRepresentationType::StaticMeshInstance;
Params.LODRepresentation[EMassLOD::Off] = EMassRepresentationType::None;
// Set bKeepLowResActor to true as a spawning optimization, this will keep the low-res actor if available while showing the static mesh instance
Params.bKeepLowResActors = true;
Params.bKeepActorExtraFrame = true;
Params.bSpreadFirstVisualizationUpdate = false;
Params.WorldPartitionGridNameContainingCollision = NAME_None;
Params.NotVisibleUpdateRate = 0.5f;
LODParams.BaseLODDistance[EMassLOD::High] = 0.f;
LODParams.BaseLODDistance[EMassLOD::Medium] = 500.f;
LODParams.BaseLODDistance[EMassLOD::Low] = 1000.f;
LODParams.BaseLODDistance[EMassLOD::Off] = 5000.f;
LODParams.VisibleLODDistance[EMassLOD::High] = 0.f;
LODParams.VisibleLODDistance[EMassLOD::Medium] = 1000.f;
LODParams.VisibleLODDistance[EMassLOD::Low] = 5000.f;
LODParams.VisibleLODDistance[EMassLOD::Off] = 10000.f;
LODParams.LODMaxCount[EMassLOD::High] = 10;
LODParams.LODMaxCount[EMassLOD::Medium] = 20;
LODParams.LODMaxCount[EMassLOD::Low] = 500;
LODParams.LODMaxCount[EMassLOD::Off] = TNumericLimits<int32>::Max();
LODParams.BufferHysteresisOnDistancePercentage = 20.0f;
LODParams.DistanceToFrustum = 0.0f;
LODParams.DistanceToFrustumHysteresis = 0.0f;
LODParams.FilterTag = FMassCrowdTag::StaticStruct();
}
기본값은 위와 같지만 매스엔티티configAsset에서 재정의 가능하다. 만약 High,Med가 필요없고 전부 Low만 필요하다면 거리를 설정하고 High,Low를 None으로 설정하면 된다.
High 슬롯에 ISM을 넣는것도 가능하나 반드시 티가 나기 때문에 High에 Med액터를 쓰고, Med를 생략, Low에 ISM을 써도 된다.
매스를 반드시 캐릭터 군중에만 사용할 필요는 없다. 건물 시뮬레이션, 카메라가 에이전트에 가까이 가지 않는다면 High,LOWRes탬플릿액터를 None으로, ISM만 설정한 후, LOD High,Mid=None, Low=ISM으로 ISM만 사용하는 방법도 있다.
추가적인 프래그먼트 중에 아래 두개가 유용해보임. 뭐하는놈인지는 찾아봐야됨
UMassDistanceVisualizationTrait::UMassDistanceVisualizationTrait()
{
RepresentationSubsystemClass = UMassRepresentationSubsystem::StaticClass();
Params.RepresentationActorManagementClass = UMassRepresentationActorManagement::StaticClass();
Params.LODRepresentation[EMassLOD::High] = EMassRepresentationType::HighResSpawnedActor;
Params.LODRepresentation[EMassLOD::Medium] = EMassRepresentationType::LowResSpawnedActor;
Params.LODRepresentation[EMassLOD::Low] = EMassRepresentationType::StaticMeshInstance;
Params.LODRepresentation[EMassLOD::Off] = EMassRepresentationType::None;
LODParams.LODDistance[EMassLOD::High] = 0.f;
LODParams.LODDistance[EMassLOD::Medium] = 1000.f;
LODParams.LODDistance[EMassLOD::Low] = 2500.f;
LODParams.LODDistance[EMassLOD::Off] = 10000.f;
LODParams.BufferHysteresisOnDistancePercentage = 10.0f;
bAllowServerSideVisualization = false;
}
UMassCrowdVisualizationTrait::UMassCrowdVisualizationTrait()
{
// Override the subsystem to support parallelization of the crowd
RepresentationSubsystemClass = UMassCrowdRepresentationSubsystem::StaticClass();
Params.RepresentationActorManagementClass = UMassCrowdRepresentationActorManagement::StaticClass();
Params.LODRepresentation[EMassLOD::High] = EMassRepresentationType::HighResSpawnedActor;
Params.LODRepresentation[EMassLOD::Medium] = EMassRepresentationType::LowResSpawnedActor;
Params.LODRepresentation[EMassLOD::Low] = EMassRepresentationType::StaticMeshInstance;
Params.LODRepresentation[EMassLOD::Off] = EMassRepresentationType::None;
// Set bKeepLowResActor to true as a spawning optimization, this will keep the low-res actor if available while showing the static mesh instance
Params.bKeepLowResActors = true;
Params.bKeepActorExtraFrame = true;
Params.bSpreadFirstVisualizationUpdate = false;
Params.WorldPartitionGridNameContainingCollision = NAME_None;
Params.NotVisibleUpdateRate = 0.5f;
LODParams.BaseLODDistance[EMassLOD::High] = 0.f;
LODParams.BaseLODDistance[EMassLOD::Medium] = 500.f;
LODParams.BaseLODDistance[EMassLOD::Low] = 1000.f;
LODParams.BaseLODDistance[EMassLOD::Off] = 5000.f;
LODParams.VisibleLODDistance[EMassLOD::High] = 0.f;
LODParams.VisibleLODDistance[EMassLOD::Medium] = 1000.f;
LODParams.VisibleLODDistance[EMassLOD::Low] = 5000.f;
LODParams.VisibleLODDistance[EMassLOD::Off] = 10000.f;
LODParams.LODMaxCount[EMassLOD::High] = 10;
LODParams.LODMaxCount[EMassLOD::Medium] = 20;
LODParams.LODMaxCount[EMassLOD::Low] = 500;
LODParams.LODMaxCount[EMassLOD::Off] = TNumericLimits<int32>::Max();
LODParams.BufferHysteresisOnDistancePercentage = 20.0f;
LODParams.DistanceToFrustum = 0.0f;
LODParams.DistanceToFrustumHysteresis = 0.0f;
LODParams.FilterTag = FMassCrowdTag::StaticStruct();
}
프로세서
생성된 모든 프로세서는 기본적으로 자동 실행되므로 실행을 위해 어디에 연결/정의할 필요가 없다.
프로세서 유형
- UMassProcessor (각 프레임 처리 ) - 업데이트용
- UMassObserverProcessor (EMassObservedOperation에서 처리) - 값 설정용
UMassProcessor
초기화 시 Mass는 실행 규칙을 사용하여 프로세서의 종속성 그래프를 생성하여 순서대로 실행(예제에서는 UMyProcessor에서 Execute를 호출하기 전에 MSMovementProcessor로 엔티티를 이동해야 한다).
// Processor Constructor setup
UMyProcessor::UMyProcessor()
{
// This processor is registered with mass by just existing! This is the default behaviour of all processors.
bAutoRegisterWithProcessingPhases = true;
// Setting the processing phase explicitly
ProcessingPhase = EMassProcessingPhase::PrePhysics;
// Using the built-in movement processor group
ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::Movement;
// You can also define other processors that require to run before or after this one
ExecutionOrder.ExecuteAfter.Add(TEXT("MSMovementProcessor"));
// This executes only on Clients and Standalone
ExecutionFlags = (int32)(EProcessorExecutionFlags::Client | EProcessorExecutionFlags::Standalone);
// This processor should not be multithreaded
bRequiresGameThreadExecution = true;
}
생성자, 틱 그룹, 스레드 설정 (기본 멀티스레드. 단일스레드로 설정하려면 bRequiresGameThreadExecution true로)
Processing phases
UMassProcessor 에서 파생된 언리얼 클래스는 Mass 에 자동 등록되어 기본적으로 EMassProcessingPhase::PrePhysics 처리 단계에 추가됨.
각 EMassProcessingPhase 는 기본적으로 프로세서가 주어진 처리 단계의 모든 프레임을 틱한다는 의미의 ETickingGroup 과 관련됨.
UMassSimulationSubsystem에 생성 및 등록할 수도 있지만, 일반적인 경우는 새 타입을 생성하는 것.
사용자는 UMassProcessor에 포함된 ProcessingPhase 변수를 수정하여 프로세서가 어느 처리 단계에 속할지 구성할 수 있습니다:
EMassProcessingPhase Related ETickingGroup Description
PrePhysics TG_PrePhysics Executes before physics simulation starts.
StartPhysics TG_StartPhysics Special tick group that starts physics simulation.
DuringPhysics TG_DuringPhysics Executes in parallel with the physics simulation work.
EndPhysics TG_EndPhysics Special tick group that ends physics simulation.
PostPhysics TG_PostPhysics Executes after rigid body and cloth simulation.
FrameEnd TG_LastDemotable Catchall for anything demoted to the end.
서브시스템 데이터를 읽기/쓰기 바인딩 필수. EMassFragmentPresence(필터링) 설정도 필수.
쿼리(FMassEntityQuery)는 쿼리 구성(ConfigureQueries)에 따라 프래그먼트와 태그로 필터링해서 반복작업함.
프로세서는 여러 FMassEntityQuery를 정의할 수 있으며, 프로세서의 헤더에 정의된 여러 쿼리에 규칙을 추가하기 위해 ConfigureQueries를 재정의해야 함:
void UMyProcessor::ConfigureQueries()
{
// Entities must have an FTransformFragment and we are reading and writing it (EMassFragmentAccess::ReadWrite)
MyQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
// Entities must have an FMassForceFragment and we are only reading it (EMassFragmentAccess::ReadOnly)
MyQuery.AddRequirement<FMassForceFragment>(EMassFragmentAccess::ReadOnly);
// Entities must have a common FClockSharedFragment that can be read and written
MyQuery.AddSharedRequirement<FClockSharedFragment>(EMassFragmentAccess::ReadWrite);
// Entities must have a UMassDebuggerSubsystem that can be read and written
MyQuery.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite);
MyQuery.AddTagRequirement<FMoverTag>(EMassFragmentPresence::All);
MyQuery.AddRequirement<FHitLocationFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::Optional);
MyQuery.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite); // This line is duplicated from a few lines above
// Register Queries by calling RegisterWithProcessor passing the processor as a parameter.
MyQuery.RegisterWithProcessor(*this);
ProcessorRequirements.AddSubsystemRequirement<UMassDebuggerSubsystem>(EMassFragmentAccess::ReadWrite);
}
ProcessorRequirements 는 쿼리 범위 밖에서 Execute 함수에서 액세스하는 모든 UWorldSubsystem 을 보유하는 UMassProcessor 의 특수 쿼리 스코프.
예제에서 UMassDebuggerSubsystem은 MyQuery의 스코프(MyQuery.AddSubsystemRequirement)와 Execution 함수 스코프(ProcessorRequirements.AddSubsystemRequirement) 에서 액세스됨.
참고: 쿼리는 프로세서 외부에서도 생성 및 반복할 수 있다.
Access requirements
EMassFragmentAccess Description
None No binding required.
ReadOnly We want to read the data for the fragment/subsystem.
ReadWrite We want to read and write the data for the fragment/subsystem.
Presence requirements
EMassFragmentPresence Description
All All of the required fragments/tags must be present. Default presence requirement.
Any At least one of the fragments/tags marked any must be present.
None None of the required fragments/tags can be present.
Optional If fragment/tag is present we'll use it, but it does not need to be present.
프로세서는 Excute 함수 내에서 쿼리를 실행:
쿼리는 람다로 ForEachEntityChunk 멤버 함수를 호출하고 관련 FMassEntityManager 및 FMassExecutionContext를 전달하여 실행.
void UMyProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
//Note that this is a lambda! If you want extra data you may need to pass it in the [] operator
MyQuery.ForEachEntityChunk(EntityManager, Context, [](FMassExecutionContext& Context)
{
//Loop over every entity in the current chunk and do stuff!
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// ...
}
});
}
즉, 필터링 후 엔티티 인덱스로 식별해서 Excute 함수에서 ForEachEntityChunk를 타는데, 람다를 사용하고 FMassEntityManager 에 FMassExecutionContext를 전달해서 실행.
이때, 인덱스는 시간에 따라 변할 수 있음! 청크별로 도는데, 현재 프레임 특정 청크에 있는 특정 인덱스가 있는 엔티티가 다음 프레임에서는 다른 청크에 다른 인덱스를 가지고 있을 수 있음.
따라서 인덱스를 저장하는 방법은 불가능. 매번 필터링 후 반복 실행만 가능.
여기도 읽기 쓰기 제어자 있음.
EMassFragmentAccess Type Function Description
ReadOnly Fragment GetFragmentView Returns a read only TConstArrayView containing the data of our ReadOnly fragment.
ReadWrite Fragment GetMutableFragmentView Returns a writable TArrayView containing de data of our ReadWrite fragment.
ReadOnly Shared Fragment GetConstSharedFragment Returns a constant reference to our read only shared fragment.
ReadWrite Shared Fragment GetMutableSharedFragment Returns a reference of our writable shared fragment.
ReadOnly Subsystem GetSubsystemChecked Returns a read only constant reference to our world subsystem.
ReadWrite Subsystem GetMutableSubsystemChecked Returns a reference of our writable shared world subsystem.
프로세서는 커스텀으로 추가하기 나름.
위치 업데이트 예시
// .h file
UCLASS()
class UAdvancedRandomMovementProcessor : public UMassProcessor
{
GENERATED_BODY()
public:
UAdvancedRandomMovementProcessor();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context) override;
private:
FMassEntityQuery EntityQuery;
};
// .cpp file
// Constructor
UAdvancedRandomMovementProcessor::UAdvancedRandomMovementProcessor()
{
bAutoRegisterProcessingPhases = true; // Note: Correct member name might be bAutoRegisterWithProcessingPhases
ExecutionFlags = (int32)EProcessorExecutionFlags::All;
ExecutionOrder.ExecuteBefore.Add(UE::Mass::ProcessorGroupNames::Avoidance);
}
void UAdvancedRandomMovementProcessor::ConfigurateQueries() // Note: Typo in original, should be ConfigureQueries
{
EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadOnly);
EntityQuery.AddRequirement<FMassMoveTargetFragment>(EMassFragmentAccess::ReadWrite);
EntityQuery.AddConstSharedRequirement<FMassMovementParameters>(EMassFragmentPresence::All);
// EntityQuery.RegisterWithProcessor(*this); // Usually queries are registered in ConfigureQueries
}
// Execute is going to be executed just one time at the beginning. // This comment might be misleading, Execute runs based on processor group and phase.
void UAdvancedRandomMovementProcessor::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context)
{
// Attaching variables with ReadWrite permissions
const TArrayView<FMassMoveTargetFragment> NavTargetsList = Context.GetMutableFragmentView<FMassMoveTargetFragment>();
// Attaching variables with ReadOnly permissions (no need of Mutable)
// const TConstArrayView<FOccupantTrajectoryFragment> TrajectoryDataArr = Context.GetFragmentView<FOccupantTrajectoryFragment>(); // This fragment is not in the query
const TConstArrayView<FTransformFragment> TransformsList = Context.GetFragmentView<FTransformFragment>();
const FMassMovementParameters& MovementParams = Context.GetConstSharedFragment<FMassMovementParameters>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
const FTransform& Transform = TransformsList[EntityIndex].GetTransform();
FMassMoveTargetFragment& MoveTarget = NavTargetsList[EntityIndex];
FVector CurrentLocation = Transform.GetLocation();
FVector TargetVector = MoveTarget.Center - CurrentLocation;
TargetVector.Z = 0.f; // Keep movement on the XY plane
MoveTarget.DistanceToGoal = (TargetVector).Size();
MoveTarget.Forward = (TargetVector).GetSafeNormal(); // Corrected from (TTargetVector)
if(MoveTarget.DistanceToGoal <= 20.f || MoveTarget.Center == FVector::ZeroVector)
{
MoveTarget.Center = FVector(FMath::RandRange(-1.f, 1.f) * 1000.f, FMath::RandRange(-1.f, 1.f) * 1000.f, CurrentLocation.Z);
// Recalculate DistanceToGoal and Forward after updating Center
FVector NewTargetVector = MoveTarget.Center - Transform.GetLocation();
NewTargetVector.Z = 0.f;
MoveTarget.DistanceToGoal = NewTargetVector.Size();
MoveTarget.Forward = NewTargetVector.GetSafeNormal();
MoveTarget.DesiredSpeed = FMassInt16Real(MovementParams.DefaultDesiredSpeed);
}
}
}));
}
UMassObserverProcessor (값 세팅용 프로세서)
UMassObserverProcessor 는 다른 Fragment의 정보에 의존하는 Fragment나 생성 시점에 초기화되지 않을 수 있는 다른 시스템을 초기화하는 데 도움이 된다.
UMassObserverProcessor는 관찰된 Fragment/Tag에 대해 바로 직전 EMassObservedOperation을 수행한 엔티티에 대해 동작함.
EMassObservedOperation종류 Add,Remove가 있음.
EMassObservedOperation Description
Add The observed Fragment/Tag was added to an entity.
Remove The observed Fragment/Tag was removed from an entity.
색 변경 예시
UMSObserverOnAdd::UMSObserverOnAdd()
{
ObservedType = FSampleColorFragment::StaticStruct();
Operation = EMassObservedOperation::Add;
ExecutionFlags = (int32)(EProcessorExecutionFlags::All);
}
void UMSObserverOnAdd::ConfigureQueries()
{
EntityQuery.AddRequirement<FSampleColorFragment>(EMassFragmentAccess::ReadWrite);
// EntityQuery.RegisterWithProcessor(*this); // Observers typically don't need to register queries this way, the system handles it based on ObservedType.
}
void UMSObserverOnAdd::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntityManager, Context, [&,this](FMassExecutionContext& Context)
{
auto Colors = Context.GetMutableFragmentView<FSampleColorFragment>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
// When a color is added, make it random!
Colors[EntityIndex].Color = FColor::MakeRandomColor();
}
});
}
Entity Manager Observer calls
이 글을 쓰는 시점에서 옵저버는 이러한 특정 엔티티 작업 중에만 매스 매니저가 직접 트리거 한다.
이는 주로 addfragmenttoentity와 같은 일부 특정 단일 엔티티 수정 함수 때문에 발생.
Entity changes in the entity manager:
FMassEntityManager::BatchBuildEntities
FMassEntityManager::BatchCreateEntities
FMassEntityManager::BatchDestroyEntityChunks
FMassEntityManager::AddCompositionToEntity_GetDelta
FMassEntityManager::RemoveCompositionFromEntity
FMassEntityManager::BatchChangeTagsForEntities
FMassEntityManager::BatchChangeFragmentCompositionForEntities
FMassEntityManager::BatchAddFragmentInstancesForEntities
태그
필터링에 사용
필터링 전
필터링 후
필터링 방식은 아래와 같다.
FMassTag 파생해서 생성. Trait에 추가해서 사용도 가능하고, 직접 엔티티ConfigAsset에 추가하는것도 가능
특정 엔티티를 필터링하는 주요 예시가 바로 LOD 시스템.
실행중에 동적으로 태그 추가/제거는 아래 함수 사용. (이를 엔티티 뮤테이션이라 함. 변조했다는 뜻)
Context.Defer().AddTag<FSomeTag>(Context.GetEntity(i));
Context.Defer().RemoveTag<FSomeTag>(Context.GetEntity(i));
다른 방법들도 있음. 아래 헤더에 정의됨
Plugins/Runtime/MassEntity/Source/MassEntity/Public/MassCommandBuffer.h.
엔티티 뮤테이션을 지연하려면 엔티티 핸들FMassEntityHandle 을 얻을 필요가 있다. FMassExecutionContext에 엔티티 핸들 배열이 있으니 액세스
Singular FMassEntityHandle EntityHandle = Context.GetEntity(EntityIndex);
Plural auto EntityHandleArray = Context.GetEntities();
정리 및 군중 체력 소진으로 죽는 로직 예시
- ConfigQueries에서 쿼리 조건 설정 FDead 태그가 있으면 필터링에서 제외.
- ForEachEntityChunk로 현재 실행 컨텍스트에 엑세스 -> FMassExecutionContext로 엔티티 데이터 가져오고 구성 변경 -> Health <=0인지 검사 -> FDead 태그 추가
void UDeathProcessor::ConfigureQueries()
{
// All the entities processed in this query must have the FHealthFragment fragment
DeclareDeathQuery.AddRequirement<FHealthFragment>(EMassFragmentAccess::ReadOnly, EMassFragmentPresence::All);
// Entities processed by this queries shouldn't have the FDead tag, as this query adds the FDead tag
DeclareDeathQuery.AddTagRequirement<FDead>(EMassFragmentPresence::None);
DeclareDeathQuery.RegisterWithProcessor(*this);
}
void UDeathProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context)
{
DeclareDeathQuery.ForEachEntityChunk(EntityManager, Context, [&,this](FMassExecutionContext& Context)
{
auto HealthList = Context.GetFragmentView<FHealthFragment>();
for (int32 EntityIndex = 0; EntityIndex < Context.GetNumEntities(); ++EntityIndex)
{
if(HealthList[EntityIndex].Health <= 0.f)
{
// Adding a tag to this entity when the deferred commands get flushed
FMassEntityHandle EntityHandle = Context.GetEntity(EntityIndex);
Context.Defer().AddTag<FDead>(EntityHandle);
}
}
});
}
소스파일 구조
엔티티, 프래그먼트, 태그, 프로세서 및 기타 매스 클래스는 하나의 파일 내에서 정의, 선언이 가능
#include "CoreMinimal.h"
#include "CrowdCharacterDefinition.h"
#include "MassEntityTypes.h"
#include "MassObserverProcessor.h"
#include "CrowdVisualizationFragment.generated.h"
struct FMassEntityQuery;
USTRUCT()
struct CITYSAMPLE_API FCitySampleCrowdVisualizationFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category = "")
FCrowdVisualizationID VisualizationID;
UPROPERTY(EditAnywhere, Category = "")
uint32 TopColor = 0;
UPROPERTY(EditAnywhere, Category = "")
uint32 BottomColor = 0;
UPROPERTY(EditAnywhere, Category = "")
uint32 ShoesColor = 0;
UPROPERTY(EditAnywhere, Category = "")
uint8 SkinAtlasIndex = 0;
};
UCLASS()
class CITYSAMPLE_API UCitySampleCrowdVisualizationFragmentInitializer : public UMassObserverProcessor
{
GENERATED_BODY()
public:
UCitySampleCrowdVisualizationFragmentInitializer();
protected:
virtual void ConfigureQueries() override;
virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;
protected:
FMassEntityQuery EntityQuery;
uint32 FindColorOverride(FCrowdCharacterDefinition& CharacterDefinition, USkeletalMesh* SkelMesh);
UAnimToTextureDataAsset* GetAnimToTextureDataAsset(TSoftObjectPtr<UAnimToTextureDataAsset> SoftPtr);
};
// Others
매스 캐릭터 BP 클래스
매스 관련 액터는 MassAgent 컴포넌트가 포함된 C++ 클래스의 BP에서 파생된 액터일 뿐임.
기본 매스캐릭터 (단순)
시티샘플 매스액터 (확장시킴)
MassAgent컴포넌트 -> EntityConfig에서 에이전트별 Trait 추가, 정보가 액터 <-> 매스 시스템 단일방향 / 양방향 / 초기화에만 동기화 등을 결정할 수 있음.
스마트 오브젝트 (MassAI) -> 일반적으로 StateTree 기반으로 AI 에이전트와 상호작용. SmartObject 컴포넌트가 포함된 액터임. DefinitionAsset에서 설정.
스마트 오브젝트와 에이전트의 상호작용은 Smart Object User Trait을 통해 이루어짐.
UWorldSubsystem
리소스나 건물 등 다양한 요소의 위치지정 처리에 사용됨.
월드 그리드의 각 셀은 2x2미터 2차원 배열이 됨.
UCLASS()
class BUILDYMASS_API UBuildyMassGridSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
typedef FBuildyMassGridCellContent BuildyMassGrid[GridDimensions][GridDimensions];
private:
BuildyMassGrid Grid;
};
그리드는 빌더 ID가 연관된 건물, 리소스, 나무 바위 등을 포함.
그리드를 캐시에 저장, 엔티티가 캐시된 그리드 밖에 있을 때만 그리드 서브시스템에 새 데이터를 쿼리 -> 사이클 최적화.
typedef FBuildyMassGridCellContent BuildyMassGrid[3][3];
USTRUCT()
struct BUILDYMASS_API FBuildyMassMovementTargetFragment : public FMassFragment
{
GENERATED_BODY()
FVector TargetLocation;
uint32 CachedGridX;
uint32 CachedGridY;
UBuildyMassGridSubsystem::BuildyMassSubGrid CachedGrid; // Assuming BuildyMassSubGrid is a valid type, perhaps another typedef
bool bStartGoing;
};
캐시된 데이터는 MassNavSubsystem 에서도 사용함.
UBuildyMassBuildingSubSystem
매스는 움직이지 않지만 탐색을 위한 장애물이 있는 엔티티(나무,바위,건물)도 지원함. 어보이드를 사용하는 에이전트는 이를 고려.
나이아가라나 일반ISM, 폴리지 등으로 배치하는것에 비해 장점이 여기서 나옴.
월드에 엔티티 스폰 = 매스 스포너 액터
매스 스포너는 매스 엔티티를 월드에 가져오는 엔트리포인트
매스 스포너는 두 가지 요소, 어떤 유형의 엔티티를 스폰할지 어디에 스폰할지를 결정.
매스 스포너는 미리 정의된 데이터로 엔티티를 스폰하는 데 유용.
매스스포너에 세팅하는 값
Count = 스폰 수
EntryConfig = 엔티티 타입의 배열(UMassEntutyConfigAsset타입)
SpawnDataGenerators = FMassSpawnDataGenerator의 배열. 월드에 스폰할 위치를 결정. EQSSpawnPointsGenerator가 들어감. 쿼리 멤버에는 EQSRequest로 EQ_Grid가 들어감.
매스 스포너는 레벨에 배치되며, 자동으로 스폰을 시작하도록 설정하거나(플레이 시작 시 자동 스폰) 요청에 따라 런타임에 쿼리할 수 있음.
AMassSpawnner 클래스에 이를 위한 다수의 공용 블루프린트 함수가 포함되어 있음 (DoSpawning(), DoDespawning(), ScaleSpawningCount(float Scale), GetCount(), GetSpawningCountScale() 등.)
시티샘플에는 여러가지 스포너가 있음.

BP_MassCrowdSpawner , BP_MassTrafficIntersectionSpawner , BP_MassTrafficParkedVehicleSpawner, BP_MassTrafficTrailerSpawner, BP_MassTrafficVehicleSpawner
이렇게 CDO처럼 EntityConfig에 정의된 아키타입으로 배칭 스포닝을 하는 이유는 캐시메모리를 효율적으로 사용하기 때문.
various 엔티티들의 스폰 방법
하나의 스포너에 여러 엔티티 config를 세팅 가능(여러 타입). 시티샘플에서는 vehCar_vehicle02 , 03, 04... 7개를 세팅함.
SpawnDataGenerators도 7개가 들어감.
결과적으로 Count개수만큼 7개의 타입 엔티티중에서 7개의 다른 장소에 각각 스폰.
런타임데이터로 엔티티를 스폰 = BatchSpawning , SingleEntitySpawning
BatchSpawning
C++ 에서는 원하는 수만큼 특정 아키타입을 전달해 FMassEntityManager 인스턴스에서 BatchCreateEntities() 를 호출.
이건 실제로 AMassSpawner가 내부적으로 오브젝트를 스폰하는 방식
그 후 BatchSetEntityFragmentsValues()를 호출하여 반환된 FEntityCreationContext에 초기 데이터를 설정합니다.
SingleEntitySpawning는
새 엔티티를 스폰하려면 MassEntitySubsystem에 새 엔티티를 요청하면 됨.
구현 방식은 다양한데, MassEntityConfigAsset을 여러개로 만들어 각각 CDO처럼 만드는 방법(시티샘플 차량 예제)
하나의 MassEntityConfigAsset인데, 액터 멤버에 데이터 구조체를 넣고 캐릭터 빌드시에 구조체 안에서 랜덤 데이터나, 특정 조건에 따라 데이터를 읽어 초기화도 가능(시티샘플캐릭터).
아래 방법은 DT를 사용해 초기화 하는 방법과 유사. 완전히 랜더마이즈가 필요한 경우 후자(옷 색을 0~1 변조, 체형을 0~1 변조 등). 변경할 타입이 몇개 없을때는 전자의 방법이 간단. (차량 기껏해야 10종류. 공산품이므로 개별적 옵션도 없다고 가정.)
그러나 차량별로 노후 정도가 다르거나, 최대 속도를 다르게 주는 등의 다양성을 주려면 후자를 사용해야 한다.
MassEntityConfigAsset
엔티티의 정의. 매스스포너에서 스폰하기 위한 데이터. BP와 동치 개념. 비주얼, 디테일, 동작 등 엔티티의 특성을 지정.
ex) Trait[0]=CrowdVisualization -> 스태틱메시,HighResTargetActor,LowResTargetActor, LODParameters 등.
SpawnDataGenerators
매스스포너가 엔티티를 어디 스폰할지 정의.
3가지 타입의 Generator Instance를 제공.
1: EQS SpawnPoints Generator
2: Zone Graph SpawnPoints Generator
3: Custom Written Generator
1: EQS SpawnPoints Generator
EQSRequest 쿼리 탬플릿과 함께 작동한다. EQS SpawnPoints Generator를 선택하면 아래에 쿼리 탬플릿 창에도 하나 넣어줘야 세트로 작동함. Environment Query를 만들려면 쿼리 탬플릿 선택기 맨 위에 + 버튼으로 EQ_이름 으로 하나 만들면 됨. 기존 EQS 쿼리와 호환됨.
Environment Query를 사용하면 Point:Grid(기본으로 제공하는것) 같은 다양한 노드를 배치할 수 있음.
이는 기본적으로 엔티티를 배치할 그리드의 크기와 배치 방법을 정의한다.
기본 쿼리 요소들은 아래와 같음
Actors of class
Composite - Allows placing multiple Generator types at once
Current Location
Perceived Actors
Points: Circle
Points: Cone
Points: Donut
Points: Grid
Points: Pathing Grid
Smart Objects
2: Zone Graph SpawnPoints Generator
존그래프는 포인트 to 포인트 복도 구조를 따르는 가벼운 디자인 드리븐 AI용 플로우.
AI 동작에 활용할 수 있는 의미 있는 정적/동적 태그를 저장 가능.
Zone Graph SpawnPoints Generator는 프로젝트에서 존 그래프 빌드와 함께 작동함.
민희님 설명에 따르면 ZoneShape를 모으면 ZoneGraph가 되는듯.
시티샘플은 엔티티 생성 위치 결정에 후디니 절차적 데이터로 만든 존그래프에 따른 분포로 크라우드, 트래픽 시스템에 사용할 존그래프를 따라서 생성 지점을 생성함.
그러니까 존그래프 내에 적절하게 분포시켜서 스폰포인트를 생성한다.
3: 커스텀 제너레이터
BP Mass Traffic Parked Vehicle Spawn Data Generator에 보면 예시가 하나 있음. CitySampleSmallCityParkingSpaces 같은 데이터를 쓰는데, 차량 타입 - 차량 주차 위치 세트를 만들고 셔플하는걸 코드에서 함.
엔티티 파괴
1: 지연
2: 직접
지연방식이 가장 안전하므로 선호됨.
EntityManager->Defer().DestroyEntities(Entities);
EntityManager->Defer().DestroyEntity(Entity);
직접 처리는 옵저버 매니저를 호출하므로 BatchDestroyEntityChunks가 선호됨.
다른 직접 컴포지션 변경과 같이 반드시 메인 스레드 외부에서만 호출해야 안전함.
UMassSpawnerSubsystem::DestroyEntities 도 이를 호출함.
EntityManager->BatchDestroyEntityChunks(Collection)
LOD는 텍스처 품질 설정뿐만 아니라 동작에 대해서도 사용 가능. 예를 들어 카메라 거리에 따라 회피를 활성화/비활성화할 가능.
내부적으로 LOD는 태그를 사용하여 먼 레벨을 필터링하거나 더 간단한 기능을 선택한다.
2가지 예시
UENUM()
enum class EMassFragmentPresence : uint8
{
/** All of the required fragments must be present */
All,
/** One of the required fragments must be present */
Any,
/** None of the required fragments can be present */
None,
/** If fragment is present we'll use it, but it missing stop processing of a given archetype */
Optional,
MAX
};
// Option 1 of writing // 반드시 해당 태그를 가진것만 필터링 -> High 따로 Mid 따로 필터링
void USampleProcessor::ConfigurateQueries()
{
HighLODQuery.AddTagRequirement<FMassHighLODTag>(EMassFragmentPresence::All);
MediumLODQuery.AddTagRequirement<FMassMediumLODTag>(EMassFragmentPresence::All);
// Similar for LowLOD and OffLOD
}
void USampleProcessor::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context)
{
HighLODQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context)
{
// More complex code
});
MediumLODQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context)
{
// Simpler code
});
// Even simpler code for LowLOD and OffLOD
}
// Option 2 of writing // 해당 태그가 없는것만 필터링. High와 Med 태그를 가진 엔티티만 모임
void USampleProcessor::ConfigurateQueries()
{
EntityQuery.AddTagRequirement<FMassLowLODTag>(EMassFragmentPresence::None);
EntityQuery.AddTagRequirement<FMassOffLODTag>(EMassFragmentPresence::None);
}
void USampleProcessor::Execute(UMassEntitySubsystem& EntitySubsystem, FMassExecutionContext& Context)
{
EntityQuery.ForEachEntityChunk(EntitySubsystem, Context, ([this](FMassExecutionContext& Context)
{
// Code that will run only for HighLOD and MediumLOD
});
}
이렇게 요구 사항이 다른 다양한 쿼리를 사용하여 각기 다른 수준의 프로세서에 서로 다른 기능을 설정할 수 있다.
이 시스템은 실행하는 코드의 양에 영향을 줄 수 있으므로 확장성의 또 다른 열쇠.
이 시스템을 사용하면 각기 다른 틱 레이트를 가지고 다른 양의 코드를 처리하는 10만개의 유닛에 도달할 수 있다.
엔티티에서 작동하도록 하려면 MassEntityConfigAsset에 추가할 수 있는 LODCollector라는 추가 Trait이 있다.
LODCollector는 프로젝트 설정에서 등록해야 함.
프로젝트 설정 → 매스 → 모듈 설정 / Mass Entitz / ProcessorsCDOs에 NassLODCollectorProcessor를 검색, Auto Register with Processing Phases 체크.
FMassEntityView
FMassEntityView 는 모든 종류의 엔티티 작업을 쉽게 해주는 구조체.
FMassEntityHandle 과 FMassEntityManager 로 구성할 수 있다.
생성 시 FMassEntityView는 엔티티의 아키타입 데이터를 캐시하여 나중에 엔티티에 대한 정보를 검색하는 데 필요한 반복 작업을 줄여준다.
다음의 프로세서 예제에서는 NearbyEntity가 적인지 확인하고, 적인 경우 적을 손상시킨다:
FMassEntityView EntityView(Manager, NearbyEntity.Entity);
//Check if we have a tag
if (EntityView.HasTag<FEnemyTag>())
{
if(auto DamageOnHitFragment = EntityView.GetFragmentDataPtr<FDamageOnHit>())
{
// Now we defer something to do to the other entity!
FDamageFragment DamageFragment;
DamageFragment.Damage = DamageOnHitFragment.Damage * AttackPower;
Context.Defer().PushCommand<FMassCommandAddFragmentInstances>(EntityView.GetEntity, DamageFragment);
}
}
Debugging Mass behaviour
Mass Framework offers a bunch of console commands to debug the Mass function.
EnableGDT
VisLog
mass.Debug
PS
군중을 생성하는 데 특화된 MassCrowd 플러그인
언리얼 엔진 5에 스테이트 트리가 도입되었고 매스 시스템에 매우 쉽게 연결된다.
스테이트 트리 에셋 하나를 생성하고 스키마 매스 비헤이비어로 선택하면 다양한 태스크를 통해 엔티티의 상태를 제어할 수 있다.
태스크는 프로세서 역할을 하되, Fragment를 처리하여 결과를 출력하는 역할을 하게 된다.
따라서 State의 새로운 전환을 평가할 수 있다. 이는 엔티티의 상태를 보다 깔끔하게 처리하는 방법이며, 기술 수준이 낮은 팀원도 편안하게 느낄 수 있는 추상화 수준에서 작동한다.
Mass 시스템의 작업은 여전히 C++로 정의해야 하나, 액터에 대한 태스크는 비헤이비어 트리에 대한 태스크를 생성하는 것과 같은 방식으로 블루프린트에서 생성할 수 있다.
정리
ECS구조 = 데이터드리븐 플라이웨이트 패턴 프레임워크. 동일 데이터 구조를 반복사용(배칭) 하는것이 핵심.
엔티티 에이전트 / 엔티티 액터 = 액터 인스턴스 = cpp 액터에 특정 Mass에이전트컴포넌트를 붙인것. MassSpawner를 통해 스폰됨.
엔티티 = 액터 클래스 = EntityConfigAsset에 정의됨. 엔티티는 아키타입의 ID만을 가짐.
아키타입 = Trait의 집합. 일종의 클래스 개념. 동일 아키타입을 반복사용하는것이 메모리블럭 최적화.
Trait = 컴포넌트와 유사 개념. Fragment의 집합.
Fragment = 멤버 데이터와 유사 개념.
태그 = Fragment와 유사하지만 Type으로만 정의가능 (Enum과 유사). 태그로 필터링해서 쿼리도 가능. 대표적인 태그기반 동작 = LOD시스템
Processor = 틱/노티파이/함수 개념. 특정 태그, Fragment로 필터링하여 ForeachChunk를 돌며 MassEntityManager에 Excute로 데이터 쿼리. 실행 시점을 TickGroup처럼 정의 가능. 값 변조/흐름 제어. 큐잉된 쿼리는 순차 실행. 개별 실행 함수는 Excute에 정의.
UMassProcessor = 업데이트용. 매 프레임 처리
UMassObserverProcessor = 값 설정용. EMassObservedOperation(Add/Remove) 실행시 처리
서브시스템 = 분야별로 여러 서브시스템이 있음. 스폰된 액터를 자동으로 재활용, 풀링처리함. 서브시스템끼리는 직접적으로 작동함. 최종 처리 단계
ex) Mass Representation 서브시스템은 비주얼레이션 타입 전환 핸들링 및 MassActorSpawner, MassLOD 서브시스템과 직접 연동함.
MassActorSpawner 서브시스템은 MassActorSpawner오브젝트와 프로시저럴 호출을 통해 엔티티를 스폰 및 관리함.
MassSpawner서브시스템은 사용가능한 엔티티 탬플릿에 대한 정보를 호스팅하는 매스엔티티 탬플릿 레지스트리 인스턴스를 소유함.
MassLOD 서브시스템은 각 매스엔티티에 필요한 LOD를 계산. High/Med/Low/Off 네가지 LOD 출력. 각 LOD레벨에 대해 거리, 최대 수를 구성 가능. 이 서브시스템의 클라이언트는 세가지 시스템임
- Mass (Representation/Visualization) LOD -> 시각적 LOD 처리용. 디스턴스컬링, 프러스텀컬링 처리. 엔티티가 프러스텀 내 여부와 관계없이 다른 LOD 디스턴스 제공 가능.
- MassSimulationLOD -> 모든 엔티티 계산의 부하 분산. 모든 엔티티를 동일 LOD를 가지는 청크로 그룹화. 쿼리에 필터 사용시 유용. 모든 계산에 대해 가변 주기 업데이트 옵션도 있음.
- MassReplicationLOD -> 네트워크
MassStateTree 서브시스템은 StateTree시스템을 매스엔티티와 통합. 각 엔티티에 대한 StateTree 구성, 다른 매스시스템 시그널에 따라 각 엔티티의 StateTree 업데이트.
해당 로직은 엔티티에 대한 데이터를 구성, 설정에만 사용, 이후 동작은 비헤이비어 트리에 정해진대로 알아서 작동.
MassMovement 서브시스템은 MassAgent를 위한 간단한 이동 모델 정의. Fragment와 Processor는 다른 Trait이 속도나 힘을 직접 수정할 수 있도록 설정됨. 이 값들은 매스 에이전트가 사용하는 최종 이동 값과 결합됨.
ex) 에이전트 조종시 시스템은 초기 스티어링 포스 설정, Avoid 기능을 통해 이동중에 충돌 회피를 위해 스티어링 포스를 변경 가능
MassSmartObject 서브시스템은 스마트오브젝트 시스템을 매스엔티티와 통합. 스마트 오브젝트 쿼리 수행, 매스엔티티 에이전트에서 간단한 비헤이비어 실행에 필요한 트레잇, 프래그먼트, 프로세서 제공
'언리얼' 카테고리의 다른 글
루멘 GI, 메시디스턴스필드, 글로벌디스턴스필드 (0) | 2025.05.14 |
---|---|
2024언리얼페스타 2일차 프로그램세션 정리 (0) | 2025.05.13 |
에셋 레지스트리 (0) | 2025.05.13 |
에셋 참조 (0) | 2025.05.13 |
코어 리디렉트 (0) | 2025.05.13 |