방향이 다르면 통로의 모양도 다르다
Esc

    방향이 다르면 통로의 모양도 다르다

    –UnityDOTS

    플레이어는 GameObject 입니다. 콤보, 타격감, 카메라, UI 가 전부 거기 붙어 있습니다. 적 1만은 ECS 입니다. 그리고 둘은 서로를 직접 못 봅니다.

    엔티티는 GetComponent 로 못 찾고, MonoBehaviour 는 잡 안에서 못 만집니다. 통로를 뚫어야 하는데, 통로 셋을 만들고 나니 셋의 모양이 전부 달랐습니다.

    통로 셋

    GameObject 플레이어 · UI · VFX ECS 적 1만 · 시스템 싱글톤 덮어쓰기 · 최신값 1개 PlayerStateBridge (위치) 요청 엔티티 · 사건 1건 PlayerAttackBridge (공격) NativeQueue 싱글톤 · 사건 N건 DeathEventBridge · PlayerHealthBridge
    방향이 하나 다를 뿐인데 세 번째만 모양이 다릅니다.
    브릿지 방향 방식 왜 이 방식인가
    PlayerStateBridge GO → ECS 싱글톤 덮어쓰기 매 프레임 최신값 하나면 충분합니다
    PlayerAttackBridge GO → ECS 요청 엔티티 던지기 사건 1건, 수명 1프레임
    DeathEventBridge · PlayerHealthBridge ECS → GO NativeQueue 싱글톤 사건 N건, 받는 쪽이 GO

    모양을 정한 것은 두 가지입니다. 방향과 건수 입니다.

    위치는 매 프레임 최신값 하나만 있으면 되니 덮어쓰면 됩니다. 공격은 일어난 순간에 한 번이니 사건 하나를 던집니다. 사망은 한 프레임에 9,600건이 한꺼번에 일어날 수 있고, 받는 쪽이 GO 라서 통로가 달라집니다.

    GetSingleton 은 복사본을 준다

    여기서 한 번 크게 헷갈렸습니다.

    var state = SystemAPI.GetSingleton<PlayerState>();
    state.Position = newPos;              // 복사본을 고친 것뿐입니다

    GetSingleton 은 값을 복사해서 줍니다. 고쳐도 원본이 안 바뀝니다. SetComponentData 로 되돌려 써야 반영됩니다.

    그런데 큐는 반대로 동작합니다.

    var queue = SystemAPI.GetSingleton<DeathEventQueue>();
    while (queue.Value.TryDequeue(out var e)) { ... }   // 원본이 비워집니다

    NativeQueue 는 구조체 안에 포인터 하나만 들고 있습니다. 복사본을 만들어도 그 포인터가 가리키는 메모리는 하나입니다.

    같은 GetSingleton 인데 하나는 되돌려 써야 하고 하나는 안 그래도 되는 이유가 이것입니다. 값이냐 포인터냐 입니다. 앞에서 잡 필드를 볼 때 나왔던 것과 같은 구분입니다.

    CreateSingleton 은 편의 함수가 아니라 가드다

    public void OnCreate(ref SystemState state)
    {
        state.EntityManager.CreateSingleton(new PlayerState());
    }

    CreateEntity 하고 AddComponentData 하는 것과 동작은 같습니다. 다른 점은 “이미 있는데 또 만든다” 를 만드는 시점에 잡아 준다는 것입니다.

    수동으로 하면 두 개가 조용히 만들어지고, 한참 뒤 GetSingleton 에서 “싱글톤이 하나가 아니다” 로 터집니다. 그때는 어디서 두 번 만들었는지 찾아야 합니다.

    만드는 것과 공개하는 것은 별개다

    이 구분을 몰라서 큐가 통째로 안 돌았습니다.

    _queue = new NativeQueue<DeathEvent>(Allocator.Persistent);   // 만들었습니다
    // CreateSingleton 을 안 했습니다

    new NativeQueue 는 사물함을 하나 사는 일 이고, CreateSingleton 은 그 사물함 주소를 게시판에 붙이는 일 입니다.

    후자를 빼먹으면 큐는 멀쩡히 있는데 아무도 못 찾습니다. 그리고 소비자 쪽 코드가 이렇게 생겼습니다.

    if (_queue.IsEmpty) return;   // 큐를 못 찾았는데 여기서 조용히 나갑니다

    가드가 실패를 삼킵니다. 에러도 로그도 없고 사망 연출만 안 나옵니다. 방어 코드가 진단을 막는 자리입니다.

    월드는 언제 존재하나

    브릿지는 MonoBehaviour 라서 Start 나 Update 에서 ECS 월드를 잡아야 합니다.

    _em = World.DefaultGameObjectInjectionWorld.EntityManager;   // Start 에서 안전합니다

    AutomaticWorldBootstrap 이 [RuntimeInitializeOnLoadMethod(BeforeSceneLoad)] 라서 Start 시점에는 월드가 반드시 있습니다.

    단 SubScene 엔티티는 아직 없습니다. 비동기로 로드되기 때문입니다. 그래서 시스템 쪽은 RequireForUpdate 로 막고, 브릿지 쪽은 쿼리가 비었는지 확인하고 나가는 방어가 필요합니다.

    이 두 가지를 헷갈리면 “월드는 있는데 싱글톤이 없다” 로 터집니다.