다양한 물리 joint 활용하기

# 학습 과정 소개

물리를 사용하는 맵에서 특정 엔티티끼리의 운동 방식을 설정하는 방법을 알아봅시다. 다양한 JointComponent를 활용해 다양한 운동을 하는 엔티티를 만들어 월드 제작에 활용할 수 있습니다.

# Joint

Joint란 물리 법칙을 적용한 맵에서 특정 엔티티끼리만 연결해 서로 영향을 주고받을 수 있게 만드는 특정 연결부를 의미합니다. 아래의 그림과 같이 Entity A와 Entity B가 서로 영향을 주고받을 수 있도록 하는 연결부가 바로 Joint입니다. 기준 엔티티인 Entity A에 Joint 연결 위치(LocalAnchor A)를 설정하고 연결할 엔티티(TargetEntityRef)인 Entity B에도 Joint 연결 위치(LocalAnchor B)를 설정하면 Entity A와 Entity B가 연결됩니다.

[png](1663837353395619ae9a35f0a47a481baf3f797bb5446)

Anchor의 위치는 엔티티의 ColliderOffset을 기준으로 떨어진 정도를 설정합니다. 서로 연결된 엔티티는 크리에이터의 의도에 따라 충돌할 수도 있고, 충돌하지 않을 수도 있습니다.

  * Entity A: JointComponent를 가진 엔티티로 Joint를 추가해 다른 엔티티를 TargetEntityRef로 사용할 수 있습니다.

    * ColliderOffset: 엔티티의 PhysicsColliderComponent의 ColliderOffset입니다. LocalAnchor 위치의 원점으로 활용됩니다.
    * LocalAnchorA: JointComponent를 가진 Entity A의 Anchor를 설정합니다. Entity A의 ColliderOffset을 원점으로 합니다.
  * Entity B: Joint를 연결할 엔티티로 Entity A의 JointComponent에서 TargetEntityRef로 지정합니다.

    * TargetEntityRef: Joint를 연결할 Entity입니다.
    * LocalAnchorB: Joint를 연결할 Entity인 Entity B의 Anchor를 설정합니다. Entity B의 ColliderOffset을 원점으로 합니다.



[png](16638373662946c98c73c26ec4d78933f46034b0179c2)

#### Anchor

Joint가 동작하는 기준점입니다. 엔티티의 ColliderOffset 위치를 Anchor의 원점으로 삼습니다. 예를 들어 아래 그림처럼 LocalAnchor A, B의 값이 동일하게 (3,0)이라도 기준이 되는 엔티티의 offset이 다릅니다. LocalAnchor는 다른 곳에 위치하고, Entity A, B 동작의 기준점은 지점입니다.

LocalAnchor A의 기준 Offset은 Entity A이고 LocalAnchor B의 기준 Offset은 Entity B입니다.

[png](1663839068005e7743548cf8e44349be58cdf0cbf9300)

[gif](16638363593571f7fb2dde7e7440cb0c1b9447f4123e4)

#### Motor

Motor를 사용하면 Entity에 특정 방향으로 운동하도록 힘을 가합니다. RevoluteJointComponent, WheelJointComponent, PrismaticJointComponent에서 사용합니다. 모터를 사용하기 위해 MotorEnable 프로퍼티를 true로 설정하면 목표 속도에 맞게 엔티티가 돕니다. 목표 속도란 MotorSpeed의 값을 의미합니다. 예를 들어 MotorSpeed가 100이고, 속도가 100에 도달한 상태에서 MotorSpeed 값이 10으로 줄게 되면 속도가 감소합니다.

[gif](16638363279474aea43c2e0f849e78e12f9371b760269)

# JointComponent 종류

6가지 DistanceJointComponent, RevoluteJointComponent, PrismaticJointComponent, PulleyJointComponent, WeldJointComponent, WheelJointComponent가 있습니다. 종류마다 다른 특성을 가지고 있으므로 크리에이터가 제작하고 싶은 의도에 맞는 JointComponent를 사용해야 합니다.

#### DistanceJointComponent

DistanceJointComponent는 두 엔티티의 거리 관련 제한을 설정합니다. LocalAnchorA, B 사이의 거리(Length)가 일정하게 유지됩니다.

[{"index":"주요 프로퍼티","0":"Length"},{"index":"설명","0":"Joint로 연결된 두 Entity가 유지할 거리를 설정합니다."}]

[gif](1660634671012e4dbb294cfa94678b6af5dfc7804bc7f)

#### RevoluteJointComponent

RevoluteJoint는 엔티티의 회전과 관련된 제한을 설정합니다. UseLimits를 활성화한 후, LowerAngle, UpperAngle를 활용해 회전 각도를 제한할 수 있습니다. 엔티티가 회전할 때는 LocalAnchorA와 B가 만나는 지점을 공통의 Anchor로 사용합니다. 또한 Motor를 사용하면 Anchor를 기준으로 회전하는 힘을 받을 수 있습니다.

[{"주요 프로퍼티":"UseLimits","설명":"상대적 각도에 제한을 설정할지 여부를 결정합니다."},{"주요 프로퍼티":"LowerAngle","설명":"최소 상대 각도를 설정합니다. Entity의 Local Vector2(1, 0)을 기준으로 합니다."},{"주요 프로퍼티":"UpperAngle","설명":"최대 상대 각도를 설정합니다. Entity의 Local Vector2(1, 0)을 기준으로 합니다."},{"주요 프로퍼티":"MotorSpeed","설명":"Motor의 목표 속도를 설정합니다. 양수일 경우 CCW(반시계) 방향으로 회전합니다."}]

[gif](1660634686437edf5ce687bf04396952bf91d804c774c)

#### PrismaticJointComponent

PrismaticJointComponent는 엔티티가 축을(LocalAxis) 따라 엔티티가 선형 운동하도록 제한을 설정합니다. LowerTranslation, UpperTranslation 값을 설정해 엔티티가 이동하는 최대, 최소 거리를 제한할 수 있습니다. LocalAxis의 위치는 LocalAnchor를 기준으로 합니다.

[{"주요 프로퍼티":"UseLimits","설명":"상대적 이동 거리 제한을 둘지 여부를 설정합니다."},{"주요 프로퍼티":"LowerTranslation","설명":"최소 상대적 이동 가능 거리를 설정합니다."},{"주요 프로퍼티":"UpperTranslation","설명":"최대 상대적 이동 가능 거리를 설정합니다."},{"주요 프로퍼티":"MotorEnable","설명":"Motor를 사용할지 여부를 설정합니다. True일 경우 연결된 Entity가 LocalAxis 방향의 힘을 받습니다."},{"주요 프로퍼티":"MotorSpeed","설명":"Motor의 목표 속도를 설정합니다. MotorEnable이 True여야 동작합니다."}]

[gif](16606347060771a318324cf13474ba3e52d4b6f847f92)

#### PulleyJointComponent

PulleyJointComponent는 LocalAnchor와 GroundAnchor 사이의 거리들의 총길이 합이 일정하게 유지됩니다. 특정 엔티티의 선 길이가(Length) 짧아지면 다른 엔티티의 선 길이가 길어지며 선의 총길이는 유지됩니다. 총길이는 Entity A와 GroundAnchorA 사이의 최초 거리 + Ratio * Entity B와 GroundAnchorB 사이의 최초 거리 공식에 따라 정해집니다. 각 엔티티의 선은 LocalAnchor와 GroundAnchor 사이의 거리로 GroundAnchor의 원점은 LocalAnchor의 위치입니다. Ratio는 Entity와 GroundAnchor 사이의 거리가 줄어드는 비율을 결정합니다. 만약 Ratio가 3일 때 Entity A의 선이 3만큼 줄어든다면, Entity B와의 선은 1만큼 늘어나도 Entity A의 선은 Entity B의 선에 비해 구속력(constraint force)이 1/3입니다.

[{"index":"주요 프로퍼티","0":"Ratio"},{"index":"설명","0":"PulleyJoint를 이루는 양쪽 가상의 줄의 구속력 비율을 설정합니다."}]

[gif](1660634724636c6a1887dec234a97ad13994d906a7ebc)

#### WeldJointComponent

연결된 엔티티끼리 함께 움직이도록 설정합니다. 마치 꼬챙이에 양 끝을 끼운 것처럼 서로 일정한 거리가 벌어져 있는 상태로 움직입니다. 연결된 엔티티는 상대적 이동 및 회전이 제한됩니다.

[gif](1660634741001b728a6793af4499e8f64070bcf551639)

#### WheelJointComponent

WheelJoint는 연결된 엔티티가 바퀴처럼 동작하도록 제한을 설정합니다. Motor를 사용한 TargetEntityRef는 MotorSpeed 값을 목표 속도로 사용합니다.

[{"주요 프로퍼티":"MaxMotorTorque","설명":"목표 모터 속도에 도달하기 위한 Joint의 모터에 적용할 수 있는 최대 힘을 설정합니다."},{"주요 프로퍼티":"MotorEnable","설명":"Motor를 사용할지 여부를 설정합니다. True일 경우 연결된 Entity가 LocalAxis 방향의 힘을 받습니다."},{"주요 프로퍼티":"MotorSpeed","설명":"Joint의 목표 모터 속도를 설정합니다. MotorEnable이 True여야 동작합니다."}]

[gif](16606347549715160d2650c9c4197a2b2b2ba189ecc6a)

# Joint 추가하기

JointComponent를 추가하는 방법은 모두 동일합니다.

  1. PhysicsRigidbodyComponent, PhysicsColliderComponent를 추가한 엔티티를 두 개 생성하고, 배치합니다.
  2. 사용하고 싶은 JointComponent를 추가합니다. Size에 생성할 Joint 개수를 입력합니다. [png](1690259361892e5e2bdd02e44452fb93da6ac7a30c4d6)
  3. TargetEntityRef로 사용할 엔티티 선택한 후, LocalAnchor A, B의 위치를 설정합니다. [png](1690259508600e65c1ca226c84bcfbdfa719299acb7a5)



# 활용 예제

RevoluteJointComponent를 활용해 공을 튀기는 간단한 게임을 만들어 봅시다. [gif](1660804708258db7c651cc14d4beb87fc23eee95cc3c8)

#### 준비

  1. 타일맵모드를 RectTile로 변경합니다. [png](16902702934039459adc7e8234eb4950ba11f2d11493f)
  2. Hierachy - World - maps - map01 - PhysicsSimulatorComponent를 추가합니다.
  3. Hierachy - World - maps - map01 - RectTileMap - RectTileMapComponent - PhysicsInteractable을 활성화합니다.
  4. 공이 이리저리 부딪힐 수 있도록 Static 타입 물리 엔티티를 배치합니다. 아래가 뚫린 사다리꼴 모양으로 배치해 봅시다. 
     * SpriteRUID: 1ce2eff829ae4346bc89a3f0a357ac0e [png](169026003093236f6d51367a14dd4a906324c452a0d7d)



#### 공 만들기

  1. 새로운 Ball 모델을 만들고, PhysicsRigidbodyComponent, PhysicsColliderComponent를 추가합니다. 
     * Ball SpriteRUID: 164e94bea20441adac2af9c6f800bdf3
  2. PhysicsRigidbodyComponent의 BodyType을 Dynamic으로 변경합니다.
  3. PhysicsColliderComponent의 ColliderType을 Circle로 변경하고, 스프라이트 크기에 맞게 CircleRadius를 조절합니다.
  4. 중력을 받아 공이 떨어질 수 있도록, 공중에 배치합니다.



#### 장애물 만들기

  1. 새로운 Obstacle 모델을 만들고, PhysicsRigidbodyComponent, PhysicsColliderComponent를 추가합니다. 
     * SpriteRUID: 3ada0e4cc7f44cb988932f1b183ec778
  2. PhysicsRigidbodyComponent의 BodyType을 Static으로 변경합니다.
  3. PhysicsColliderComponent의 ColliderType을 Circle로 변경하고, 스프라이트 크기에 맞게 CircleRadius를 조절합니다.



#### 레버 만들기

  1. 새로운 LeftEntity, LeftBar 오브젝트 엔티티를 만들고, PhysicsRigidbodyComponent, PhysicsColliderComponent를 추가합니다.

     * LeftEntity SpriteRUID: e75cac80f719471fa482195531ed57cc
     * LeftBar SpriteRUID: 0308ac89b10843fcab6384aa43f6ae22
  2. 원형 모양 엔티티에 RevoluteJointComponent를 추가하고, Joint에 1을 입력합니다. TargetEntityRef에 LeftBar를 지정하고, LocalAnchorB, LowerAngle, UpperAngle, MotorSpeed, UseLimits 값을 아래와 같이 지정합니다. [png](16902694608421c03f7e8bfc44e6eba274aefaef0bbf7)

  3. 위의 두 엔티티를 복사해 반대쪽을 만듭니다. [png](1690269890755b731c1c566234f999b810610411cf45f)

  4. 새로운 Swing 스크립트 컴포넌트를 생성하고, DefaultPlayer에 추가합니다.

  5. N, M을 눌렀을 때 RevoluteJoint의 모터를 사용하고, 떼면 사용하지 않도록 아래와 같이 입력합니다.
         
         @Component
         script KeyboardMotorControl extends Component
         @Sync
         property Entity Left = "/maps/map01/LeftEntity"
         @Sync
         property Entity Right = "/maps/map01/RightEntity"
         
         @ExecSpace("ServerOnly")
         method void MoveLeft(boolean enable)
         self.Left.RevoluteJointComponent:SetMotorEnable(1, enable)
         end
         
         @ExecSpace("ServerOnly")
         method void MoveRight(boolean enable)
         self.Right.RevoluteJointComponent:SetMotorEnable(1, enable)
         end
         
         @EventSender("Service", "InputService")
         handler HandleKeyDownEvent(KeyDownEvent event)
         -- Parameters
         local key = event.key
         --------------------------------------------------------
         if key == KeyboardKey.N then
             self:MoveLeft(true)
         elseif key == KeyboardKey.M then
             self:MoveRight(true)
         end
         end
         
         @EventSender("Service", "InputService")
         handler HandleKeyReleaseEvent(KeyReleaseEvent event)
         -- Parameters
         local key = event.key
         --------------------------------------------------------
         if key == KeyboardKey.N then
             self:MoveLeft(false)
         elseif key == KeyboardKey.M then
             self:MoveRight(false)
         end
         end
         end
         



    
    
    7. 시작을 눌러 N, M을 눌렀을 때 막대가 각각 움직이는지 확인합니다.
    
    ##### 참고 가이드
    * [물리 사용하기](/docs?postId=757)
    * [충돌 그룹 만들기](/docs?postId=754)
    * [엔티티에 물리 적용하기](/docs?postId=761)