CODEONWORT

던파 좌표계 구현하기 본문

Season 1/플래시

던파 좌표계 구현하기

codeonwort 2012.01.25 20:44

네이버 카페 쉬프트에 올린 시리즈

http://cafe.naver.com/shiftouch/451283
http://cafe.naver.com/shiftouch/451429
http://cafe.naver.com/shiftouch/451478
http://cafe.naver.com/shiftouch/451964

----------------------------------------------------------------------------------------


위 그림과 같은 시점을 천지를 먹다, 닌자 베이스볼 같은 고전 게임에서 많이 볼 수 있습니다. 캐릭터는 땅에서 상하좌우 네 방향으로 걸을 수 있지만 위로 걸어서 화면상 더 뒤로 간다고 해도 캐릭터가 작아지는 일은 없습니다. 즉 원근감이 없는 것입니다. 또한 점프를 할 수 있습니다.

플래시의 2차원 x, y 좌표를 그대로 쓰면 위와 같은 좌표계를 구현하기 어렵습니다. 이번 글에서는 실제로 화면에 나타나는 y 좌표와 데이터로 저장되는 y 좌표를 달리하여 원하는 결과를 내보겠습니다.

일단 좌표축을 정해야 하는데 저는 바닥을 xy 평면으로 놓고 z축은 점프하는 방향으로 잡겠습니다.


보기 편하게 시점을 조금 기울였습니다. 실제로는 y 좌표가 증가하면 그냥 아래로 내려가지 위 그림처럼 기울어져 내려가지는 않습니다.

x 좌표는 그대로 오른쪽으로 가면 증가하고 왼쪽으로 가면 감소합니다.
y 좌표도 그대로 위로 가면 감소하고 아래로 가면 증가합니다. 그리고 y 좌표가 클 수록 더 앞에 있는 것입니다.
z 좌표는 점프를 하면 증가하고 낙하하면 감소합니다.

어떤 개체를 더 앞쪽에 표시하느냐는 y축에만 관련되며 z축과는 상관이 없습니다. 예를 들어 위 그림의 캐릭터를 A라 하고 그 앞에 다른 캐릭터 B가 서 있다 치면 B의 y 좌표가 더 크니 B가 앞에 보입니다.



이 상태에서 B가 점프하면 화면상으로 보이는 y 좌표는 B가 더 작습니다. 하지만 가상의 좌표계에서 B의 y 좌표는 그대로고 z 좌표만 변하니 여전히 B가 A의 앞에 표시됩니다.

* 주의: 용어 *
가상의 x, y, z 좌표계를 도입하고 나니 실제 플래시의 x, y 좌표와 헷갈릴 우려가 있어 가상의 좌표들은 _x, _y, _z라고 부르고 실제 좌표는 x, y라고 부르겠습니다.

플래시에서 표시 객체에는 원근이 적용되는 3D 효과를 위한 z 좌표가 이미 있기 때문에, z라는 이름을 그대로 쓸 수 없습니다. 그래서 무비클립의 하위 클래스를 만들어서 y, z 속성을 재정의할 것입니다. 클래스 문법을 아는 사람이 별로 없을 것 같아 고민했는데 개념만 알면 직접 작성할 수 있을 것이니 그냥 클래스 쓰기로 했습니다.



스테이지에 캐릭터를 그리고 무비클립 심벌으로 만들고 Character라는 클래스와 연결합니다. Character 클래스는 MovieCilp 클래스를 상속하도록 합니다. 인스턴스 이름은 crt를 씁니다.



가상의 두 좌표를 도입합니다.



_x = x이기 때문에 x는 전혀 건드리지 않습니다. y 좌표는 _y와 _z를 이용하여 계산할 것입니다. 생성자에서 _y에는 y 좌표를 그대로 대입하고 _z 좌표는 0으로 둡니다.



y, z 속성이 가상의 좌표를 나타내도록 바꿨습니다. 현 시점에서 crt.y = 100 이나 crt.z = 200 을 실행해도 화면에서는 아무 변화가 없습니다. 또한 (getter)y를 재정의했기 때문에 생성자에서는 _y = super.y로 고쳤습니다.

화면상 y 좌표가 바뀌는 것은 가상의 _y 좌표와 _z 좌표가 변할 때입니다. 그러니 재정의한 (setter)y와 (setter)z에서 y 좌표를 바꿔야 하는데, _y와 _z를 가지고 실제 y 좌표를 계산하는 방법을 먼저 알아야 합니다. 일단 _z = 0 이면 _y가 곧 y 좌표와 일치하는 것을 알 수 있습니다. _z = 10 이라면 캐릭터가 위로 조금 떠서 y = _y - 10 입니다. 간단히 y = _y - _z 임을 도출해낼 수 있고 이것을 적용해보겠습니다.



아래 플래시로 _x, _y, _z 좌표에 따른 x, y 좌표 변화를 확인할 수 있습니다.


글이 길어지니 이쯤에서 자르고 다음 편부터 _y 좌표를 이용한 깊이 정렬, _z 좌표를 고려한 충돌 검사를 다루겠습니다.

--------------------------------------------------------------------------------------------------

저번 글에서는 화면상 y 좌표를 가상의 _y 좌표와 _z 좌표로 분리하여 던파 좌표계를 흉내냈습니다. 이번 글에서는 _y 좌표를 이용하여 깊이 정렬을 해보겠습니다. 앞서 설명했듯이 _z 좌표와는 상관 없이 _y 좌표가 더 큰 물체가 앞에 보입니다.



스테이지에는 저번에 만든 Character 무비 클립 심벌을 두 개 놓고 각각 인스턴스 이름을 A, B로 지었습니다.



키보드 방향키를 누르면 A가 움직이는 코드를 작성합니다. 예전에 배포했던 Key 클래스를 활용하는데 이게 저번에 쉬프트에 올린 이후로 시간이 꽤 흐른 만큼 사용법이 조금 변했습니다. 조만간 문서화 더 꼼꼼히 해서 다시 올리겠습니다.


지금은 그저 A가 움직이는 코드만을 작성했기 때문에 좌표로 따지면 A가 B의 앞에 표시되어야 하는데도, 스테이지에서 B를 더 앞에 배치해놓았기 때문에 A가 항상 B에 가려집니다.



A의 _y 좌표가 B의 _y 좌표와 같거나 크면 A가 앞에 오도록, 아니면 B가 앞에 오도록 하였습니다. 지금 스테이지에는 그리기 도구로 그린 배경, A, B가 있는데 배경은 항상 A, B의 뒤에 보이니 무관하고 A.y >= B.y이면 A가 가장 높은 깊이를 차지하여 B의 앞에 보이고 A.y < B.y이면 반대로 됩니다.


위 방법은 스테이지에 캐릭터가 A와 B 둘만 있는 경우만 고려하여 짠 상당히 주먹구구식으로서 몬스터를 열 마리 쯤 넣는다면 전혀 써먹을 수 없습니다. 그래서 스테이지에 캐릭터가 임의의 수만큼 있는 경우를 고려하는 유연한 깊이 정렬을 구현해보겠습니다.



캐릭터들을 담는 컨테이너 용도의 Sprite 객체를 하나 만들어 루트에 추가합니다. 그려놓은 배경이 하나의 Shape 객체로 취급되기 때문에 캐릭터들만을 고려하기 위해 컨테이너를 별도로 만들었습니다. Character 객체 10개를 임의로 배치하고 컨테이너에 추가합니다. 우리가 조종할 A로는 컨테이너에 첫 번째로 들어간 캐릭터를 고릅니다.

이제 깊이 정렬을 해보겠습니다. 열 캐릭터의 _y 좌표를 가지고 정렬하여 _y 좌표가 낮은 순서대로 깊이 0, 1, 2, ..., 9에 배치하면 정렬은 성공합니다.



container 안의 캐릭터들을 ary 배열에 모두 담고 캐릭터들의 y값을 가지고 정렬합니다. 정렬 후에는 y 좌표의 값이 가장 낮은 순서대로 ary의 0, 1, 2, ..., 9번 인덱스에 배치됩니다. 이 순서를 그대로 깊이로 지정하면 됩니다.


아직까지는 _z 좌표를 건드리지 않아서 깊이 정렬이 _y 좌표에 의해서만 되는 것인지 확인할 길이 없습니다. 다음 글에서는 점프를 간단히 구현하여 이것을 확인해본 다음 충돌 검사와 관련된 문제를 다룹니다.

-------------------------------------------------------------------------------------------------- 

점프에 앞서 캐릭터가 밟는 땅을 표현하는 방법을 정해야 하는데 저는 Ground 클래스를 따로 만들겠습니다. Ground 클래스는 땅의 울퉁불퉁함을 표현하기 위해 좌표 (x, y)에서 땅이 얼마나 솟아올랐는지를 반환하는 메서드 heightAt()를 가집니다.



어느 지점에서나 0을 반환하게 만들어 둡니다. 지금은 어느 Ground 객체든 z = 0인 평지를 나타냅니다. 땅이 울퉁불퉁하면 xy 평면에서 움직이는 것도 손봐야하는데 일단 넘어갑니다.

Character 클래스에는 ground 변수를 추가하고
public var ground:Ground

프레임에서는 Ground 객체를 만들어 Character 객체들의 ground 변수에 대입합니다.
// ... 앞선 코드
var ground:Ground = new Ground

var container:Sprite = new Sprite
for(var i:int = 0 ; i < 10 ; i++){
var crt:Character = new Character
crt.x = Math.random() * 500
crt.y = 210 + Math.random() * 90
crt.ground = ground
container.addChild(crt)
}
// ... 따라오는 코드

프레임 스크립트의 enterFrame 수신자에는 다음 줄을 추가합니다.
addEventListener("enterFrame", loop)
function loop(e:Event):void {
if(key.isDown(Keyboard.LEFT)) A.x -= 5
else if(key.isDown(Keyboard.RIGHT)) A.x += 5
if(key.isDown(Keyboard.UP)) A.y -= 5
else if(key.isDown(Keyboard.DOWN)) A.y += 5
if(A.onGround && key.isDown(Keyboard.SPACE)) A.jump()
// ... 따라오는 코드
}

(getter)onGround는 캐릭터가 지금 땅을 밟고 있는 지를 알려주고, jump()는 캐릭터가 뛰어오르도록 합니다. 이제 Character 클래스에 두 메서드를 구현합니다.



캐릭터의 z 좌표가 캐릭터가 있는 위치의 땅의 높이와 같으면 땅을 밟은 것이니 onGround 메서드는 간단하게 작성합니다. 수치 오류로 인한 문제를 예방하기 위해 실제 구현에서는 = 대신 <=를 썼습니다.

jump()를 호출하면 초기 속도는 양의 z축 방향으로 주고 enterFrame 수신자를 추가하여 속도에 중력을 더해 캐릭터가 점점 내려오다 땅에 닿으면 멈추도록 하였습니다.




기왕 땅을 이렇게 표현한 김에 볼록한 땅은 만들어보고 넘어갑시다. 땅의 기울기가 x = 200, x = 400에서 바뀌고 가운데 평평한 부분의 높이는 50 픽셀입니다.

x = 0 ~ 200에서 높이 : 50 * x/200 = x / 4
x = 200 ~ 400에서 높이 : 50
x = 400 ~ 500에서 높이 : 50 - 50 * (x-400)/100 = 50 - (x-400)/2 이므로



Ground 클래스의 heightAt() 메서드는 이렇게 바꿉니다. 이제 Character 클래스에서 땅의 높이에 따라 수시로 캐릭터의 z 좌표를  변경해야 하는데 지금 구조로는 문제가 있습니다. 점프할 때만 z축 관련 처리를 하기 때문에 방향키로 캐릭터를 조종해서 x, y 좌표가 바뀌어도 점프 중이 아닌 이상 땅의 높이를 반영하지 않습니다.



jump()에서는 z 속도만 설정하고 update() 메서드를 따로 만들어 z축 관련 처리를 여기에 둡니다. 프레임 스크립트에서는 프레임마다 캐릭터들의 update() 메서드를 호출합니다.

// ... 앞선 코드
addEventListener("enterFrame", loop)
function loop(e:Event):void {
if(key.isDown(Keyboard.LEFT)) A.x -= 5
else if(key.isDown(Keyboard.RIGHT)) A.x += 5
if(key.isDown(Keyboard.UP)) A.y -= 5
else if(key.isDown(Keyboard.DOWN)) A.y += 5
if(A.onGround && key.isDown(Keyboard.SPACE)) A.jump()
if(A.x < 0) A.x = 0
else if(A.x > 500) A.x = 500
if(A.y < 210) A.y = 210
else if(A.y > 300) A.y = 300
var len:int = container.numChildren
var ary:Array = []
for(var i:int = 0 ; i < len ; i++){
ary.push(container.getChildAt(i))
Character(container.getChildAt(i)).update()
}
ary.sortOn("y", Array.NUMERIC)
for(i = 0 ; i < len ; i++){
container.setChildIndex(ary[i], i)
}
}


캐릭터가 내리막길을 걸을 때 덜컹거리는 것은 이전 위치에서보다 땅이 낮아졌기 때문에 공중에서 낙하하는 것으로 처리되기 때문입니다. 이것을 예방하려면 현재 높이가 이전 높이보다 낮은데 그 차이가 임의의 양 D 이하면 땅에 바로 붙이고 D보다 높으면 자유 낙하하게 놔둬야 합니다. 각자 해보시고 글이 자꾸 길어지네요 충돌 검사는 다음 편으로 미루겠습니다.

-------------------------------------------------------------------------------------------------- 

이번 글에서 충돌 검사를 다루면 던파 좌표계에 관한 기초적인 사항은 끝납니다.

던전앤파이터에서 남격투가 업데이트 이후, 캐릭터의 공격 판정 영역이 반투명하게 드러나는 버그가 있었습니다. 남격투가로 일발화약성을 쓰면 밑의 그림과 같이 빨간 사각형이 보이는 식이였지요.



어벤저 업데이트 때도 보호의 가시를 쓰면 비슷한 사각형이 보이곤 했습니다. 위 사각형은 xz 평면상의 충돌 판정 영역입니다. 공격 영역이 드러나는 이런 버그가 있을 때도 y축 판정 영역은 볼 수 없었는데 아마 게임 내에서 y축 판정 영역은 단순한 수로 저장하지 않았나 싶습니다.

시점을 조금 기울이면 던파에서 쓰는 충돌 판정 영역이 단순한 직육면체임을 알 수 있습니다.



xz평면상 충돌 검사는 hitTestObject()로 판정할 수 있으니 여기에 y축 검사만 더하면 되겠다라는 생각을 해볼 수 있습니다.



캐릭터 무비 클립에 충돌 검사를 위한 두 직사각형을 추가합니다. xz 판정에 쓸 무비 클립은 hitXZ, y 판정에 쓸 것은 hitY라는 이름을 붙였습니다. hitY의 가로 길이는 충돌 검사와 상관이 없습니다. hitY의 기준점은 왼쪽 위로 지정해야 합니다. 저는 그냥 보이게 해놨는데 실제로 쓸 때는 visible = false로 숨기면 되겠죠

Character 클래스에 hitTest 메서드를 추가합니다.



xz 영역을 나타낸다고는 하지만 y 좌표의 영향을 받기 때문에 일단 둘의 y 좌표를 저장해두고 똑같게 만듭니다. 그 다음 xz 충돌 검사를 하고 y 좌표를 되돌립니다.

y축 충돌 검사는 일직선상의 두 폐구간이 겹치느냐를 판별하는 문제와 같이 때문에 아는 공식으로 처리했습니다. 이 역시 x, z 좌표를 저장해놓고 똑같이 만든 다음 hitY끼리 hitTestObject()를 써서 검사한 뒤 x, z 좌표를 되돌리는 식으로 할 수는 있겠습니다.



최소한으로만 만들어서 충돌 검사로 공격을 한다던가 할 건덕지가 없기 때문에 충돌하면 반투명하게 만들어봅니다. container.getChildIndex(A) != i를 조건으로 넣은 이유는 자기 자신과 충돌 검사를 하면 항상 true가 반환되기 때문입니다. 이런 추가 검사는 조종하는 캐릭터만 목록에서 빼놓는 식으로 피할 수 있겠습니다.

던파 좌표계 구현은 이걸로 끝났습니다. 재밌는 게임 만드세요
1 Comments
댓글쓰기 폼