ECS – Unity Physics Stateful Event Study

StatefulTriggerEventBufferAuthoring.cs

코드 설명

이 코드는 TriggerEvent를 StatefulTriggerEvent로 변환하여 Dynamic Buffer에 저장하는 역할을 합니다.

특정 엔티티에 StatefulTriggerEventBufferAuthoring 컴포넌트를 추가하면 Trigger 이벤트의 상태(Enter, Stay, Exit)를 추적할 수 있습니다.

using Unity.Assertions;
using Unity.Entities;
using UnityEngine;

namespace Unity.Physics.Stateful
{
    // StatefulTriggerEventBufferAuthoring 컴포넌트
    // GameObject에 추가하면 해당 오브젝트는 ECS Entity로 변환될 때 StatefulTriggerEvent 버퍼를 갖게 됨.
    public class StatefulTriggerEventBufferAuthoring : MonoBehaviour
    {
        // Baker 클래스: GameObject를 ECS Entity로 변환하는 역할
        class Baker : Baker<StatefulTriggerEventBufferAuthoring>
        {
            public override void Bake(StatefulTriggerEventBufferAuthoring authoring)
            {
                // 현재 GameObject를 ECS Entity로 변환 (Transform 사용 방식: 동적)
                var entity = GetEntity(TransformUsageFlags.Dynamic);

                // StatefulTriggerEvent를 저장할 Dynamic Buffer 추가
                AddBuffer<StatefulTriggerEvent>(entity);
            }
        }
    }

    // StatefulTriggerEvent 구조체
    // Trigger 이벤트를 저장할 수 있는 데이터 구조 (IStatefulSimulationEvent 인터페이스 구현)
    public struct StatefulTriggerEvent : IBufferElementData, IStatefulSimulationEvent<StatefulTriggerEvent>
    {
        // 트리거 이벤트에 연관된 두 개의 엔티티
        public Entity EntityA { get; set; }
        public Entity EntityB { get; set; }

        // 물리 본체(Body)의 인덱스
        public int BodyIndexA { get; set; }
        public int BodyIndexB { get; set; }

        // 충돌한 Collider의 키
        public ColliderKey ColliderKeyA { get; set; }
        public ColliderKey ColliderKeyB { get; set; }

        // 현재 이벤트 상태 (Enter, Stay, Exit)
        public StatefulEventState State { get; set; }

        // 생성자: 기존의 TriggerEvent 데이터를 StatefulTriggerEvent로 변환
        public StatefulTriggerEvent(TriggerEvent triggerEvent)
        {
            EntityA = triggerEvent.EntityA;
            EntityB = triggerEvent.EntityB;
            BodyIndexA = triggerEvent.BodyIndexA;
            BodyIndexB = triggerEvent.BodyIndexB;
            ColliderKeyA = triggerEvent.ColliderKeyA;
            ColliderKeyB = triggerEvent.ColliderKeyB;
            State = default; // 초기 상태 설정 (Enter, Stay, Exit 없음)
        }

        // 특정 엔티티에 대해, 이벤트에 연관된 '다른 엔티티'를 반환하는 메서드
        public Entity GetOtherEntity(Entity entity)
        {
            // 전달된 엔티티가 EntityA 또는 EntityB인지 검증 (디버그용)
            Assert.IsTrue((entity == EntityA) || (entity == EntityB));

            // EntityA가 입력되면 EntityB를 반환하고, 반대로 EntityB가 입력되면 EntityA 반환
            return (entity == EntityA) ? EntityB : EntityA;
        }

        // 이벤트 비교를 위한 메서드 (정렬, 검색 등에 사용 가능)
        public int CompareTo(StatefulTriggerEvent other)
        {
            return ISimulationEventUtilities.CompareEvents(this, other);
        }
    }

    // StatefulTriggerEvent를 제외하는 컴포넌트
    // 이 컴포넌트가 추가된 엔티티는 StatefulTriggerEventBufferSystem에서 이벤트를 수집하지 않음
    public struct StatefulTriggerEventExclude : IComponentData { }
}

Unity의 GameObject를 ECS(Entity-Component-System) 엔티티로 변환하는 역할

Baker 클래스에서 StatefulTriggerEvent를 저장할 Dynamic Buffer를 추가함

이를 통해 Trigger 이벤트를 저장하는 버퍼를 ECS 엔티티에 추가 가능

해당 엔티티는 StatefulTriggerEvent 버퍼를 가지게 됨.

  • TriggerEvent 정보를 저장하는 Stateful 버전
  • StatefulEventState를 추가하여 Trigger 이벤트의 상태(Enter, Stay, Exit)를 추적
  • 기존 TriggerEvent는 한 프레임 동안만 존재 → 이를 StatefulTriggerEvent로 변환하여 지속적인 상태 추적 가능
필드명설명
EntityA, EntityB트리거 이벤트가 발생한 두 개의 엔티티
BodyIndexA, BodyIndexB각각의 물리 본체(Body)의 인덱스
ColliderKeyA, ColliderKeyB충돌한 Collider의 키
StateEnter, Stay, Exit 상태를 저장
  • 특정 엔티티가 트리거 이벤트에 포함된 다른 엔티티를 가져옴
  • EntityA를 전달하면 EntityB를 반환하고, 반대로 EntityB를 전달하면 EntityA를 반환

StatefulTriggerEventBufferSystem특정 엔티티의 트리거 이벤트를 저장하지 않도록 제외 처리 (쿼리에서 제외)


StatefulTriggerEventBufferSystem.cs

코드 설명

  1. 트리거 이벤트를 StatefulTriggerEvent로 변환하여 Dynamic Buffer에 저장
  2. Enter, Stay, Exit 이벤트를 추적하여 충돌 지속 여부를 확인 가능
  3. OnCreate()
    • StatefulSimulationEventBuffers를 생성하고 이벤트 저장을 위한 버퍼를 할당
    • StatefulTriggerEvent를 포함하고 StatefulTriggerEventExclude가 없는 엔티티를 찾는 쿼리 설정
  4. OnUpdate()
    • 기존 이벤트 버퍼 초기화
    • TriggerEvent 데이터를 수집하고, StatefulTriggerEvent로 변환하여 저장
  5. OnDestroy()
    • 사용했던 메모리를 해제하여 최적화
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics.Systems;
using Unity.Collections;
using Unity.Burst;

namespace Unity.Physics.Stateful
{
    // 이 시스템은 TriggerEvent의 스트림을 StatefulTriggerEvent로 변환하여 Dynamic Buffer에 저장할 수 있도록 합니다.
    // 이를 통해 충돌 상태(Enter, Stay, Exit)를 추적할 수 있습니다.
    [UpdateInGroup(typeof(PhysicsSystemGroup))] // PhysicsSystemGroup 내에서 실행됨
    [UpdateAfter(typeof(PhysicsSimulationGroup))] // PhysicsSimulationGroup 이후에 실행됨
    public partial struct StatefulTriggerEventBufferSystem : ISystem
    {
        // 현재 및 이전 프레임의 Trigger 이벤트를 저장하는 버퍼
        private StatefulSimulationEventBuffers<StatefulTriggerEvent> m_StateFulEventBuffers;
        // 이벤트 처리에 필요한 컴포넌트 조회 핸들
        private ComponentHandles m_ComponentHandles;
        // 트리거 이벤트를 처리할 엔티티 쿼리
        private EntityQuery m_TriggerEventQuery;

        // 이벤트를 저장할 컴포넌트 핸들 구조체
        struct ComponentHandles
        {
            // 특정 이벤트를 제외할 엔티티 조회
            public ComponentLookup<StatefulTriggerEventExclude> EventExcludes;
            // StatefulTriggerEvent를 저장할 Dynamic Buffer 조회
            public BufferLookup<StatefulTriggerEvent> EventBuffers;

            // 컴포넌트 핸들 초기화
            public ComponentHandles(ref SystemState systemState)
            {
                EventExcludes = systemState.GetComponentLookup<StatefulTriggerEventExclude>(true);
                EventBuffers = systemState.GetBufferLookup<StatefulTriggerEvent>();
            }

            // 최신 상태로 업데이트
            public void Update(ref SystemState systemState)
            {
                EventExcludes.Update(ref systemState);
                EventBuffers.Update(ref systemState);
            }
        }

        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
            // 트리거 이벤트를 처리할 엔티티 쿼리 생성
            // (StatefulTriggerEvent가 있으며, StatefulTriggerEventExclude가 없는 엔티티)
            EntityQueryBuilder builder = new EntityQueryBuilder(Allocator.Temp)
                .WithAllRW<StatefulTriggerEvent>() // StatefulTriggerEvent를 읽고 쓸 수 있는 엔티티 선택
                .WithNone<StatefulTriggerEventExclude>(); // StatefulTriggerEventExclude가 없는 엔티티만 선택

            // Stateful 이벤트 버퍼를 생성하고 할당
            m_StateFulEventBuffers = new StatefulSimulationEventBuffers<StatefulTriggerEvent>();
            m_StateFulEventBuffers.AllocateBuffers();

            // 쿼리 설정 및 시스템 업데이트 필요성 명시
            m_TriggerEventQuery = state.GetEntityQuery(builder);
            state.RequireForUpdate(m_TriggerEventQuery);

            // 컴포넌트 핸들 초기화
            m_ComponentHandles = new ComponentHandles(ref state);
        }

        [BurstCompile]
        public void OnDestroy(ref SystemState state)
        {
            // Stateful 이벤트 버퍼 해제
            m_StateFulEventBuffers.Dispose();
        }

        // 트리거 이벤트 버퍼를 초기화하는 Job
        [BurstCompile]
        public partial struct ClearTriggerEventDynamicBufferJob : IJobEntity
        {
            // 이벤트 버퍼 초기화
            public void Execute(ref DynamicBuffer<StatefulTriggerEvent> eventBuffer) => eventBuffer.Clear();
        }

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // 최신 상태로 컴포넌트 핸들 업데이트
            m_ComponentHandles.Update(ref state);

            // 기존 트리거 이벤트 버퍼 초기화 (병렬 실행)
            state.Dependency = new ClearTriggerEventDynamicBufferJob() .ScheduleParallel(m_TriggerEventQuery, state.Dependency);

            // 이전 프레임과 현재 프레임의 이벤트 버퍼를 교체
            m_StateFulEventBuffers.SwapBuffers();

            var currentEvents = m_StateFulEventBuffers.Current; // 현재 프레임의 이벤트
            var previousEvents = m_StateFulEventBuffers.Previous; // 이전 프레임의 이벤트

            // 새로운 Trigger 이벤트를 수집하는 Job 실행
            state.Dependency = new StatefulEventCollectionJobs.CollectTriggerEvents
            {
                TriggerEvents = currentEvents
            }.Schedule(SystemAPI.GetSingleton<SimulationSingleton>(), state.Dependency);

            // 트리거 이벤트를 StatefulTriggerEvent로 변환하여 Dynamic Buffer에 저장하는 Job 실행
            state.Dependency = new StatefulEventCollectionJobs.ConvertEventStreamToDynamicBufferJob<StatefulTriggerEvent, StatefulTriggerEventExclude>
            {
                CurrentEvents = currentEvents, // 현재 프레임의 이벤트
                PreviousEvents = previousEvents, // 이전 프레임의 이벤트
                EventLookup = m_ComponentHandles.EventBuffers, // 이벤트 버퍼 조회

                UseExcludeComponent = true, // 제외할 컴포넌트(StatefulTriggerEventExclude) 사용 여부
                EventExcludeLookup = m_ComponentHandles.EventExcludes // 제외할 이벤트 조회
            }.Schedule(state.Dependency);
        }
    }
}

ComponentHandles: StatefulTriggerEventExcludeStatefulTriggerEvent 버퍼를 조회하는 핸들을 보유하는 구조체

  • EventExcludes: 트리거 이벤트가 수집되지 않도록 제외할 엔티티를 조회합니다.
  • EventBuffers: StatefulTriggerEvent를 저장하는 Dynamic Buffer를 조회합니다.

트리거 이벤트를 처리할 쿼리 생성:

  • StatefulTriggerEvent 컴포넌트를 읽고 쓸 수 있는 엔티티를 선택하고,
  • StatefulTriggerEventExclude가 없는 엔티티만 선택하여 트리거 이벤트를 수집합니다.

m_StateFulEventBuffers를 초기화하고, 버퍼를 할당합니다.

m_TriggerEventQuery 쿼리를 설정하여 이 쿼리가 시스템 업데이트에 필요함을 명시합니다.

컴포넌트 핸들 업데이트: m_ComponentHandles를 최신 상태로 업데이트합니다.

버퍼 초기화: ClearTriggerEventDynamicBufferJob을 병렬로 실행하여 이전 트리거 이벤트 버퍼를 초기화합니다.

이벤트 버퍼 교체: SwapBuffers를 사용하여 현재 프레임과 이전 프레임의 버퍼를 교체합니다.

Trigger 이벤트 수집: StatefulEventCollectionJobs.CollectTriggerEvents를 통해 새로운 트리거 이벤트를 수집합니다.

StatefulTriggerEvent 변환: 수집한 트리거 이벤트를 StatefulTriggerEvent로 변환하고 Dynamic Buffer에 저장합니다.


IStatefulSimulationEvent.cs

충돌 상태 변화에 대한 추적을 가능하게 하여, 물체 간 상호작용의 상태를 세분화하고, 이를 이벤트 시스템을 통해 관리

using Unity.Entities;

namespace Unity.Physics.Stateful
{
    // 이벤트 상태를 설명합니다.
    // 이벤트 상태는 다음과 같이 설정됩니다:
    //    0) Undefined (정의되지 않음): 상태가 불명확하거나 필요하지 않을 때
    //    1) Enter (진입): 두 물체가 현재 프레임에서 상호작용하고 있지만, 이전 프레임에서는 상호작용하지 않았을 때
    //    2) Stay (유지): 두 물체가 현재 프레임에서 상호작용 중이고, 이전 프레임에서도 상호작용했을 때
    //    3) Exit (탈출): 두 물체가 현재 프레임에서 상호작용하지 않지만, 이전 프레임에서는 상호작용했을 때
    public enum StatefulEventState : byte
    {
        Undefined,  // 상태가 정의되지 않음
        Enter,      // 두 물체가 현재 프레임에서 상호작용 중이지만 이전 프레임에서는 상호작용하지 않았을 때
        Stay,       // 두 물체가 현재 프레임과 이전 프레임에서 모두 상호작용하고 있을 때
        Exit        // 두 물체가 현재 프레임에서 상호작용하지 않지만 이전 프레임에서는 상호작용했을 때
    }

    // 추가적인 StatefulEventState를 포함하여 ISimulationEvent를 확장합니다.
    public interface IStatefulSimulationEvent<T> : IBufferElementData, ISimulationEvent<T>
    {
        public StatefulEventState State { get; set; } // 이벤트 상태
    }
}

StatefulJobs.cs

Unity에서 물리 시뮬레이션의 충돌 이벤트를 상태 기반으로 추적하고, 이를 Dynamic Buffer에 저장하는 시스템을 구현하는 데 사용됩니다.

여기서는 StatefulEventCollectionJobs라는 이름의 클래스가 주요한 역할을 합니다.

여러 개의 Job을 정의하여 충돌 이벤트를 수집하고, 이를 상태 기반 이벤트로 변환하여 저장합니다.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;

namespace Unity.Physics.Stateful
{
    public static class StatefulEventCollectionJobs
    {
        // Trigger 이벤트를 StatefulTriggerEvent로 변환하여 NativeList에 추가하는 Job
        [BurstCompile]
        public struct CollectTriggerEvents : ITriggerEventsJob
        {
            // 저장할 Trigger 이벤트를 담을 리스트
            public NativeList<StatefulTriggerEvent> TriggerEvents;

            // TriggerEvent를 StatefulTriggerEvent로 변환하여 리스트에 추가
            public void Execute(TriggerEvent triggerEvent) => TriggerEvents.Add(new StatefulTriggerEvent(triggerEvent));
        }

        // Collision 이벤트를 StatefulCollisionEvent로 변환하여 NativeList에 추가하는 Job
        [BurstCompile]
        public struct CollectCollisionEvents : ICollisionEventsJob
        {
            // 저장할 Collision 이벤트를 담을 리스트
            public NativeList<StatefulCollisionEvent> CollisionEvents;

            // CollisionEvent를 StatefulCollisionEvent로 변환하여 리스트에 추가
            public void Execute(CollisionEvent collisionEvent) => CollisionEvents.Add(new StatefulCollisionEvent(collisionEvent));
        }

        // Collision 이벤트와 함께 세부 정보를 계산하여 StatefulCollisionEvent로 변환하고 NativeList에 추가하는 Job
        [BurstCompile]
        public struct CollectCollisionEventsWithDetails : ICollisionEventsJob
        {
            // 저장할 Collision 이벤트를 담을 리스트
            public NativeList<StatefulCollisionEvent> CollisionEvents;

            // 물리 월드와 세부 사항 계산에 사용할 컴포넌트 조회
            [ReadOnly] public PhysicsWorld PhysicsWorld;
            [ReadOnly] public ComponentLookup<StatefulCollisionEventDetails> EventDetails;

            // 세부 사항 계산을 강제할지 여부
            public bool ForceCalculateDetails;

            // CollisionEvent를 처리하고 세부 사항을 계산하여 StatefulCollisionEvent로 변환
            public void Execute(CollisionEvent collisionEvent)
            {
                var statefulCollisionEvent = new StatefulCollisionEvent(collisionEvent);

                // 세부 사항 계산 여부 결정
                bool calculateDetails = ForceCalculateDetails;
                if (!calculateDetails && EventDetails.HasComponent(collisionEvent.EntityA))
                {
                    calculateDetails = EventDetails[collisionEvent.EntityA].CalculateDetails;
                }
                if (!calculateDetails && EventDetails.HasComponent(collisionEvent.EntityB))
                {
                    calculateDetails = EventDetails[collisionEvent.EntityB].CalculateDetails;
                }

                // 세부 사항을 계산하고 상태에 저장
                if (calculateDetails)
                {
                    var details = collisionEvent.CalculateDetails(ref PhysicsWorld);
                    statefulCollisionEvent.CollisionDetails = new StatefulCollisionEvent.Details(
                        details.EstimatedContactPointPositions.Length,
                        details.EstimatedImpulse,
                        details.AverageContactPointPosition);
                }

                // 계산된 CollisionEvent를 리스트에 추가
                CollisionEvents.Add(statefulCollisionEvent);
            }
        }

        // 이벤트 스트림을 Dynamic Buffer로 변환하는 Job
        // T: StatefulSimulationEvent 타입
        // C: 제외할 컴포넌트 타입
        [BurstCompile]
        public struct ConvertEventStreamToDynamicBufferJob<T, C> : IJob
            where T : unmanaged, IBufferElementData, IStatefulSimulationEvent<T>  // T는 IStatefulSimulationEvent를 구현해야 함
            where C : unmanaged, IComponentData  // C는 IComponentData를 구현해야 함
        {
            // 이전과 현재의 이벤트 리스트
            public NativeList<T> PreviousEvents;
            public NativeList<T> CurrentEvents;

            // 이벤트를 저장할 Dynamic Buffer 조회
            public BufferLookup<T> EventLookup;

            // 제외할 컴포넌트를 사용할지 여부
            public bool UseExcludeComponent;

            // 제외할 컴포넌트를 조회하는 컴포넌트 핸들
            [ReadOnly] public ComponentLookup<C> EventExcludeLookup;

            // 이벤트를 변환하고 Dynamic Buffer에 추가하는 작업
            public void Execute()
            {
                // 이벤트를 저장할 임시 리스트
                var statefulEvents = new NativeList<T>(CurrentEvents.Length, Allocator.Temp);

                // 이전 및 현재 이벤트에서 상태를 기반으로 상태 변경된 이벤트를 가져옴
                StatefulSimulationEventBuffers<T>.GetStatefulEvents(PreviousEvents, CurrentEvents, statefulEvents);

                // 상태가 변경된 이벤트를 처리
                for (int i = 0; i < statefulEvents.Length; i++)
                {
                    var statefulEvent = statefulEvents[i];

                    // 이벤트가 추가될 엔티티 A와 B가 동적으로 버퍼를 가지고 있는지 확인
                    var addToEntityA = EventLookup.HasBuffer(statefulEvent.EntityA) &&
                        (!UseExcludeComponent || !EventExcludeLookup.HasComponent(statefulEvent.EntityA));
                    var addToEntityB = EventLookup.HasBuffer(statefulEvent.EntityB) &&
                        (!UseExcludeComponent || !EventExcludeLookup.HasComponent(statefulEvent.EntityA));

                    // 엔티티 A와 B에 이벤트를 추가
                    if (addToEntityA)
                    {
                        EventLookup[statefulEvent.EntityA].Add(statefulEvent);
                    }

                    if (addToEntityB)
                    {
                        EventLookup[statefulEvent.EntityB].Add(statefulEvent);
                    }
                }
            }
        }
    }
}

TriggerEvent 객체를 받아 StatefulTriggerEvent로 변환한 후 TriggerEvents 리스트에 추가하는 작업을 수행합니다.

TriggerEvent: 두 개체 간의 충돌 여부를 나타내는 기본적인 이벤트입니다.

StatefulTriggerEvent: 이 구조체는 충돌 상태를 추적할 수 있도록 StatefulEventState (Enter, Stay, Exit, Undefined) 상태를 추가하여 TriggerEvent를 확장한 것입니다.

Execute 메서드는 주어진 TriggerEventStatefulTriggerEvent로 변환하여 TriggerEvents 리스트에 추가합니다.

CollisionEvent를 받아 StatefulCollisionEvent로 변환한 후 CollisionEvents 리스트에 추가합니다.

CollisionEvent: 두 개체 간의 충돌을 나타내는 이벤트입니다.

StatefulCollisionEvent: 충돌 상태를 추적하기 위해 StatefulEventState를 추가한 CollisionEvent입니다.

Execute 메서드는 CollisionEventStatefulCollisionEvent로 변환하여 CollisionEvents 리스트에 추가합니다.

충돌 이벤트와 함께 세부 정보를 계산하고 이를 StatefulCollisionEvent로 변환하여 CollisionEvents 리스트에 추가합니다.

ForceCalculateDetails: 세부 사항을 강제로 계산할지 여부를 나타내는 플래그입니다.

EventDetails: 각 개체에 대한 세부 사항을 계산할지 여부를 결정하는 StatefulCollisionEventDetails 컴포넌트의 조회입니다.

  • Execute 메서드는 CollisionEventStatefulCollisionEvent로 변환한 후, 해당 이벤트에 대해 세부 사항을 계산합니다.
  • CalculateDetails 메서드는 충돌 세부 사항을 계산하고, 그 결과를 StatefulCollisionEventCollisionDetails에 저장합니다.
  • 세부 사항을 계산할 필요가 없다면 기본적으로 세부 사항을 계산하지 않습니다.

세부 사항 계산

Estimated Contact Point Positions (예상 접촉 지점 위치):

  • 충돌이 발생했을 때 예상되는 접촉 지점의 위치들
  • 이는 충돌이 발생한 표면에서 실제로 물리적으로 접촉한 지점들
  • 예를 들어, 두 물체가 충돌하면서 여러 접촉 지점이 생길 수 있다.

Estimated Impulse (예상 충격량):

  • 두 객체 간의 충돌로 발생하는 충격량입니다. 충돌 후 물체가 받을 힘을 측정하는 값
  • 충돌 전후의 물리적 특성(속도 변화, 질량 등)을 바탕으로 계산
  • 이는 두 객체 간의 충돌로 인해 변화하는 운동량

Average Contact Point Position (평균 접촉 지점 위치):

  • 여러 접촉 지점이 있다면, 그 중에서 평균적인 접촉 지점 위치를 계산한 것
  • 이는 충돌이 발생한 여러 지점들 중에서, 전체적인 충격을 대표할 수 있는 중간 지점을 제공하는 역할

상태 기반 이벤트 스트림을 Dynamic Buffer로 변환하는 작업을 수행합니다.

EventLookup: StatefulTriggerEventStatefulCollisionEvent와 같은 상태 기반 이벤트를 저장할 Dynamic Buffer의 조회입니다.

UseExcludeComponent: 특정 컴포넌트를 제외할지 여부를 결정하는 플래그입니다.

  • PreviousEventsCurrentEvents에서 상태 기반 이벤트를 비교하여 상태 변화가 있는 이벤트를 가져옵니다.
  • 변환된 이벤트를 EventLookup에 해당하는 엔티티의 Dynamic Buffer에 추가합니다.
  • EventExcludeLookup를 사용하여 제외할 컴포넌트가 있으면 해당 엔티티에 대한 이벤트 추가를 방지합니다.

StatefulSimulationEventBuffers.cs

StatefulSimulationEventBuffers라는 구조체로, 상태를 추적할 수 있는 이벤트 버퍼를 관리합니다.

정의된 함수들은 물리 시뮬레이션 이벤트의 상태를 추적하고 이를 처리하는데 필요한 버퍼 관리 및 이벤트 비교 기능을 제공합니다. (주로 충돌이나 상호작용 이벤트를 처리 )

각 이벤트가 이전 프레임과 비교하여 어떤 상태(Enter, Stay, Exit)에 해당하는지 판단하고, 이를 기반으로 상태를 변경하여 결과를 반환합니다.

using Unity.Collections;

namespace Unity.Physics.Stateful
{
    // StatefulSimulationEventBuffers는 이벤트의 상태를 추적하는 두 버퍼를 관리하는 구조체입니다.
    // 이 구조체는 지난 프레임과 현재 프레임의 이벤트를 저장하며, 충돌 이벤트에 대한 상태 변경을 추적합니다.
    public struct StatefulSimulationEventBuffers<T> where T : unmanaged, IStatefulSimulationEvent<T>
    {
        // 이전 프레임의 이벤트를 저장하는 리스트
        public NativeList<T> Previous;
        // 현재 프레임의 이벤트를 저장하는 리스트
        public NativeList<T> Current;

        // 이벤트 버퍼 할당
        public void AllocateBuffers()
        {
            // 이전 및 현재 프레임 이벤트를 저장할 버퍼를 할당
            Previous = new NativeList<T>(Allocator.Persistent);
            Current = new NativeList<T>(Allocator.Persistent);
        }

        // 이벤트 버퍼를 해제
        public void Dispose()
        {
            // 버퍼가 생성되었으면 해제
            if (Previous.IsCreated) Previous.Dispose();
            if (Current.IsCreated) Current.Dispose();
        }

        // 이전 프레임과 현재 프레임의 버퍼를 교환
        public void SwapBuffers()
        {
            // 이전과 현재 이벤트 버퍼를 교환
            var tmp = Previous;
            Previous = Current;
            Current = tmp;
            // 현재 버퍼를 비움 (새로운 이벤트로 덮어쓰기 위해)
            Current.Clear();
        }

        /// <summary>
        /// 이전 프레임과 현재 프레임의 이벤트를 결합하여 하나의 정렬된 리스트로 반환합니다.
        /// 반드시 SortBuffers를 먼저 호출하여 정렬된 상태로 만들어야 합니다.
        /// </summary>
        /// <param name="statefulEvents">결합된 이벤트 리스트</param>
        /// <param name="sortCurrent">현재 이벤트 리스트를 정렬해야 하는지 여부</param>
        public void GetStatefulEvents(NativeList<T> statefulEvents, bool sortCurrent = true) =>
            GetStatefulEvents(Previous, Current, statefulEvents, sortCurrent);

        /// <summary>
        /// 두 개의 정렬된 이벤트 버퍼를 결합하여 하나의 리스트로 반환합니다.
        /// 각 이벤트에는 적절한 StatefulEventState가 설정됩니다.
        /// </summary>
        /// <param name="previousEvents">이전 프레임의 이벤트 버퍼</param>
        /// <param name="currentEvents">현재 프레임의 이벤트 버퍼</param>
        /// <param name="statefulEvents">결합된 이벤트 리스트</param>
        /// <param name="sortCurrent">현재 이벤트 리스트를 정렬해야 하는지 여부</param>
        public static void GetStatefulEvents(NativeList<T> previousEvents, NativeList<T> currentEvents, NativeList<T> statefulEvents, bool sortCurrent = true)
        {
            // 현재 이벤트 리스트가 정렬되지 않았다면 정렬
            if (sortCurrent) currentEvents.Sort();

            // 새로운 리스트를 비움
            statefulEvents.Clear();

            int c = 0; // 현재 이벤트 리스트의 인덱스
            int p = 0; // 이전 이벤트 리스트의 인덱스

            // 현재 이벤트와 이전 이벤트를 비교하며 상태 변경
            while (c < currentEvents.Length && p < previousEvents.Length)
            {
                // 두 이벤트를 비교
                int r = previousEvents[p].CompareTo(currentEvents[c]);
                if (r == 0)
                {
                    // 현재 이벤트가 이전 이벤트와 동일하다면 'Stay' 상태로 추가
                    var currentEvent = currentEvents[c];
                    currentEvent.State = StatefulEventState.Stay;
                    statefulEvents.Add(currentEvent);
                    c++;
                    p++;
                }
                else if (r < 0)
                {
                    // 이전 이벤트가 현재 이벤트보다 먼저 왔다면 'Exit' 상태로 추가
                    var previousEvent = previousEvents[p];
                    previousEvent.State = StatefulEventState.Exit;
                    statefulEvents.Add(previousEvent);
                    p++;
                }
                else //(r > 0)
                {
                    // 현재 이벤트가 이전 이벤트보다 먼저 왔다면 'Enter' 상태로 추가
                    var currentEvent = currentEvents[c];
                    currentEvent.State = StatefulEventState.Enter;
                    statefulEvents.Add(currentEvent);
                    c++;
                }
            }

            // 현재 이벤트가 다 처리되었고, 이전 이벤트가 남았다면 'Exit' 상태로 추가
            if (c == currentEvents.Length)
            {
                while (p < previousEvents.Length)
                {
                    var previousEvent = previousEvents[p];
                    previousEvent.State = StatefulEventState.Exit;
                    statefulEvents.Add(previousEvent);
                    p++;
                }
            }
            // 이전 이벤트가 다 처리되었고, 현재 이벤트가 남았다면 'Enter' 상태로 추가
            else if (p == previousEvents.Length)
            {
                while (c < currentEvents.Length)
                {
                    var currentEvent = currentEvents[c];
                    currentEvent.State = StatefulEventState.Enter;
                    statefulEvents.Add(currentEvent);
                    c++;
                }
            }
        }
    }
}

AllocateBuffers()

PreviousCurrent 두 개의 이벤트 버퍼를 할당합니다.

이 함수는 NativeList<T> 타입의 PreviousCurrent 버퍼를 할당합니다.

이 버퍼들은 각 프레임의 이벤트들을 저장하는 데 사용됩니다.

Allocator.Persistent는 이 메모리가 여러 프레임에 걸쳐 유지될 수 있도록 해줍니다.

Dispose()

할당된 PreviousCurrent 이벤트 버퍼를 해제합니다.

Dispose()AllocateBuffers()에서 할당한 PreviousCurrent 버퍼를 메모리에서 해제하는 역할을 합니다.

IsCreated를 통해 버퍼가 생성되었는지 확인한 후, 생성된 버퍼에 대해 Dispose()를 호출하여 메모리 관리를 수행합니다.

SwapBuffers()

PreviousCurrent 버퍼를 교환하고, Current 버퍼를 비웁니다.

이 함수는 이전 프레임의 이벤트(Previous)와 현재 프레임의 이벤트(Current)를 교환합니다.

그 후, Current 버퍼를 비워서 다음 프레임의 데이터를 받을 준비를 합니다.

교환하는 이유는 이벤트가 프레임 간에 이동하면서 상태를 추적하고 업데이트하는 방식 때문입니다.

PreviousCurrent 이벤트 리스트를 결합하여 상태별 이벤트 리스트를 반환합니다.

이 함수는 두 개의 이벤트 리스트인 PreviousCurrent를 결합하는 작업을 수행하는 GetStatefulEvents()의 래퍼(wrapper) 함수입니다.

sortCurrent 파라미터를 사용하여 Current 리스트가 정렬되어야 하는지를 결정합니다.

기본적으로 sortCurrenttrue로 설정되어 있어 Current 리스트가 정렬됩니다.

두 개의 정렬된 이벤트 리스트를 결합하여 하나의 상태별 이벤트 리스트로 반환합니다.

주어진 두 개의 이벤트 리스트(previousEventscurrentEvents)를 비교하고, 각 이벤트에 적절한 StatefulEventState를 설정합니다.

각 상태는 Enter, Stay, Exit으로 나뉩니다.

  • Enter: 현재 프레임에 새롭게 등장한 이벤트.
  • Stay: 이전과 현재 프레임 모두에서 존재하는 이벤트.
  • Exit: 이전 프레임에는 존재했지만 현재 프레임에는 없는 이벤트.

이 함수는 각 이벤트를 비교하여 상태를 설정하고, statefulEvents에 추가합니다.


StatefulCollisionEventBufferAuthoring.cs

using System.Runtime.InteropServices;
using Unity.Assertions;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;

namespace Unity.Physics.Stateful
{
    // StatefulCollisionEventBufferAuthoring은 Unity의 MonoBehaviour로, 
    // 물리 충돌 이벤트가 포함된 버퍼를 정의하고, 해당 엔티티에 대해 세부 사항을 계산할지 여부를 결정합니다.
    public class StatefulCollisionEventBufferAuthoring : MonoBehaviour
    {
        [Tooltip("선택하면 이 엔티티의 충돌 이벤트 동적 버퍼에서 세부 정보가 계산됩니다")]
        public bool CalculateDetails = false; // 충돌 이벤트에 세부 사항을 계산할지 여부를 결정하는 변수

        // Baker 클래스는 Authoring 컴포넌트에서 데이터를 추출하고 Entity로 변환하는 역할을 합니다.
        class StatefulCollisionEventBufferBaker : Baker<StatefulCollisionEventBufferAuthoring>
        {
            public override void Bake(StatefulCollisionEventBufferAuthoring authoring)
            {
                // TransformUsageFlags.Dynamic 플래그를 사용하여 해당 엔티티를 생성합니다.
                var entity = GetEntity(TransformUsageFlags.Dynamic);

                // CalculateDetails가 true일 경우, StatefulCollisionEventDetails를 엔티티에 추가합니다.
                if (authoring.CalculateDetails)
                {
                    var dynamicBufferTag = new StatefulCollisionEventDetails
                    {
                        CalculateDetails = authoring.CalculateDetails
                    };

                    AddComponent(entity, dynamicBufferTag); // StatefulCollisionEventDetails 컴포넌트 추가
                }

                // 충돌 이벤트를 저장할 버퍼 추가
                AddBuffer<StatefulCollisionEvent>(entity);
            }
        }
    }

    // StatefulCollisionEventDetails 컴포넌트는 엔티티가 세부 사항을 계산할지를 설정하는 플래그입니다.
    public struct StatefulCollisionEventDetails : IComponentData
    {
        public bool CalculateDetails; // 세부 사항을 계산할지 여부를 설정하는 변수
    }

    // StatefulCollisionEvent는 DynamicBuffer에 저장할 수 있는 충돌 이벤트 구조체입니다.
    public struct StatefulCollisionEvent : IBufferElementData, IStatefulSimulationEvent<StatefulCollisionEvent>
    {
        public Entity EntityA { get; set; } // 첫 번째 엔티티
        public Entity EntityB { get; set; } // 두 번째 엔티티
        public int BodyIndexA { get; set; } // 첫 번째 엔티티의 바디 인덱스
        public int BodyIndexB { get; set; } // 두 번째 엔티티의 바디 인덱스
        public ColliderKey ColliderKeyA { get; set; } // 첫 번째 엔티티의 충돌체 키
        public ColliderKey ColliderKeyB { get; set; } // 두 번째 엔티티의 충돌체 키
        public StatefulEventState State { get; set; } // 이벤트 상태 (Enter, Stay, Exit 등)
        public float3 Normal; // 충돌 법선 벡터

        // CalculateDetails가 체크된 경우, 이 필드는 유효한 값이 있으며 그렇지 않으면 기본값으로 초기화됩니다.
        internal Details CollisionDetails;

        // 생성자: CollisionEvent를 StatefulCollisionEvent로 변환합니다.
        public StatefulCollisionEvent(CollisionEvent collisionEvent)
        {
            EntityA = collisionEvent.EntityA;
            EntityB = collisionEvent.EntityB;
            BodyIndexA = collisionEvent.BodyIndexA;
            BodyIndexB = collisionEvent.BodyIndexB;
            ColliderKeyA = collisionEvent.ColliderKeyA;
            ColliderKeyB = collisionEvent.ColliderKeyB;
            State = default;
            Normal = collisionEvent.Normal;
            CollisionDetails = default;
        }

        // 충돌 세부 사항을 설명하는 구조체
        public struct Details
        {
            internal bool IsValid; // 세부 사항이 유효한지 여부

            // 1이면 정점 충돌, 2이면 모서리 충돌, 3 이상이면 면 충돌
            public int NumberOfContactPoints;

            // 추정된 임펄스
            public float EstimatedImpulse;
            // 평균 접촉 지점 위치
            public float3 AverageContactPointPosition;

            // 생성자: 세부 사항을 초기화합니다.
            public Details(int numContactPoints, float estimatedImpulse, float3 averageContactPosition)
            {
                IsValid = (0 < numContactPoints); // 유효한 접촉점이 있을 경우에만 세부 사항이 유효함
                NumberOfContactPoints = numContactPoints;
                EstimatedImpulse = estimatedImpulse;
                AverageContactPointPosition = averageContactPosition;
            }
        }

        // 주어진 엔티티와 다른 엔티티를 반환하는 함수
        public Entity GetOtherEntity(Entity entity)
        {
            Assert.IsTrue((entity == EntityA) || (entity == EntityB)); // 엔티티 A 또는 B만 유효함
            return entity == EntityA ? EntityB : EntityA;
        }

        // 주어진 엔티티에서 다른 엔티티로 향하는 법선 벡터를 반환합니다.
        public float3 GetNormalFrom(Entity entity)
        {
            Assert.IsTrue((entity == EntityA) || (entity == EntityB)); // 엔티티 A 또는 B만 유효함
            return math.select(-Normal, Normal, entity == EntityB); // EntityB에 대해서는 반대 방향 법선을 반환
        }

        // 충돌 세부 사항이 유효한지 확인하고, 유효한 경우 세부 사항을 반환합니다.
        public bool TryGetDetails(out Details details)
        {
            details = CollisionDetails;
            return CollisionDetails.IsValid; // 세부 사항이 유효한지 여부를 반환
        }

        // 이벤트를 비교하는 함수
        public int CompareTo(StatefulCollisionEvent other) => ISimulationEventUtilities.CompareEvents(this, other);
    }
}

StatefulCollisionEventBufferAuthoringMonoBehaviour를 상속받는 클래스

이 클래스는 CalculateDetails라는 변수를 통해 세부 사항 계산 여부를 설정할 수 있도록 합니다.

StatefulCollisionEventBufferBakerBaker 클래스로, StatefulCollisionEventBufferAuthoring의 값을 기반으로 Entity를 생성합니다.

이 클래스에서 CalculateDetails 값이 true일 경우, StatefulCollisionEventDetails 컴포넌트를 해당 엔티티에 추가합니다.

StatefulCollisionEventDetailsIComponentData를 구현한 구조체로,

충돌 이벤트에 대해 세부 사항을 계산할지 여부를 설정하는 CalculateDetails 변수만을 포함하고 있습니다.

StatefulCollisionEvent는 충돌 이벤트를 나타내는 구조체로, EntityAEntityB를 비롯한 충돌 관련 정보를 포함합니다.

State는 충돌 상태를 추적하며, CollisionDetails는 선택적으로 세부 충돌 정보를 포함합니다.

Details 구조체는 충돌의 세부 사항을 설명하며, 접촉 지점 수, 추정 임펄스 및 평균 접촉 지점 위치를 저장합니다.


StatefulCollisionEventBufferSystem.cs

CollisionEventStatefulCollisionEvent로 변환하여 동적 버퍼에 저장하는 역할

using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics.Stateful;
using Unity.Physics;
using Unity.Physics.Systems;

// 이 시스템은 CollisionEvent 스트림을 StatefulCollisionEvent로 변환하여 Dynamic Buffer에 저장할 수 있도록 합니다.
// 변환을 위해서는 아래의 조건을 만족해야 합니다:
//    1) PhysicsShapeAuthoring 컴포넌트의 'Collision Response' 속성에서 'Collide Raise Collision Events' 옵션을 활성화하고,
//    2) StatefulCollisionEventBufferAuthoring 컴포넌트를 엔티티에 추가하여 (세부 사항 계산 여부도 선택),
//    또는, 만약 Character Controller에서 이를 원한다면:
//    1) CharacterControllerAuthoring 컴포넌트에서 'Raise Collision Events' 플래그를 활성화합니다.
[UpdateInGroup(typeof(PhysicsSystemGroup))] // 물리 시스템 그룹에 포함
[UpdateAfter(typeof(PhysicsSimulationGroup))] // 물리 시뮬레이션 그룹 후에 실행
public partial struct StatefulCollisionEventBufferSystem : ISystem
{
    private StatefulSimulationEventBuffers<StatefulCollisionEvent> m_StateFulEventBuffers; // StatefulCollisionEvent 버퍼
    private ComponentHandles m_Handles; // 컴포넌트 핸들

    // 아무런 작업을 하지 않는 컴포넌트. 일반적인 job을 사용하기 위해 만들어졌습니다. OnUpdate() 메서드에서 설명
    internal struct DummyExcludeComponent : IComponentData { };

    // 컴포넌트 핸들 구조체
    struct ComponentHandles
    {
        public ComponentLookup<DummyExcludeComponent> EventExcludes; // 이벤트 제외를 위한 컴포넌트 조회
        public ComponentLookup<StatefulCollisionEventDetails> EventDetails; // 이벤트 세부 사항을 위한 컴포넌트 조회
        public BufferLookup<StatefulCollisionEvent> EventBuffers; // 이벤트 버퍼 조회

        // 시스템 상태에 맞는 컴포넌트 핸들을 초기화합니다.
        public ComponentHandles(ref SystemState systemState)
        {
            EventExcludes = systemState.GetComponentLookup<DummyExcludeComponent>(true);
            EventDetails = systemState.GetComponentLookup<StatefulCollisionEventDetails>(true);
            EventBuffers = systemState.GetBufferLookup<StatefulCollisionEvent>(false);
        }

        // 시스템 상태에 맞는 컴포넌트 핸들을 업데이트합니다.
        public void Update(ref SystemState systemState)
        {
            EventExcludes.Update(ref systemState);
            EventBuffers.Update(ref systemState);
            EventDetails.Update(ref systemState);
        }
    }

    // 시스템 초기화
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        m_StateFulEventBuffers = new StatefulSimulationEventBuffers<StatefulCollisionEvent>(); // StatefulCollisionEvent 버퍼 할당
        m_StateFulEventBuffers.AllocateBuffers(); // 버퍼 할당
        state.RequireForUpdate<StatefulCollisionEvent>(); // 시스템이 업데이트 되도록 설정

        m_Handles = new ComponentHandles(ref state); // 컴포넌트 핸들 초기화
    }

    // 시스템 종료
    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {
        m_StateFulEventBuffers.Dispose(); // 버퍼 해제
    }

    // 충돌 이벤트 동적 버퍼를 지우는 작업을 수행하는 Job
    [BurstCompile]
    public partial struct ClearCollisionEventDynamicBufferJob : IJobEntity
    {
        public void Execute(ref DynamicBuffer<StatefulCollisionEvent> eventBuffer) => eventBuffer.Clear(); // 버퍼 초기화
    }

    // 시스템 업데이트 메서드
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        m_Handles.Update(ref state); // 컴포넌트 핸들 업데이트

        // 충돌 이벤트 동적 버퍼를 지우는 작업을 병렬로 스케줄
        state.Dependency = new ClearCollisionEventDynamicBufferJob()
            .ScheduleParallel(state.Dependency);

        // 이전/현재 버퍼를 교환
        m_StateFulEventBuffers.SwapBuffers();

        var currentEvents = m_StateFulEventBuffers.Current; // 현재 이벤트
        var previousEvents = m_StateFulEventBuffers.Previous; // 이전 이벤트

        // 충돌 이벤트와 세부 사항을 수집하는 작업을 스케줄
        state.Dependency = new StatefulEventCollectionJobs.
            CollectCollisionEventsWithDetails
        {
            CollisionEvents = currentEvents,
            PhysicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>().PhysicsWorld, // 물리 월드
            EventDetails = m_Handles.EventDetails // 이벤트 세부 사항
        }.Schedule(SystemAPI.GetSingleton<SimulationSingleton>(), state.Dependency);

        // 이벤트 스트림을 동적 버퍼로 변환하는 작업을 스케줄
        state.Dependency = new StatefulEventCollectionJobs.
            ConvertEventStreamToDynamicBufferJob<StatefulCollisionEvent, DummyExcludeComponent>
        {
            CurrentEvents = currentEvents,
            PreviousEvents = previousEvents,
            EventLookup = m_Handles.EventBuffers, // 이벤트 버퍼 조회
            UseExcludeComponent = false, // 제외 컴포넌트 사용 안함
            EventExcludeLookup = m_Handles.EventExcludes // 이벤트 제외 조회
        }.Schedule(state.Dependency);
    }
}

세부 작업:

  1. 컴포넌트 핸들 업데이트:
    m_Handles.Update(ref state)를 호출하여 컴포넌트 핸들을 최신 상태로 업데이트합니다.
  2. 충돌 이벤트 버퍼 초기화:
    ClearCollisionEventDynamicBufferJob을 병렬로 스케줄하여 충돌 이벤트 버퍼를 지웁니다.
  3. 이벤트 버퍼 교환:
    SwapBuffers()를 호출하여 현재와 이전 프레임의 이벤트 버퍼를 교환합니다.
    이후 새로운 이벤트를 currentEvents에 기록하고, 이전 이벤트는 previousEvents로 저장됩니다.
  4. 충돌 이벤트와 세부 사항 수집:
    StatefulEventCollectionJobs.CollectCollisionEventsWithDetails를 스케줄하여 충돌 이벤트와 관련된 세부 사항을 수집합니다.
    물리 월드 및 이벤트 세부 사항도 이 작업에서 처리됩니다.
  5. 이벤트 스트림을 동적 버퍼로 변환:
    StatefulEventCollectionJobs.ConvertEventStreamToDynamicBufferJob을 호출하여 충돌 이벤트를 동적 버퍼로 변환하고, StatefulCollisionEvent 버퍼에 기록합니다.

충돌 위치 쿼리

1. 충돌 이벤트에서 충돌 위치(World Position) 가져오기

public partial struct CollisionPositionLoggerSystem : ISystem
{
    public void OnUpdate(ref SystemState state)
    {
        foreach (var (collisionEvents, entity) in SystemAPI.Query<DynamicBuffer<StatefulCollisionEvent>>()
                     .WithEntityAccess())
        {
            foreach (var collisionEvent in collisionEvents)
            {
                if (collisionEvent.TryGetDetails(out var details))
                {
                    float3 worldPosition = details.AverageContactPointPosition;
                    Debug.Log($"Collision at {worldPosition} between {collisionEvent.EntityA} and {collisionEvent.EntityB}");
                }
            }
        }
    }
}

Query로 StatefulCollisionEvent 버퍼를 가져옴

  • SystemAPI.Query<DynamicBuffer<StatefulCollisionEvent>>()를 사용해 현재 프레임의 충돌 이벤트를 가져옴.

각 충돌 이벤트의 세부 사항 가져오기

  • collisionEvent.TryGetDetails(out var details)를 호출하여 충돌 세부 정보를 가져올 수 있는지 확인.
  • details.AverageContactPointPosition은 충돌 지점들의 평균 좌표로, 월드 좌표계에서의 위치를 제공.

디버그 로그 출력

  • Debug.Log()를 이용해 충돌 위치와 관련된 엔티티 정보를 출력.

2. 여러 개의 접촉점(Contact Points) 활용하기

만약 여러 개의 접촉점을 각각 확인하려면, CalculateDetails를 활성화하고 details.NumberOfContactPoints 값을 사용해야 합니다.

하지만, 현재 StatefulCollisionEventAverageContactPointPosition만을 제공하므로, 개별 접촉점의 좌표를 얻으려면 PhysicsWorld를 직접 사용해야 합니다.

이 경우, collisionEvent.CalculateDetails(ref PhysicsWorld)를 호출하여 접촉점 데이터를 직접 가져올 수 있습니다.

if (collisionEvent.TryGetDetails(out var details))
{
    Debug.Log($"Collision has {details.NumberOfContactPoints} contact points.");
}

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤