Physics Owner 지정하기

메이플스토리 월드에서의 기본적인 물리 연산 방식에 대해 알아봅시다. Physics Owner를 변경했을 때의 변화에 대해서도 알아봅시다.

##### 참고 가이드

[물리 사용하기](/docs?postId=757)

## 기본 물리 연산 방식

메이플스토리 월드는 최적화된 환경을 제공하기 위해 접속한 클라이언트 중 하나를 물리 연산의 주체로 임의 선정합니다. 임의로 선정된 클라이언트를 Host Client라고 칭합니다. 이 Host Client가 연산한 결괏값을 서버가 받아 온 뒤, 모든 클라이언트에게 전달해 동기화합니다.(`TransformComponent.position`, 강체의 위치, 속도 등) 이 연산 방식으로 인해 엔티티가 충돌한 뒤에 크리에이터가 `PhysicsContactEvent`에서 정의한 다음 명령이 모든 클라이언트에게 즉시 동기화되지 않습니다. 즉 메이플스토리 월드의 기본 물리 연산 방식은 강체들의 충돌 직후 즉시 무언가를 실행시킬 수 없습니다.

  1. 충돌이 Host Client에서 발생하면, 이 충돌 정보를 서버로 전송합니다.
  2. 서버에서 `PhysicsContactBeginEvent`가 발생합니다.
  3. 발생한 이벤트와 크리에이터가 작성한 다음 작업의 명령을 서버가 Host Client로 전달합니다.
  4. Host Client가 수행한 명령의 결과를 서버로 전달하고, 명령의 결과를 모든 클라이언트로 동기화합니다.



[png](1773385421236bc928b49892947e28f01412cef4384e4)

### 예제

예제를 통해 기본 물리 연산의 작동 방식을 살펴봅시다. 각기 다른 방향에서 같은 속도로 이동하는 눈덩이가 충돌하게 만들고 충돌 직후 반발 없이 바로 멈추도록 예제를 만들어 봅시다. 이 예제를 만들면 크리에이터는 강체들이 충돌한 직후 어떠한 반발도 없이 즉시 충돌한 지점에서 멈출 수 있을 거라고 생각합니다. 그러나 호스트 클라이언트가 연산한 값을 서버가 받은 뒤 모든 클라이언트에게 동기화해주는 과정에서 충돌 직후 약간의 떨림이 발생하는 것을 확인할 수 있습니다.

[gif](1773390295485056801bad3ec4fd4ba37fc379559362a)

  1. 새로운 SnowBall 모델을 생성합니다.

  2. 모델에 PhysicsRigidbodyComponent를 추가하고, GravityScale 값과 Restitution 값을 수정합니다.

     * GravityScale: 0
     * Restitution: 1

[png](1773385438260bac914e134e4403fb83577c2c6e71515)

  3. SnowBall 모델에 PhysicsColliderComponent를 추가하고, EnableContactEvent를 true로 설정합니다. [png](1773385451235e00f637d82ba4a56ac8dcbe87f515daa)

  4. 새로운 SnowBall 컴포넌트를 생성하고 SnowBall 모델에 추가합니다.

  5. SnowBall 컴포넌트에 강체가 충돌하면 속도가 0이 되도록 아래와 같이 작성합니다.
         
         @Component
         script CollisionStopper extends Component
         @EventSender("Self")
         handler HandlePhysicsContactBeginEvent(PhysicsContactBeginEvent event)
         --------------- Native Event Sender Info ----------------
         -- Sender: PhysicsColliderComponent
         -- Space: Server, Client
         ---------------------------------------------------------
         
         -- Parameters
         -- local ContactedBodyEntity = event.ContactedBodyEntity
         ---------------------------------------------------------
         if self:IsClient() then
             log("Client Collision Event")
         
         else 
             log("Server Collision Event")
             self.Entity.PhysicsRigidbodyComponent:SetLinearVelocity(Vector2.zero)
         end
         end
         end
         



    
    
    6. 새로운 **SnowBallShooter** 컴포넌트를 생성합니다.
    7. **map01**에 **SnowBallShooter**를 추가합니다.
    [png](17733877162544bcb02557c164aa0a41d164ad9f95313)
    8. **SnowBallShooter 컴포넌트**에 아래와 같이 작성합니다.
    서버에서 `SpawnService`를 통해 공을 스폰시키고, `ApplyLinearImpulse()`를 사용해  강체들이 충돌할 수 있도록 아래와 같이 작성합니다.
    
        ```lua
    @Component
    script RadialSnowballShooter extends Component
        @Sync
        property string Model_SnowBall = "model://9aa7c347-c258-47d1-b92a-35d078e1f092"
        property table Dirs = {}
    
        method void OnBeginPlay()
        self:InitDirs()
        self:StartGame()
        end
    
        method void InitDirs()
        self.Dirs[1] = Vector3(math.sqrt(3)/2, 1/2, 0)
        self.Dirs[2] = Vector3(0, -1,0)
        self.Dirs[3] = Vector3(-math.sqrt(3)/2, 1/2, 0)
        end
    
        @ExecSpace("ServerOnly")
        method void StartGame()
        wait(2)
        log("launch snowballs")
    
        --3방향에서 눈뭉치 발사
        self:SpawnAndShoot(1)
        self:SpawnAndShoot(2)
        self:SpawnAndShoot(3)
        end
    
        @ExecSpace("ServerOnly")
        method void SpawnAndShoot(integer index)
        local myBall = _SpawnService:SpawnByModelId(self.Model_SnowBall, "ball"..tostring(index) , self.Dirs[index]*-6, self.Entity)
        myBall.PhysicsRigidbodyComponent:SetClientAsPhysicsOwner(_UserService.Users.Keys[1])
        myBall.PhysicsRigidbodyComponent:ApplyLinearImpulse(self.Dirs[index]:ToVector2()*4)  
        end
    end
    

## Physics Owner 지정

Physics Owner는 특정 강체의 물리 연산의 주체를 지정하기 위해 사용합니다. PhysicsOwner를 지정하게 되면 지정된 연산 주체가 물리 연산과 충돌 이벤트를 처리하게 됩니다. Server, 특정 Physics Owner Client, ClientOnly를 Physics Owner로 지정할 수 있고, 연산 결과는 TransformComponent 업데이트에 우선 적용됩니다. 크리에이터가 제작하는 월드의 성격에 따라 Physics Owner를 적절하게 지정하면 충돌 판정 오류, 충돌 지연, 월드 전체 성능 저하 등의 문제를 해결하고, 최적화된 월드를 제작하는데 도움이 됩니다.

### Physics Owner가 Client일 때

`SetClientAsPhysicsOwner()`를 사용해 특정 강체의 물리 연산을 특정 클라이언트에 위임할 수 있습니다. 강체의 물리 연산을 하는 특정 클라이언트는 Physics Owner Client가 됩니다. `PhysicsContactBeginEvent`가 Physics Owner Client와 Server에서 발생하기 때문에 지정된 강체의 충돌 타이밍을 Physics Owner Client가 정확하게 알 수 있습니다. 예를 들어 아래 그림과 같이 3개의 강체가 지면을 향해 떨어지게 만든다고 했을 때 각 강체마다 Physics Owner Client를 각각 지정했다고 생각해봅시다. 이 경우 각각의 클라이언트에 지정된 강체에서 발생한 충돌 이벤트 발생 시점을 보다 더 정확하게 측정할 수 있게 됩니다.

[png](1773644417701290752d1e5e44fc9935dad6dd5f0ad91) [gif](17736443892644623df0598bc4d10b3daeab74de68e9f)

아래 로그처럼 Client의 값이 Server의 값보다 더 정확하게 나오는 것을 확인할 수 있습니다. Server의 Y축 로그 값은 강체가 튀어 오른 후의 값이기 때문입니다. [png](1773736003314057cdf9d44724cc2bdfc8583be24a3fc)

> Tip. Physics Owner Client가 지정된 강체들끼리 충돌하는 경우 각 클라이언트의 연산 시점이 다르기 때문에 예상치 못한 결과나 값이 나올 수 있습니다. 그러므로 Physics Owner Client를 지정할 때는 해당 강체가 어떤 대상과 충돌하는지 확인하고, Physics Owner Client가 지정된 강체끼리의 충돌은 피하기를 권장합니다.

### Physics Owner가 Server일 때

`SetServerAsPhysicsOwner()` 사용해 Physics Owner를 Server로 설정하면 서버에서 충돌 이벤트 전달, 명령 전달 등 모든 물리 연산과 관련된 처리를 서버에서 처리합니다. 충돌 후의 명령을 바로 수행하고, 동기화할 수 있기 때문에 충돌 시 지연되는 모습이 나타나지 않습니다. 그러나 서버에서 연산을 과도하게 처리할 경우 부하가 발생해 월드가 느려지게 될 수 있습니다. 서버에서 물리 연산을 할 경우 불필요한 연산을 최소화하고, 프로파일러와 디버깅을 활용해 월드의 성능을 최적화하는 작업을 반드시 거치기를 권장합니다.

예를 들어 기본 물리 연산의 예제에서 SnowBallShooter 컴포넌트의 `SpawnAndShoot()` 메소드에서 `SetServerAsPhysicsOwner()`로 수정했다고 생각해 봅시다. 아래처럼 코드를 변경하면 물리 연산을 서버에서 진행하게 됩니다. 강체들이 충돌한 순간 발생하는 `PhysicsContactBeginEvent`가 서버에서 처리되기 때문에 강체들이 즉시 멈추는 것을 확인할 수 있습니다.

[gif](177339315661688ca00b333aa46a39ea4e3aeac32898d)
    
    
    @Component
    script BallLauncher extends Component
        @ExecSpace("ServerOnly")
        method void SpawnAndShoot(integer index)
        local myBall = _SpawnService:SpawnByModelId(self.Model_SnowBall, "ball"..tostring(index) , self.Dirs[index]*-6, self.Entity)
        myBall.PhysicsRigidbodyComponent:SetServerAsPhysicsOwner()
        myBall.PhysicsRigidbodyComponent:ApplyLinearImpulse(self.Dirs[index]:ToVector2()*4)  
        end
    end
    

### ClientOnly로 물리 연산을 할 경우

PhysicsColliderComponent의 `ClientOnly` 프로퍼티를 true로 설정하면 클라이언트에서만 프로퍼티와 메소드를 조작할 수 있습니다. `PhysicsContactBeginEvent` 또한 클라이언트에서만 발생합니다. 각각의 클라이언트는 실시간으로 동기화된 화면을 볼 수 없게 됩니다. 따라서 함께 월드를 사용하고 있더라도 엔티티가 다른 위치에 있을 수 있습니다.

[png](177338552149387dce4cb11224832baa63fe841f654bf)