게임엔진/DOTS

Unity.Physics에 가속도와 힘 구현해보기 - 2

tsyang 2023. 3. 12. 02:34

2023.02.18 - [유니티/DOTS] - Unity.Physics에 가속도와 힘 구현해보기 - 1

 

Unity.Physics에 가속도와 힘 구현해보기 - 1

2023.02.10 - [유니티/DOTS] - Physics 1.0 써보기 주의 : 개인적으로 시행착오 겪으며 구현해 보는 것이니 신뢰 ㄴㄴ , 주절거리는 글임 개요 Unity.Physics에서는 속도에 따른 Transform 계산과 각종 충돌 계산

tsyang.tistory.com

 

오류 수정


 

이전 글에서 내가 구현한 가속도로 중력을 만들었을 때 유니티 physics의 중력보다 더 빠르게 움직이는 현상이 있었다. 처음엔 시스템 업데이트 순서가 잘못됐거나 버그로 여겼는데 아니었다.

 

SystemAPI.Time.fixedDeltaTime이 당~연히

 


Physics 시스템이 돌아가는 Fixed Step Simulation System Group의 deltaTime이라고 생각했지만 .. 사실 저 값은 프로젝트에서 설정하는 값인 초기값 1/50초로 설정이 되어 있었고...

 

FixedStepSimulationSystemGroup의 DeltaTime은 위 프로퍼티로 얻어와야 한다 .. 아무튼 저걸 가져다 쓰거나 그냥 1/60초로 업데이트를 해주면 똑같이 동작한다.

 

 


 

 

힘의 구현


 

게임에서 쓰일 힘은 크게 세 가지로 정할 수 있다. 

 

  1. 특정 개체에 직접 적용되는 힘
  2. 모든 개체에 적용되는 힘
  3. 일정 영역에 적용되는 힘

1,2는 AI나 별도의 시스템에서 처리해주면 되므로 비교적 간단한다.

 

문제는 3. 일정 영역에 적용되는 힘인데, 이를 위해서는 일정 영역에 오브젝트가 닿았는지 안 닿았는지를 판단해야 하기 때문이다. 다행히 이런 충돌 문제는 Unity Physics의 Trigger를 이용하면 된다.

 

3번과 같은 힘의 적용은, 별도의 Force Entity를 생성한 뒤 이것이 다른 Entity와 충돌할 때 발생하는 TriggerEvent를 이용하여 가능하다.

 

 

 

Force Component

힘 컴포넌트의 구현은 간단하다. Vector3값만 가지고 있으면 된다. 

 

public struct ForceComponent : IComponentData
{
    public float3 Linear;
}
public class ForceAuthoring : MonoBehaviour
{
    public Vector3 Linear;
}

public class ForceBaker : Baker<ForceAuthoring>
{
    public override void Bake(ForceAuthoring authoring)
    {
        AddComponent<ForceComponent>(new ForceComponent
        {
            Linear = authoring.Linear,
        });
    }
}

 

 

 

 

 

ApplyingForceSystem

힘의 적용은 힘을 부여할 Entity가 Force Component를 가진 Entity와 충돌할 때 발생되는 TriggerEvent를 이용하여 적용한다. 

 

우선 TriggerEvent를 이용하기 위해서는 필요한 컴포넌트들에 대한 참조가 필요하다. 이런 참조를 ComponentLookup이라고 하는데 편의상 이런 ComponentLookup을 한 데 모아준 ComponentDataHandles를 정의해줄 것이다.

 

struct ComponentDataHandles
{
    public ComponentLookup<AccelerationComponent> AccelerationComponentGroup;
    public ComponentLookup<ForceComponent> ForceComponentGroup;
    public ComponentLookup<PhysicsMass> PhysicsMassComponentGroup;

    public ComponentDataHandles(ref SystemState state)
    {
        AccelerationComponentGroup = state.GetComponentLookup<AccelerationComponent>();
        ForceComponentGroup = state.GetComponentLookup<ForceComponent>();
        PhysicsMassComponentGroup = state.GetComponentLookup<PhysicsMass>();
    }

    public void Update(ref SystemState state)
    {
        AccelerationComponentGroup.Update(ref state);
        ForceComponentGroup.Update(ref state);
        PhysicsMassComponentGroup.Update(ref state);
    }
}

ApplyingForceSystem에서는 Acceleration와 Force 그리고 PhysicsMass (질량과 위치를 얻어오기 위함이다.)를 사용한다. 

 

public void OnUpdate(ref SystemState state)
{
    _handles.Update(ref state);
    state.Dependency = new ForceComponentTriggerJob
    {
        AccelerationComponentGroup = _handles.AccelerationComponentGroup,
        ForceComponentGroup = _handles.ForceComponentGroup,
        PhysicsMassComponentGroup = _handles.PhysicsMassComponentGroup,
    }.Schedule(SystemAPI.GetSingleton<SimulationSingleton>(), state.Dependency);
}

그리고 이렇게 SystemState를 이용하여 컴포넌트들을 업데이트하고 Job을 실행한다. 참고로 TriggerEvent를 병렬적으로 처리하는 건 안되는 것 같다.

 

 

ForceTriggerJob

[BurstCompile]
public partial struct ForceComponentTriggerJob : ITriggerEventsJob
{
    public ComponentLookup<AccelerationComponent> AccelerationComponentGroup;
    [ReadOnly] public ComponentLookup<ForceComponent> ForceComponentGroup;
    [ReadOnly] public ComponentLookup<PhysicsMass> PhysicsMassComponentGroup;

    public void Execute(TriggerEvent triggerEvent)
    {
        var entityA = triggerEvent.EntityA;
        var entityB = triggerEvent.EntityB;

        bool isForceComponentA = ForceComponentGroup.HasComponent(entityA);
        bool isForceComponentB = ForceComponentGroup.HasComponent(entityB);
        if(!isForceComponentA && !isForceComponentB) return;    

        bool isAccelComponentA = AccelerationComponentGroup.HasComponent(entityA);
        bool isAccelComponentB = AccelerationComponentGroup.HasComponent(entityB);
        if (!isAccelComponentA && !isAccelComponentB) return;

        bool isMassComponentA = PhysicsMassComponentGroup.HasComponent(entityA);
        bool isMassComponentB = PhysicsMassComponentGroup.HasComponent(entityB);
        if (!isMassComponentA && !isMassComponentB) return;

        if (isForceComponentA && isAccelComponentB && isMassComponentB)
            ApplyForce(entityA, entityB);

        if (isForceComponentB && isAccelComponentA && isMassComponentA)
            ApplyForce(entityB, entityA);
    }

    private void ApplyForce(Entity applier, Entity receiver)
    {
        var forceComponent = ForceComponentGroup[applier];
        var accelComponent = AccelerationComponentGroup[receiver];
        var massComponent = PhysicsMassComponentGroup[receiver];
        
        var accelVector = forceComponent.Linear * massComponent.InverseMass;

        accelComponent.Linear += accelVector;

        AccelerationComponentGroup[receiver] = accelComponent;
    }
}

 

 

실행결과 

흰 공은 질량을 가볍게하고 검은 공은 무겁게 했음.

 

 

 

인력

인력을 쓰려면 충돌한 두 엔티티의 위치 정보가 필요하다. 

 

따라서 LocalTransform을 쿼리에 추가하고 job을 돌려준다. 

 

인력 컴포넌트는 일반 힘 컴포넌트와 동일하지만 이름만 다르다.

public struct AttractiveForceComponent : IComponentData
{
    public float3 Linear;
}

 

Linear값을 두 엔티티 위치의 방향 벡터에 곱한다. 따라서 Linear값이 (1,1,1)이면 인력이 되고 (-1,-1,-1)이 되면 척력이 된다.

 

Job역시 동일하지만 힘을 적용하는 부분은 다음과 같이 조금 차이가 있다.

private void ApplyForce(Entity applier, Entity receiver)
{
    var forceComponent = AttractiveForce[applier];
    var accelComponent = AccelerationComponentGroup[receiver];
    var receiverMass = PhysicsMassComponentGroup[receiver];
    
    var applierTransform = LocalTransformGroup[applier];
    var receiverTransform = LocalTransformGroup[receiver];
    var positionVector = applierTransform.Position - receiverTransform.Position;
    if (!math.any(positionVector)) return;
    
    positionVector = math.normalize(positionVector);
    var accelVector = forceComponent.Linear * receiverMass.InverseMass * positionVector;
    
    accelComponent.Linear += accelVector;
    
    AccelerationComponentGroup[receiver] = accelComponent;
}

 

 

파랑은 척력, 빨강은 인력. 흰색은 가벼운 공, 검은색은 무거운 공이다.