반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리

Multi-Element Physics Body 다루기

2013. 1. 11. 05:51 | Posted by 솔웅


반응형
Posted on . Written by



이번주의 튜토리얼은 코로나 물리 엔진에 대한 겁니다. 특히 multi-element physics bodies 부분에 대해서 다룹니다.

첫번째로 multi-element body가 무엇인지 부터 알고 넘어가야 할 것 같습니다. multi-element body 는 두개 이상의 shapes 들이 모여서 구성된 physics body 입니다. raddoll 처럼 weld joint 같은 joint 로 연결된 physical 객체들의 집합으로 하나가 된 physical 객체를 말하는 것이 아닙니다. multi-element body 는 여러 shape 들로 구성되지만 전체가 하나로 통일되고 고정된 객체로 각각의 element들이 move 하거나 flex 하지 않습니다.



Why Multi-Element Bodies?


물리엔진을 많이 사용해 보신 분들은 다 아시는 얘기 일텐데요. 간략하게 더 설명하겠습니다. Box2D 에서는 모든 physical body 들은 반드시 오목한 angle들이 없는 최대 8개 면을 가진 다각형 모양으로 그려집니다.





볼록한 다각형은 괜찮습니다 하지만 볼록한 angle들로만 돼 있고 trace 될 수 없는 body 나 8개면 이하이면서 정확하게 trace 될 수 없는 body 라면 어떨까요? 이 룰을 따르지 않는다면 충돌시 일어나는 reaction 들은 어떻게 나올지 모르게 됩니다.


해결책은 multi-element body 입니다. 여러개의 볼록한 모양이 하나의 body 로 되서 trace 되는 모양이 기본적인 모양입니다. 여러분들은 간단한 shape들을 사용해서 multi-element bodies 들을 만들어야 합니다. 최소한의 shape 들을 사용해서요. 이 작업이 좀 어려울 것 같으면 3rd party tool인 PhysicsEditor가 이 작업을 쉽게 할 수 있도록 도와 드립니다.


Part I – Per-Element Collision Control


이미 이전에 multi-element bodies 작업을 해 보신적이 있으시면요, 아마 이 기능이 아주 유용하다는 것을 아실겁니다. 하지만 이 기능에도 약간의 제한사항들이 있는데요. 이 기능의 capabilities를 먼저 살펴보죠.


  1. 각 element 들은 유니크한 collision filters들을 가질 수 있습니다. 이 기능을 multi-element body의 일 부분이 다른 객체와 충돌하거나 할 때 어떤 reaction이 일어나도록 할 수 있습니다. 다른 부분은 충돌해도 아무 일이 일어나지 않구요. 
  2. 각각의 element들은 센서로 세팅할 수도 있습니다. 다른 객체들이 그 부분을 지나갈 때 어떤 일이 일어나도록 할 수 있죠. 충돌효과를 줄 수도 있지만 그냥 지나가도록 할 수도 있고 이 때 어떤 일이 일어나도록 할 수 있습니다.
  3. 충돌시 각 element는 사전에 physics addBody() function에서 지정된 고유의 숫자를 return 할 수 있습니다. 예를 들어 첫번째 element 는 1 두번째는 2 등으로 지정할 수 있죠.


이러한 unique capability들도 있지만 아래와 같은 제한사항들도 있습니다.


  1. 일단 collision가 filter element나 body에 정의되면 Runtime 동안은 바뀔수가 없습니다.
  2. 만약 element 가 센서로 정의돼었다면 Runtime 동안 각 element 별로 바꿀 수 없습니다. 다만 전체 body 가 sensor 나 non-sensor 로 바뀔 수 있을 뿐입니다.


Overcoming These Limitations


근데 너무 실망하지는 마세요. 오늘 이 튜토리얼에서 그 두가지 제한사항들을 어떻게 극복할지도 보여드릴테니까요. 우리는 아 작업을 physics contact를 사용해서 구현할 겁니다. 최근 있었던 튜토리얼에서 이 기능을 소개해 드린적이 있는데요. 여기로 가시면 그 글을 보실 수 있습니다.


physics contact를 사용해서 사전에 결정할 수 있습니다. pre-collision listener 를 사용해서 실제 충돌이 일어날 때 어떤 일이 일어날지를 결정할 수 있단 겁니다. 여러분 앱의 로직ㅇㅔ 근거해서 전체 collision을 void  하도록 할 수 있거든요. 이 튜토리얼에서는 이 기능을 multi-element bodies 에서 활용해 보겠습니다.



아래 “space nebula,” 를 사용할 건데요. 소설을 써 보면 star-fighter 는 우선 먼저 저 모서리에 있는 shield 를 부숴야 합니다. 그 다음에 가운에에 있는 대마왕을 없애야 해요. 이 시나리오를 구혀하기 위해서는 전통적인 방법으로는 아래와 같은 제한사항들이 있습니다. 



  1. 만약에 전통적인 방법인 joint를 사용해서 구현한다면 한쪽 모서리가 파괴 됐을 때 나머지 부분들의 joint 연결이 불안정해 지게 되는 문제점이 있습니다.
  2. object.isSensor를 사용하면 all or nothing 이 됩니다. 그러니까 파괴된 한 부분만 센서로 바꿀 수 없습니다.


이런 제약사항때문에 전통적인 joint 방법이 아닌 physics contact를 사용할 겁니다. per-element collision detection을 사용해서 이 destructible shield 이슈를 해결해 보겠습니다.




Assembling the Nebula


Let’s examine how to create a multi-element body in Corona. We’ll create a 9-element body to trace the nebula. In Corona, we simply do this:

일단 코로나에서 어떻게 multi-element body를 생성하는지 공부해 봅시다. 우리는 nebula 를 trace 하기 위해 9개의 element body 를 만들겁니다. 코로나에서는 이를 위해 아래와 같이 하면 됩니다.


  • nebula image 를 스크린에 display 시킨다.
  • nebula에 대한 shape 을 정의한다. top 을 시작점으로 해서 필요한 작업을 한다. outlying pod에 대해서는 8각형을 사용해야 합니다. 왜냐하면 multi-element body 에서는 radial shape 을 offset 할 수 없거든요. 정확하게 원이 아니니까요. 팔각형으로 충돌에 대한 자연스러운 반응을 이끌어 내기에도 문제가 없을 겁니다.
  • physical body 를 add 하고 element들의 순서에 따라 API 에 각 shape 들을 pass 합니다. 순서가 중요합니다. collision detection 에서 숫자를 return 할 거거든요.


local nebula = display.newImage( "nebula.png" )
nebula.x, nebula.y = display.contentWidth/2, display.contentHeight/2
local podT = {1,-89, 14,-83, 20,-70, 14,-57, 1,-51, -12,-57, -18,-70, -12,-83}
local beamTR = {19,-63, 63,-19, 59,-14, 14,-59}
local podR = {69,-20, 82,-14, 88,-1, 82,12, 69,18, 56,12, 50,-1, 56,-14}
local beamBR = {19,61, 14,56, 58,13, 62,17}
local podB = {1,49, 14,55, 20,68, 14,81, 1,87, -12,81, -18,68, -12,55}
local beamBL = {-18,63, -64,17, -59,13, -14,58}
local podL = {-70,-20, -57,-14, -51,-1, -57,12, -70,18, -83,12, -89,-1, -83,-14}
local beamTL = {-18,-65, -14,-61, -59,-15, -64,-20}

physics
.addBody( nebula, "dynamic",
{shape=podT},
{shape=beamTR},
{shape=podR},
{shape=beamBR},
{shape=podB},
{shape=beamBL},
{shape=podL},
{shape=beamTL},
{radius=24} --radial body used for the nucleus
)
local shieldStates = { true, true, true, true, true, true, true, true }


이제 맨 마지막에 이 8개의 shield 객체들을 관리하기 위해 간단한 shieldStates table을 셋업하셔야 합니다. 이 테이블은 나중에 어떤 특정 element 를 on/off 시킬때 사용할 겁니다. 그리고 shield element 가 inact인지 destroyed 돼 있는지를 track 할 때도 사용할 거구요. 이를 위해 8개의 boolean 값을 가진 간단한 non-indexed table 을 사용합니다.



The Basic Pre-Collision Listener



그 다음으로는 기본적인 pre-collision listener를 정의할 겁니다. 이전 튜토리얼에서 설명했듯이 물리적인 접촉을 utilize 하기 위해 pre-collision listener를 사용해야 합니다. 왜냐하면 코로나에게 충돌이 일어나기 직전에 그 충돌상황을 관리하도록 해야 하기 때문이죠.


local function nebulaCollide( self,event )
  print( event.selfElement )
end
nebula.preCollision = nebulaCollide ; nebula:addEventListener( "preCollision", nebula )


이 함수는 이주 기본적인 구조로 돼 있습니다. 이제 nebula 와 어떤 것이라도 충돌하게 되면 해당 element가 정의 돼 있는 순서에 따라 event.selfElement로 그 element의 corresponding integer가 return 될 겁니다. 위에 있는 pod를 첫번째 element로 정의했죠. 그러니까 그 element는 1을 return 할 겁니다. 오른쪽 beam 은 2를 반환하고 오른쪽 pod 는 3을 반환하고 뭐 이런식으로 진행될 겁니다.


Enhancing the Pre-Collision Listener


이제 nebula 의 어떤 element 가 충돌했는지 알 수 있게 됐습니다. 이제 shieldStates table 을 이용해서 충돌을 처리할 지 말아야 할지를 처리할 수 있도록 만들어야 합니다. 만약 그 shield element 가 게임을 하다가 destroyed 됐다면 코로나에게 해당 충돌은 void 처리하라고 지시할 수 있습니다. (event.contact)


local function nebulaCollide( self,event )

--query the position (and state) from "shieldStates" table
local isElementIntact = shieldStates[event.selfElement]

if ( isElementIntact == false ) then
event.contact.isEnabled = false --use physics contact to void collision!
end
end
 

shieldStates table를 관리하는 것은 아주 간단합니다. shield pod 을 destroy 처리하려면 간단하게 아래 처럼 하면 됩니다.



shieldStates[5] = false


Building on this concept, you can now manage your nebula shields and enact other creative methods, including:

이제 여러분은 nebula shield를 관리할 수 있게 됐습니다. 그리고 아래 내용들을 포함해서 여러 효과들을 구현하실 수 있습니다.

  1. shield pod 이 destroy 됐다면 옆에 있는 beam 도 destroy 시킴.
  2. 특정 시간이 지나면 shield pod 을 rebuild 하고 옆의 beam 들도 다시 rebuild 함
  3. shieldStates table setup 을 확장해서 각 pod 의 에너지 양을 관리함.


per-element detection 과 함께 physics contact를 사용하면 전통적인 방법이 갖는 한계를 해결할 수 있다는 걸 알게 되셨을 겁니다.




Part II – Multi-Element Bodies and Sensors



이제 multi-element bodies와 관련되서 많이들 잘 못 이해하고 계시는 sensor 부분에 대해 얘기해 보겠습니다.


Corona 의 physics 에 대해서 경험이 있으신 분들은 어떤 모양이나 타입(dynamic, kinematic, or static)에서든 physical body 가 sensor 로서 동작할 수 있다는 것을 알 수 있을 겁니다. sensor 는 부딪혀서 튀긴다던가 하는 physical sense 는 없습니다.


multi-element bodies와 관련해서 많이 오해하기 쉬운게 뭐냐하면 모든 element 들은 seosor로서 colision event를 return 합니다. body 가 전체 통으로 돼 있는 객체라면 그 전체가 충돌을 체크하는 포인트가 되죠. 여기서 헛갈릴 수 있는 것이 sensor 에서는 충돌시 여러개의 began phase event를 받을 수 있게 된다는 거죠. 혹은 센서 지역 밖에 있는데도 작은 부분이 걸쳐 있어서  ended phase를 받을 수도 있습니다.


사실 센서는 원래 그러기 위해서 만들어 졌습니다. 예를 들어서 트랙을 달리는 경주용 자동차의 앞 바퀴에만 센서를 달 필요가 있는 경우가 있을 겁니다. 조종석은 트랙이 있더라고 앞바퀴만 통과하면 어떤 결과를 일으켜야 되는 경우가 있죠. 또 다른 경우는 multi-element body 전체가 센서 지역의 안에 혹은 밖에 있는 것을 체크해야 될 때도 있습니다. “jumping fish” 를 보죠. 물고기는 완전히 물 안에 존재해야만 하죠? 이것도 센서로 정의할 수 있을 까요?



Counting the Collisions






이 jumping fish 시나리오는 fish의 body 에 있는 각각의 element들에 대해 어떤 충돌이 일어나는지를 counting 함으로서 해결할 수 있습니다. 이를 위해 값들이 들어갈 테이블을 만들고 그 이름을 elementStates라고 할 겁니다. began phase 에서 해당 count를 1씩 증가시키고 ended phase 에서는 1씩 감소 시킬 겁니다. 각 element들은 센서와 함께 collision event 를 return 할 겁니다. 그래서 만약 한 element에 4개의 센서들이 overlap 된다면 count는 4가 되겠죠. 만약 그 센서들 중 3개가 밖으로 나간다면 count 는 1로 줄겠죠. element count 가 0이 될 때는 전체가 센서 밖에 나가 있다는 것을 말합니다. 그러면 물고기가 완전히 물밖으로 나간 상태고 물고기가 점프했다고 볼 수 있겠죠.



또한 우리는 elementsIn를 만들어서 물고기의 전체 element들 중에 얼마나 많이  센서 범위 안이나 밖에 있는지를 count 할 겁니다. 이 값은 절대 5를 넘지 않을 겁니다. 왜냐하면 물고기는 5개의 element들로 구성될 거기 때문이죠.

마지막으로 inWater라는 boolean flag 를 정의할 겁니다. 이로 인해서 완전히 물 속에 있는지 물 밖에 있는지를 알 수 있게 되죠. 코딩을 편하게 하기 위해서 이 세개의 item들은 fish 의 properties 로 정의할 겁니다.

아래에 기본적인 예제 코드가 있습니다.



local
fish = display.newImage( "jumpfish.png" )

fish.x, fish.y = display.contentWidth/2, display.contentHeight/2-200
local tail = {-117,12, -123,-46, -68,-13}
local bodyBack = {-89,-26, -61,-39, -20,-46, 20,-49, 42,27, -12,28, -66,16, -94,0}
local bodyFront = {20,-49, 71,-43, 107,-32, 121,-20, 126,-10, 108,5, 78,19, 43,27}
local finBack = {-39,23, -11,29, -10,41, -32,50}
local finFront = {-9,51, -11,28, 41,27, 15,42}

physics.addBody( fish, "dynamic",
{shape=tail},
{shape=bodyBack},
{shape=bodyFront},
{shape=finBack},
{shape=finFront}
)
fish.elementStates = { 0,0,0,0,0 } --table of per-element collision counts
fish.elementsIn = 0
fish.inWater = false



보시다시피 물고기의 element들은 Part 1에서 다뤘던 nebula 와 비슷하게 정의했죠? 거기에다가 추가적으로 table elementStates와 properties elementsIn and inWater 를 생성했습니다.


Managing the Count


이 물고기는 standard collision listener 가 필요합니다. pre-collision listener 가 아니라요. 이번에는 physics contact 기능을 사용하지 않을 겁니다.


local function fishCollide( self,event )

if ( event.phase == "began" ) then
if ( self.elementStates[event.selfElement] == 0 ) then
self.elementsIn = self.elementsIn+1
end
self.elementStates[event.selfElement] = self.elementStates[event.selfElement]+1
elseif ( event.phase == "ended" ) then
self.elementStates[event.selfElement] = self.elementStates[event.selfElement]-1
if ( self.elementStates[event.selfElement] == 0 ) then
self.elementsIn = self.elementsIn-1
end
end
if ( self.elementsIn == 0 and self.inWater == true ) then
self.inWater = false
print("FISH ENTIRELY OUT OF THE WATER!")
elseif ( self.elementsIn == 5 and self.inWater == false ) then
self.inWater = true
print("FISH ENTIRELY IN THE WATER!")
end
end
fish.collision = fishCollide ; fish:addEventListener( "collision", fish )

이제 단계별로 볼까요

began phase:

  1.  첫번째로 총돌한 element의 count가 0인지 체크합니다. 만약 0이면 이 element는 처음으로 센서지역으로 들어가는 element라는 것을 알수 있죠. 그리고 이 물고기의 전체 elementsIn count를 1 증가시킵니다.
  2. 그 다음으로는 특정 element의 count 를 1 증가 시킵니다.


ended phase:

  1. 충돌한 element의 count 에서 1을 뺍니다.
  2. 그 다음에 element의 count가 0인지 체크를 하죠. 만약 0이면 이 물고기는 전체 센서지역에서 완전히 밖에 있는 상태라는 것을 알 수 있습니다. 그리고 물고기의 total elementsIn count 에서 1을 줄입니다.


The conditional check:

  1. 물고기의 total elementsIn count 가 0인지 여부를 체크합니다. 이전에 물속에 있었는지 여부를 체크합니다. 두 조건 모두 pass 하면 물고기는 물밖에 완전히 나온 상태라는 것을 알 수 있죠. 그러면 inWater flag를 false로 세팅합니다.
  2. elseif 조건문은 물고기의 elementsIn count가 5인지를 체크합니다. 이전에 물에 완전히 들어가 있지 않은지 체크하죠. 두가지 조건이 pass 라면 이 물고기는 이 물고기는 완전히 물 속에 있다는 것을 알 수 있죠. 그러면 inWater flag를 true로 세팅합니다.


물고기가 완전히 물 속에 있는지 아니면 완전히 물 밖에 있는지에 대해 이렇게 sensor collision을 가지고 handle 할 수 있습니다 이 방법은 센서들을 overlapping 하고 neighboring 과도 같이 사용할 수 있습니다. 예를 들어 core body 로 물 센서 지역을 만들었고 그 위에 파도가 있다면 이 코드는 물고기의 모든 elements들을 이 센서들에 모두 적용 시킬 수도 있습니다.


In Summary


여기까지가 오늘의 튜토리얼입니다. 배우신대로 mutli-element physics bodies 기능으로 joint-assembled bodies 가 할 수 없는 효과를 줄 수 있습니다. 하지만 사용하시려면 몇개의 장애물은 넘어야 겠죠. 이 튜토리얼이 여러분이 앱을 만드시면서 닥치는 그런 장애를 극복하는데 도움이 되기를 바랍니다.


반응형