adsense



[JAVA/slick2d] 2D 슈팅게임 위치즈 스크램블 제작강좌 -step 4-

step 4. 플레이어 캐릭터-Player 클래스를 구현해보겠습니다.



step 3에서는 배경의 구현으로 GameObject를 사용하는 기초적인 방법을 다뤄보았습니다.

문제는 Slick2D는 각 BasicGameState가 한 번 생성되면 일단 계속 유지되는 방식이라, 다른 BasicGameState로 전환해도 기존 스테이트에서 사용하던 node가 해제되지 않습니다.

그때문에 BasicScene의 leave 함수에서, node를 모두 해제하는 처리를 해 줍니다.
거기까지는 구현하지 않았지만, 만일 BasicScene을 상속한 화면에서 별도의 이미지 객체를 생성했다면 이것도 해제해주는게 좋습니다. (이미지 객체를 init가 아닌 enter에서 생성한 경우)

지역변수도 유지되므로 초기화에 주의해야 합니다.
가급적 Scene 내부에서만 사용되는 모든 변수는 별도의 초기화 함수를 지정해주고 enter 안에서 호출해 초기화해주는 습관이 필요합니다.




이번에는 플레이어 캐릭터-Player 클래스를 구현해보겠습니다.

플레이어 캐릭터 구현 시나리오는 다음과 같습니다.

- 게임 시작 시(최초 객체 생성 시)에는 좌측에서 우측으로 이동해 나타난다.
- 이때는 깜박이며 무적.
- 화면 33% 정도 되는 위치까지 오면 깜박임이 끝나고 사용자 콘트롤이 가능하다.
- 1플레이어 캐릭터는 키보드로, 2플레이어 캐릭터는 조이패드로 조작한다.
- 스페이스 키나 조이패드의 버튼(1~4번 중 아무거나 가능)을 누르고 있으면 자동으로 총알을 발사한다.
- 적의 총알을 맞거나 적과 충돌하면 라이프가 감소, 잠시 깜박인다. 이때는 조종 불가.
- 라이프가 0이 되면 연기를 뿜으며 화면 아래로 추락하여 사라진다.
- 화면에 남아있는 플레이어가 없으면 게임 오버된다.


그럼, 순서대로 구현해 보겠습니다. 우선 GameObject를 상속한 Player 클래스를 prefab에.. step2에서 만든 걸 수정해서 씁니다.

구현된 Player 클래스를 보면서 순서대로 설명해 나가겠습니다.


클래스 변수



img는 캐릭터 이미지로 총 9개. 갯수는 꼭 이래야한다고 정해진 것은 물론 아닙니다.
일단 편의상 0, 1은 아무런 조작도 하지 않거나 상하로만 이동할 때.
2, 3은 전진할 때
4, 5는 후퇴할 때
6, 7은 사격할 때
8은 데미지를 받았을 때에 각각 해당합니다.

speed는 이동 속도. 삼각함수를 이용해서 이동을 구현할 것입니다.
myState는 플레이어 캐릭터의 현재 상태. 앞에서 시나리오로 설정한대로 '최초 시작 시', '조작이 가능한 때', '적의 공격을 받았을 때', '라이프가 0이 되어 격추될 때'를 이 값으로 구별합니다.
immortal은 true로 설정되면 캐릭터가 적 총알이나 적과 충돌해도 라이프가 깎이지 않습니다.

cnt는 전반적인 제어를 위한 카운트 변수로, update에서 계속해서 1씩 증가합니다.

myScore와 myLife는 각각 점수와 목숨. 플레이어가 복수라면 이들 역시 별도로 관리되어야 하므로 이곳에 설정합니다.
keyBuff는 방향 버튼의 상태, shootBuff는 발사 버튼의 상태입니다.

anim은 애니메이션 값으로, 앞에서 img를 정의할 때 설명한 그 값을 의미합니다. 통상의 조작중일 때, render에서는 이 값을 참고로 그림을 보여주게 됩니다.



다음은 생성자를 보겠습니다.



생성자에서는 플레이어 캐릭터에 사용할 이미지를 로드한 후 클래스 변수를 초기화 합니다.

y좌표는 객체를 만드는 쪽에서 지정한 값을 받아오지만, x좌표는 화면 바깥에서 나타나야 하므로 -40으로 초기값을 지정합니다.

그리고 여기서 중요한게 tag인데..

같은 클래스로 생성한 객체라면 1플레이어와 2플레이어를 어떻게 구분할 것인가?
이 구분이 필요한 이유는 사용자가 캐릭터를 조작하기 때문입니다. 이 객체가 키보드 입력을 받아올 것인가, 게임패드 입력을 받아올 것인가 결정해야 합니다.

우리는 이 구분을 tag 정보를 사용하기로 했습니다.
tag가 0이면 1플레이어, 그 외라면 2플레이어입니다. (임의의 위치에 tag 0을 갖는 플레이어 객체를 복수로 생성한다면? 키보드 조작을 받아 모두 동시에 움직이게 됩니다.)
따로 setTag로 붙이면 안되고 생성자에 포함시켜야합니다. 초기 이미지 읽어오는데에도 영향이 있기 때문입니다.

또, tag가 0이면 플레이어 캐릭터 그림을 rsc/player/ 라는 폴더에서, 0 이외라면 rsc/player2/라는 폴더에서 읽어오도록 하였습니다.
player는 기존에 익히 보아오신 요시카이며, player2는 이번에 새로 추가한 리네짱입니다.


아직 구현할 부분은 아니지만, 적이나 적 총알은 플레이어 캐릭터를 "PLAYER" 라는 name으로 식별하게 될 것입니다. (앞에서 미리 만든 getChildrenByName을 이용하게 됩니다) 그 후에 추가적으로 플레이어 1인가 2인가 구분하려면 그때는 tag를 참조하게 되겠고요.

이번에는 캐릭터가 둘 뿐이고 컨트롤러도 둘 중 하나이기 때문에 이렇게 처리했는데,
좀 더 범용성 있게 만들자면 캐릭터 이미지와 컨트롤러, 각 캐릭터의 유니크한 속성(속도, 무기 등)을 각각 따로 따로 지정하게 해서 조합하는 방법을 쓰게 됩니다.


update에서는 myState에 따라 switch 분기합니다.



0일 때는 단순히 오른쪽으로 이동하다 특정 위치가 되면 정지하고 콘트롤 가능한 상태로 바꿉니다.

1일 때는 GameScene으로부터 키보드 또는 게임패드 입력을 받아 조작 상태 버퍼값에 넣어주고, 플레이어 움직임 처리를 위한 함수를 호출합니다.
실제 이동은 MyMove() 라는 함수를 만들어 처리할 것입니다.
1인 상태에서 데미지를 입으면 2 또는 3으로 전환되는데, 이건 조금 아래에서.

2일 경우는 잠시 경직을 주기 때문에 콘트롤을 받지 않도록 하고, 이게 일정 카운트가 되면 다시 1로 되돌립니다.

3일 경우는 캐릭터를 화면 아래로 떨구고, 화면 밖으로 사라지면 GameScene에 플레이어 캐릭터를 삭제하도록 요청합니다.

(※이후 GameScene에서는 화면상에 "PLAYER" name을 갖는 오브젝트가 전혀 없으면 게임 오버처리를 하도록 할 것입니다.)


render로 가 보겠습니다.


스테이트가 0일 때는 0, 1번 그림을 반복해서 보여주고, cnt값을 사용해서 그려주지 않고 건너뛰는 부분을 추가해 깜박이도록 했습니다.

cnt%4, 즉 cnt를 4로 나눈 나머지를 구하면 이 값은 0, 1, 2, 3이 됩니다.
이것이 <2 라는 것은 0, 1일 때.
이때는 return을 만나서 render를 여기서 끝냅니다.
그리고, 2, 3일 때는 return을 만나지 않고 break를 만나 switch문을 벗어나 drawImage까지 수행하게 됩니다.

switch문의 흐름을 어려워하는 분을 종종 보게 되는데, if문보다는 낫지만 switch문도 잘 설계하지 않으면 (break를 빠트린다던가) 흐름이 꼬이기 좋긴 합니다..

스테이트가 1일 때는 anim값에 기초하되, cnt 값을 이용해 anim 그림과 anim+1 그림을 번갈아 보여줍니다.

스테이트가 2일 때는 8번 그림(데미지 쇼크)에 강제로 맞추고, cnt로 깜박이도록 합니다.

스테이트가 3일 때는 8번 그림에 맞추기만 합니다.

그리고, 지금까지 posX, posY는 이미지의 좌상단을 지정했지만, 앞으로는 중심점이 되도록 할 것입니다.
이미지의 회전이나 확대 축소 시에는 이미지의 중심을 기점으로 해야 다루기 편하기 때문입니다. 또, 이미지 간의 거리나 방향을 얻어낼 때도 이미지의 중심점을 대상으로 하는 것이 좋습니다.

그러기 위해서 Player의 render에서는 posX, posY가 이미지의 중심점이 되도록 하는 처리-폭과 높이의 반 만큼 왼쪽/위로 밀어준 자리에서 그리기 시작-를 하고 있습니다.

앞으로 만들게 될 GameObject의 파생 클래스들도 이렇게 중심점에 맞추는 처리를 할 것입니다.
(※참고로, slick2d에서는 중심점에 그려주는 api가 이미 준비되어 있습니다. 이 텍스트를 정리하는 시점에서는 미처 발견을 못해서..)


그럼 이제 핵심(?)적이 될 MyMove 함수인데..


우선 GameScene을 손봐야겠습니다.
키보드나 마우스, 게임패드 등의 입력은 BasicGameState에서 읽어올 수 있는데, BasicGameState->BasicScene->GameScene 순서로 상속하고 있으므로 GameScene에서 일단 키보드와 마우스의 입력을 체크하도록 합니다.

키보드와 게임패드의 입력을 담기 위한 버퍼 변수를 준비하고, 각 입력값에 대응할 상수를 마련합니다.



상수가 1, 2, 4, 8, 16..이렇게 되는 것은 비트 관계 때문인데, 각 수를 2진수(비트)로 바꿔보면

00001
00010
00100
01000
10000
이렇게 됩니다.

버튼이 중복으로 눌리면 이것을 or 연산(| 연산자)합니다.
예를 들어 1인 UP과 4인 LEFT라면, 00101 이런 값이 됩니다. or 연산을 했음에도 UP과 LEFT의 정보가 모두 보존됩니다.

이때 UP 키에서 손을 떼었다면, UP값을 not-and 연산합니다.
00101에서
00001을 not-and 연산하므로
00100으로 LEFT만 눌린 상태가 됩니다.

그런데 만일 LEFT를 3으로 했다면?

3은 2진수로 00011이 됩니다.
1과 3을 or 연산 해 봐야 그대로 00011이 되기 때문에 이래서는 UP이 눌렸는지 안 눌렸는지 판단할 수가 없습니다.
또, 이때 버퍼에서 UP을 not-and연산 하여 제거하면 00010이 되어 2, DOWN값이 되어 버립니다. 눌려있는 것은 LEFT일텐데.

이런 연유로 콘트롤러의 대응 상수는 이런 구조를 취합니다.
(※이번에는 원래 발사 버튼은 별도로 취급하므로 SHOOT에 한해서 굳이 16일 필요는 없습니다.)

public void controllerButtonPressed...로 시작해서 public void keyReleased...로 끝나는 일련의 입력 함수 오버라이드는 위에서 설명한 과정을 통해, keyBuff, padBuff, keyShootBuff, padShootBuff에 각각 현재 키보드와 게임패드의 상태를 저장하게 됩니다.


그럼 다시 Player 클래스의 MyMove로 돌아가 보겠습니다.

MyMove를 부르기 직전, update에서는



tag값에 따라서 1플레이어냐 2플레이어냐 나뉜다고 했으므로, tag값에 따라서 GameScene으로부터 keyBuff를 받아올지, padBuff를 받아올지 정해줍니다.

그 후에 MyMove가 호출되고, MyMove 안에서는 8방향 중 어느 방향으로 입력되었는가에 따라 이동 처리를 해 줍니다.

※굳이 GameScene의 버퍼값을 다시 지역변수에 담는 것은, 입력 이벤트가 언제 발생할지 알 수 없기 때문입니다.
지금은 MyMove의 내용이 꽤 간단합니다만, 프로그램을 만들기에 따라서는 'UP이 입력되어서 UP 처리를 시작했는데, 처리 중에 이벤트에 의해 DOWN으로 바뀌어 버렸다' 이런 경우도 생깁니다.
그러므로 이벤트의 발생으로 수시로 변동할 수 있는 값을 바로 쓰지 말고, 별도의 버퍼에 넣어서 처리가 끝날 때 까지는 해당 값을 유지해줄 필요가 있습니다.

8방향에 따라서 x, y값에 speed를 그냥 가감만 하는 방법도 있겠습니다만, 미래를 위해서 새로운 함수를 몇 개 코딩하겠습니다.

BasicScene을 열어서
static public Point getNextPos(float sx, float sy, float degree, float speed) ※Point는 org.newdawn.slick.geom.Point의 Point입니다.
static public float getRange(float sx, float sy, float dx, float dy)
static public int getDegree(float sx, float sy, float dx, float dy)
를 만들어 넣었습니다.


getNextPos는 '현재 위치를 기준으로 degree방향 각도로 speed 속도로 움직일 경우' 다음에는 어느 위치가 되겠는가를 구해줍니다.
방향을 각도로 지정하므로 360도로 이동할 수 있고, 직각이 아닌 사선으로 움직일 때도 일정한 속도로 이동할 수 있습니다.
degree는 화면 기준으로 위 방향이 0도(=360도)이며, 시계방향으로 회전합니다.


getRange는 두 점 사이의 거리를 구합니다. 예를 들어 아이템이 일정 거리 이내로 들어오면 자동으로 빨아들인다던가..


getDegree는 두 점 사이의 각도를 구합니다. getNextPos에서 쓰는 degree와 같습니다. 적이 플레이어를 노려서 총알을 쏘려고 할 때 총알을 어느 정도 각도로 쏘면 될까 뭐 그런 걸 구할 수 있습니다.

이 함수들 자체는 그동안 요시카 스크램블에서도, 위치즈 플라이트나 아스테로이드 벨트에서도 수시로 써먹은 함수입니다.


그럼 다시 MyMove로 돌아가서,
getNextPos로 8방향에 대응되어 이동할 좌표를 구해주면 됩니다.
정직한 8방향이니 degree 값은 0, 45, 90, 135, 180, 225, 270, 315 등으로 들어갑니다.

MyMove 아래에 설명되지 않은 함수들이 보이는데, 그건 아직 신경쓰지 않아도 됩니다.


그럼 GameScene의 초기화 파트(enter)에 캐릭터를 생성해서 붙여주는 코드를 작성하고 실행해 봅니다.






잘 됩니다. 아래로 내려가 보면 구름 뒤에 가려지는 것도 확인할 수 있습니다.

게임패드가 있다면 2플레이어를 추가하는 코드의 주석을 풀고, 게임패드를 붙여서 테스트해봐도 좋겠습니다.



핑백

덧글

  • 아리가또 2016/10/03 02:53 # 삭제 답글

    감사합니다 이 오픈소스를 보면서 slick2d쓰는법을 익히고 있었는데
    왜 left와 up 이 동시에 눌렸을때 &을 쓰지않고 |을 쓰는지 몰랐었는데
    상수를 그렇게 지정하는 이유가 있었군요 정말 언어만드는 사람의 대단함에 감탄했고
    이렇게 멋지게 설명해주신 글쓴이분에게 너무너무 존경합니다. 돈 주고도 배우고싶네요!
  • 펭귄대왕 2016/10/03 21:13 #

    잘 봐주셔서 감사합니다.
    전공자가 아니고 현장에서 부딪히면서 배운 사람이다보니 경력만 길었지 실력이 태부족합니다. ^^; (지금도 네트워크 잘못 짜서 고생을..)
    졸문이 도움되었으면 좋겠고, 혹여라도 궁금하신 점이 있으시면 언제든지 찾아주세요.
  • 푸우의꿀딴지 2016/11/25 09:43 # 삭제 답글

    키입력 스위치문에서 궁금한점이 있습니다.
    |= 와 &=~ 는 처음보는데, 어떻게 이해를 하면될까요?
    하나하나 지워가면서 |를 지우면 동시키 입력이 안되고,
    &를 지웠을경우, 처음 이동이후 키입력이 안되고(이동하지 않고)
    ~를 지웠을경우, 키를 때도 이동이 멈추지 않고
    이런 증상들은 확인하였는데, 다른곳에서 어떻게 써먹을수 있을지 개념을 알고 싶네요.
    해당 강좌 정주행후에 Slick를 활용해서 학교 프로젝트용 간단한 rpg게임을 만들어볼까해서요 ^^
    (배우는 학생입니다...)
  • 펭귄대왕 2016/11/25 15:57 #

    일단 &는 and 비트연산, |는 or 비트연산입니다.
    그리고, &=, |=는 +=, -= 와 비슷한 서식입니다.
    마지막으로 ~는 not 비트연산입니다.

    그러니까
    keyBuff &=~ CONTROL_UP;
    라고 하면
    keyBuff = keyBuff & (~CONTROL_UP);
    의 의미가 됩니다.

    사실 &=와 ~가 각각 따로인 것입니다만...
    &=~를 한꺼번에 not-and 비트연산으로 이해해도 됩니다.

    이런 구조는 대각선 동시 입력을 필요로 하는 경우를 위한 것으로,
    대각선 동시 입력이 필요하지 않다면 키 이벤트에서 키 값을 버퍼에 넣기만 해도 됩니다.
댓글 입력 영역


Books

Geek라이프

메가 드라이브 퍼펙트 카탈로그
마에다 히로유키 저/조기현 역

미소녀 일러스트 테크닉
B-은하, pen스케, 카와이 저/정유진 역

핵심강좌! Cocos2d-x
이재환 저

피규어의 교과서 레진 키트 & 도색 입문 편
후지타 시게토시 저/김정규 역
예스24 | 애드온2
일본서적 전문사이트 NEPIC