Skip to content

Pawn Module

TECHNICAL DOCS · PAWN SYSTEM

Pawn
Module

순수 C# 컨테이너 클래스 · Avatar · Persona · 확장 모듈 슬롯 · SoA TickSimulation 버퍼

PawnSystem TickSimulation Flow Graph SoA Layout

Technical Docs · PawnSystem

Pawn Module

조종 가능하거나 시뮬레이션되는 모든 엔티티의 최소 단위. Identity(Avatar·Persona), 확장 모듈 슬롯, TickSimulation SoA 버퍼를 연결하는 허브 레이어.

PawnSystem TickSimulation Flow Graph Object Boxing TODO

Pawn 클래스

Pawn은 ScriptableObject나 MonoBehaviour를 상속하지 않는 순수 C# 클래스다. Avatar와 Persona를 직접 참조(Object) 또는 비동기 에셋 참조(AssetReference) 두 경로로 보유하며, 두 경로는 상호 배타적으로 관리된다.

1.1 이중 참조 전략 (상호 배타)

Avatar와 Persona는 각각 직접 참조AssetReference 두 경로를 가진다. 두 경로는 항상 상호 배타적으로 유지된다. Setter 하나를 호출하면 반대쪽 경로가 자동으로 null이 된다.

직접 참조 경로
bool SetAvatar(Object avatar)
→ AvatarRef = null
avatar is IAvatar 검증 필수
bool SetPersona(Object persona)
→ PersonaRef = null
AssetReference 경로
bool SetAvatarRef(AssetReferenceT<Avatar>)
→ _avatar = null
RuntimeKeyIsValid() 검증 필수
bool SetPersonaRef(AssetReferenceT<Persona>)
→ _persona = null

1.2 ModuleLink 슬롯

확장 모듈은 PawnModSlot(ScriptableObject 키)을 인덱스로 하는 ModuleLink[] 배열로 관리된다. 슬롯 키가 일치하는 항목이 없으면 배열을 +1 resize하여 추가한다. 모듈도 직접 참조와 AssetReference 두 경로를 지원한다.

ModuleLink (private readonly struct)
private readonly struct ModuleLink
{
    public PawnModSlot Slot      { get; }   // ScriptableObject 키 (슬롯 ID)
    public Object      Module   { get; }   // 직접 참조 (IPawnModule 검증)
    public AssetReferenceT<ScriptableObject> ModuleRef { get; }
}
TODO — Object Boxing 금지

_avatar, _persona, SetModule(), TryGetModule()Object 타입을 사용하는 모든 경로에 boxing 금지 TODO 코멘트가 붙어 있다. 현재 구조에서 as IAvatar 캐스트는 관리 힙에 boxing을 유발한다. 제네릭 기반 or 구조체 슬롯으로의 리팩터링이 필요하다.

연계 시스템

Pawn은 Avatar와 Persona를 보유하는 컨테이너 역할만 한다. 각 시스템의 데이터 구조·Flow 노드 상세는 해당 문서를 참조.

IPawnModule — 확장 슬롯 규칙

커스텀 데이터를 Pawn에 붙이려면 IPawnModule(마커 인터페이스)을 구현한 ScriptableObject를 만들고, PawnModSlot(추상 SO) 서브클래스를 슬롯 키로 생성한다. pawn.SetModule(slot, module) 호출 시 module is not IPawnModule 검증을 통과해야 등록된다.

Flow 노드 구성

Pawn은 Flow 그래프 노드를 통해 구성된다. 직접 오브젝트를 받는 PawnBuildNode와 에셋 참조를 받는 PawnRefNode 두 가지 빌드 노드가 존재한다.

[FlowNode("Pawn Build", "Pawn/Build")]
PawnBuildNode
0 Avatar Avatar
1 Persona Persona
0 Pawn Pawn

직접 참조 경로. Avatar가 null이면 빈 Pawn 생성. Persona는 선택적이며 null일 경우 SetPersona를 호출하지 않는다. GetInputOverrideOrDefault()로 포트 연결 없을 때 직렬화된 기본값 사용.

[FlowNode("Pawn Ref", "Pawn/Build")]
PawnRefNode
0 AssetReferenceT<Avatar> AvatarRef
1 AssetReferenceT<Persona> PersonaRef
0 Pawn Pawn

AssetReference 경로. 빈 Pawn을 먼저 생성한 뒤 RuntimeKeyIsValid()를 통과한 Ref만 Set한다. 비동기 로딩이 필요한 경우 이 노드를 사용한다.

TickSimulation 런타임

런타임에서 Pawn 데이터는 PawnBuffers에 SoA(Structure of Arrays) 레이아웃으로 보관된다. 이동과 위치는 더블 버퍼링으로 관리되며, 공간 쿼리는 NativeChunkedVolumeOctree를 사용한다.

4.1 PawnBuffers — SoA 레이아웃

필드타입설명더블버퍼
PawnIds NativeList<int> Pawn 식별자. 인덱스와 별도의 ID 값 보유.
AvatarIds NativeList<int> 렌더링에 사용할 Avatar 식별자.
Shapes NativeList<capsule> 충돌·물리용 캡슐 형상. 반경과 높이 포함.
Intents NativeList<float3> 이 틱에서 Pawn이 이동하려는 의도 벡터. AI/입력 레이어가 기록.
ActionStates NativeList<pawn_action_state> 현재 행동 상태 (이동·공격·대기 등 enum 기반 상태머신).
MovementsFront
MovementsBack
NativeList<segment> 이동 세그먼트(시작점→끝점). Front가 현재 틱, Back이 이전 틱. ✓ FlipMovements()
PositionsFront
PositionsBack
NativeList<float3> 월드 위치. Front 기록 완료 후 FlipPositions()로 교환. ✓ FlipPositions()
PawnOctree NativeChunkedVolumeOctree
<PawnVolumeEntry>
공간 쿼리용 옥트리. PawnVolumeEntry는 pawn_index + aabb를 보유.
Brains NativeList<TemporarySquareLoopState> AI 루프 상태. SimulationHost.TemporarySquareLoopState (임시 구조).
MinimumCap = 32

모든 NativeList는 Allocator.Persistent로 초기 용량 32로 생성된다. ReserveLength(count)는 전체 버퍼를 동시에 ResizeUninitialized하며, 초기화 비용 없이 용량을 확보한다.

4.2 PawnVolumeEntry — 옥트리 엔트리

PawnVolumeEntry.cs · IHasVolume 구현
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 더블 버퍼 패턴

Tick N
PositionsFront ← 쓰기
PositionsBack 이전 값 읽기
FlipPositions()
Tick N+1
PositionsFront ← 쓰기
PositionsBack 이전 값 읽기

FlipMovements() / FlipPositions()는 C# 튜플 스왑 (Front, Back) = (Back, Front)으로 구현. 복사 없이 포인터만 교환.

PawnSpawner

Pawn의 생성·소멸 라이프사이클을 Job 파이프라인으로 관리한다. 스폰은 틱 단위 지연으로 분산되며, 스폰 위치는 floorSampler 옥트리로 바닥 면을 샘플링하여 결정된다.

SpawnCount 128 최대 Pawn 스폰 수
PawnSpawnEdgeTicks 120 스폰 딜레이 최대 틱 범위 [0, 120]
TemporaryPawnSpawnSeed 0x6E624EB7 딜레이 계산 기본 시드 (임시)

5.1 스폰 파이프라인

Prepare()
128개 Pawn에 대해 랜덤 딜레이(틱) 계산.
seed = TemporaryPawnSpawnSeed ^ (index+1) * 747796405
결과를 Insertion Sort로 오름차순 정렬 → _spawnOrders[]
pawn_lifecycle_job
현재 틱에서 스폰할 수와 제거할 수를 계산.
spawn_count, kill_count를 NativeReference로 출력.
ReserveLength(countNext)
countNext = (현재 수 − killCount) + spawnCount
전체 버퍼를 해당 길이로 ResizeUninitialized.
pawn_apply_lifecycle_job
floorSampler 옥트리(NativeChunkedVolumeOctree<triangle_surface>)로 각 스폰 위치의 바닥 삼각형 면을 쿼리.
PawnIds, AvatarIds, Shapes, Intents, ActionStates, Movements, Positions, Brains를 base_index부터 채움. 병렬 스케줄 (innerLoopBatchCount: 1).
floorSampler — NativeChunkedVolumeOctree<triangle_surface>

스폰 위치를 바닥 면 위로 고정하기 위해 월드 바닥 지오메트리를 삼각형 서피스로 옥트리에 보관한다. 스폰 후보 위치에서 Radius Query를 실행하여 가장 가까운 triangle_surface를 찾고, 해당 삼각형의 법선 기준으로 Pawn 위치와 Shape를 초기화한다.