Pawn Module
Pawn
Module
순수 C# 컨테이너 클래스 · Avatar · Persona · 확장 모듈 슬롯 · SoA TickSimulation 버퍼
Technical Docs · PawnSystem
Pawn Module
조종 가능하거나 시뮬레이션되는 모든 엔티티의 최소 단위. Identity(Avatar·Persona), 확장 모듈 슬롯, TickSimulation SoA 버퍼를 연결하는 허브 레이어.
Section 01
Pawn 클래스
Pawn은 ScriptableObject나 MonoBehaviour를 상속하지 않는 순수 C# 클래스다.
Avatar와 Persona를 직접 참조(Object) 또는 비동기 에셋 참조(AssetReference) 두 경로로 보유하며,
두 경로는 상호 배타적으로 관리된다.
1.1 이중 참조 전략 (상호 배타)
Avatar와 Persona는 각각 직접 참조와 AssetReference 두 경로를 가진다. 두 경로는 항상 상호 배타적으로 유지된다. Setter 하나를 호출하면 반대쪽 경로가 자동으로 null이 된다.
1.2 ModuleLink 슬롯
확장 모듈은 PawnModSlot(ScriptableObject 키)을 인덱스로 하는 ModuleLink[] 배열로 관리된다.
슬롯 키가 일치하는 항목이 없으면 배열을 +1 resize하여 추가한다. 모듈도 직접 참조와 AssetReference 두 경로를 지원한다.
private readonly struct ModuleLink
{
public PawnModSlot Slot { get; } // ScriptableObject 키 (슬롯 ID)
public Object Module { get; } // 직접 참조 (IPawnModule 검증)
public AssetReferenceT<ScriptableObject> ModuleRef { get; }
} _avatar, _persona, SetModule(), TryGetModule() 등
Object 타입을 사용하는 모든 경로에 boxing 금지 TODO 코멘트가 붙어 있다.
현재 구조에서 as IAvatar 캐스트는 관리 힙에 boxing을 유발한다.
제네릭 기반 or 구조체 슬롯으로의 리팩터링이 필요하다.
Section 02
연계 시스템
Pawn은 Avatar와 Persona를 보유하는 컨테이너 역할만 한다. 각 시스템의 데이터 구조·Flow 노드 상세는 해당 문서를 참조.
IAvatar · ScriptableObject · IPawnModule 외형 정의. BodyId / OutfitId / AccessoryId (uint).
IPersona · ScriptableObject · IPawnModule 정체성 정의. FaceId / VoiceId (uint).
커스텀 데이터를 Pawn에 붙이려면 IPawnModule(마커 인터페이스)을 구현한 ScriptableObject를 만들고,
PawnModSlot(추상 SO) 서브클래스를 슬롯 키로 생성한다.
pawn.SetModule(slot, module) 호출 시 module is not IPawnModule 검증을 통과해야 등록된다.
Section 03
Flow 노드 구성
Pawn은 Flow 그래프 노드를 통해 구성된다. 직접 오브젝트를 받는 PawnBuildNode와 에셋 참조를 받는 PawnRefNode 두 가지 빌드 노드가 존재한다.
직접 참조 경로. Avatar가 null이면 빈 Pawn 생성.
Persona는 선택적이며 null일 경우 SetPersona를 호출하지 않는다.
GetInputOverrideOrDefault()로 포트 연결 없을 때 직렬화된 기본값 사용.
AssetReference 경로. 빈 Pawn을 먼저 생성한 뒤 RuntimeKeyIsValid()를
통과한 Ref만 Set한다. 비동기 로딩이 필요한 경우 이 노드를 사용한다.
Section 04
TickSimulation 런타임
런타임에서 Pawn 데이터는 PawnBuffers에 SoA(Structure of Arrays) 레이아웃으로 보관된다.
이동과 위치는 더블 버퍼링으로 관리되며, 공간 쿼리는 NativeChunkedVolumeOctree를 사용한다.
4.1 PawnBuffers — SoA 레이아웃
MovementsBack NativeList<segment> 이동 세그먼트(시작점→끝점). Front가 현재 틱, Back이 이전 틱. ✓ FlipMovements()
PositionsBack NativeList<float3> 월드 위치. Front 기록 완료 후 FlipPositions()로 교환. ✓ FlipPositions()
<PawnVolumeEntry> 공간 쿼리용 옥트리. PawnVolumeEntry는 pawn_index + aabb를 보유. —
모든 NativeList는 Allocator.Persistent로 초기 용량 32로 생성된다.
ReserveLength(count)는 전체 버퍼를 동시에 ResizeUninitialized하며,
초기화 비용 없이 용량을 확보한다.
4.2 PawnVolumeEntry — 옥트리 엔트리
public struct PawnVolumeEntry : IHasVolume
{
public int pawn_index; // PawnBuffers 배열 인덱스
public aabb volume; // 축 정렬 바운딩 박스
public cube get_aabb() => new(volume.center, volume.size);
public aabb GetVolume() => volume;
} 4.3 더블 버퍼 패턴
FlipMovements() / FlipPositions()는 C# 튜플 스왑
(Front, Back) = (Back, Front)으로 구현. 복사 없이 포인터만 교환.
Section 05
PawnSpawner
Pawn의 생성·소멸 라이프사이클을 Job 파이프라인으로 관리한다.
스폰은 틱 단위 지연으로 분산되며, 스폰 위치는 floorSampler 옥트리로 바닥 면을 샘플링하여 결정된다.
5.1 스폰 파이프라인
seed = TemporaryPawnSpawnSeed ^ (index+1) * 747796405결과를 Insertion Sort로 오름차순 정렬 →
_spawnOrders[] spawn_count, kill_count를 NativeReference로 출력.
countNext = (현재 수 − killCount) + spawnCount전체 버퍼를 해당 길이로 ResizeUninitialized.
floorSampler 옥트리(NativeChunkedVolumeOctree<triangle_surface>)로
각 스폰 위치의 바닥 삼각형 면을 쿼리.PawnIds, AvatarIds, Shapes, Intents, ActionStates, Movements, Positions, Brains를 base_index부터 채움. 병렬 스케줄 (innerLoopBatchCount: 1).
스폰 위치를 바닥 면 위로 고정하기 위해 월드 바닥 지오메트리를 삼각형 서피스로 옥트리에 보관한다.
스폰 후보 위치에서 Radius Query를 실행하여 가장 가까운 triangle_surface를 찾고,
해당 삼각형의 법선 기준으로 Pawn 위치와 Shape를 초기화한다.