adsense



[JAVA] 2D 슈팅게임 개발강좌- 위치즈 플라이트 3-2, 3-3부

강좌용 프로젝트 파일 다운로드 : GamePlayer.zip

오늘 내용으로 여기까지 구현합니다


※잠시만.. : 질문은 항상 환영합니다만, 굳이 비밀글로 하지 않아도 되는 경우라면 비밀글보다는 공개로 해 주시면 어떨까, 살짝 생각합니다. 그 답변이 다른 분에게도 참고가 될 지도 모르니까요.

2. 자신의 캐릭터를 등장시켜보자.


플레이어 캐릭터 관련된 내용을 우선 설정합니다.

- 드○○플○○○ 타입 게임이므로 상하 움직임은 없고, 좌우 움직임만
- 원래대로라면 터치(마우스?)에 반응하여 움직여줘야겠지만, 자바판은 키보드로
- 좌우로 이동하면 해당 방향으로 기울어지고 손을 떼면 중립 자세로 되돌아간다
- 총알 발사는 자동

오브젝트는 기본적으로 클래스를 만들어 처리하지만, 플레이어 캐릭터는 하나 뿐이므로 클래스화 하지 않고 바로 구현합니다.
단, 나중에 캐릭터를 늘려준다던가 하면 이 역시 클래스화 하는게 좋습니다.

기본 정보는 항상 동일합니다. 표시 좌표와 이미지.


역시 GameScene의 클래스변수로 설정. 이미지 변수 위치는 initimage 함수 앞쪽에 두는게 관리하기 편합니다.


비트맵 이미지 확보


비트맵 이미지 해제

dblpaint에 그려주는 부분을 넣습니다. 리소스가 여러 장의 프레임을 하나로 모은거라 drawImageRect를 사용합니다. 어떤 프레임을 그려줘야 하는가 지시하기 위해 myFrame이란 변수를 설정했습니다.


리소스의 lyne.png를 보면 왼쪽으로부터 순서대로 왼->오른으로 기울어짐을 5프레임으로 표현하고 있습니다.
중립 상태가 세 번째 프레임이므로 myFrame의 초기 값은 2가 됩니다. (프레임 값의 범위는 0~4 사이)

키보드 이벤트 함수에서는 바로 좌표를 변동하지 않고, 키 버퍼 변수(keybuff)만 변경시켜줍니다.
눌렀을 때 버퍼에 값을 넣고, 떼었을 때 버퍼에서 값을 제거합니다.
주로 멀티키를 처리하기 위한 방법이지만, 연속키(키 리피트)를 처리하기 위한 방법도 됩니다.



그리고, update 함수에서 이 키 버퍼 값을 참조해 캐릭터 좌표를 변경합니다.


다음은 좌우 기울어짐 구현입니다.
중립에 해당하는 그림은 3번째 프레임입니다.

왼쪽 방향키를 누르고 있으면 3->2->1 순서로 기울어지는 그림으로 바뀌고
오른쪽 방향키를 누르고 있으면 3->4->5 순서로 기울어지는 그림으로 바뀌는데, 프레임마다 바뀌는게 아니라 약간의 시차를 두고 바뀝니다.

이 시차를 계산하기 위한 변수를 둡니다. 이름은 keyTime 으로 정했습니다.


일단 눌리거나 손을 떼면 keyTime을 0으로 리셋합니다.
그리고 루프에서 1씩 증가시켜서, 적당한 수(현재는 7)이 될 때마다 프레임을 변경해줍니다. 오른쪽으로 가고 있는 상태와 왼쪽으로 가고 있는 상태는 키 처리 부분에서,


중립은 update에서 처리해 줍니다.


이렇게 하면 해당 방향으로 이동할때 기울어지거나, 손을 떼었을 때 중립으로 돌아오는 모습을 볼 수 있습니다.



3. 캐릭터가 총을 쏜다.

총알 처리를 위해 Bullet 클래스를 만들어 줍니다.
src 폴더에 subclass 패키지를 만들고, 거기 Bullet이란 이름의 클래스를 추가합니다.

Bullet 클래스에 설정된 정보를 볼까요.


일단, 좌표. 그림은 오브젝트의 공통 기본이고.

충돌 체크를 위해 필요한 상자(Rect) 정보입니다 java에서는 awt에 Rectangle 클래스가 있습니다.
이걸 어떻게 사용할 것인가- 하는 건 3-5에서 다룰 예정입니다.

총알의 파워-적에게 명중했을 때 적의 HP를 깎는 양입니다.

만일 나중에 상점을 구현하고 파워업을 구매하게 된다면? 이미지와 파워를 변경해주면 되겠습니다만, 그건 나~중에
이번엔 어디까지나 카피캣이니만큼 총알은 직선으로만 날아갑니다.


총알 발사는 어디서 처리해줘야 할까요.
이전엔 키 처리 해주는 부분에서 발사버튼(스페이스 키 라던지)이 눌려 있으면 발사하도록 했는데, 이번엔 자동 발사입니다.
'일정 간격마다' 발사해야 하니, 역시 update 루프에서 처리하면 됩니다.


cnt % 10 == 0 에서, 10을 작게 해주면 더 빠르게, 크게 해주면 느리게 발사합니다.

프로그래밍 개념적으론 발사 = 객체 생성입니다.
객체를 생성해 목록에 얹어줘야 하니 Vector가 필요합니다.
벡터의 초기화는 SceneStart 함수에서 해주고 있습니다. GameScene 클래스의 변수 선언부에서 초기화하지 않도록 주의합니다.


총알의 생성자 인자로 비트맵 객체, 총알의 타격 판정 범위, 총알을 발생시키는 위치를 각각 전달합니다.



총알과 별개로,
플레이어 캐릭터의 그림을 기울이는 처리도 있었고, 이제 총알도 발사해줘야 하니 이 부분은 묶어서 따로 함수로 빼는게 깔끔하겠네요.
myProcess 가 해당 함수입니다.

그림을 그려줄 부분, 움직임을 처리해 줄 부분에서는 벡터를 읽어 해당 객체 클래스의 서브함수를 부르도록 해 주면 코드가 간단해집니다.


그림은 그려주기만 하면 되고,


움직임은 직선으로 위로 올라가므로 적절하게 y좌표를 감소시켜주면 됩니다.


메인 루프에서 총알이 화면 밖으로 나갔다면 이 총알을 이제 소멸시켜줘야 합니다. 소멸이란 것은, 벡터에서 객체를 제거하여 더 이상 처리하지 않는 상태입니다.
여기서는 두 가지 방식을 소개하고 오늘의 턴을 마치겠습니다.

방법 1.
총알이 화면 위로 나가면 특정한 리턴값을 돌려주고, 메인 루프에서 리턴값을 참조하여 해당 객체를 Vector에서 제거하는 방식입니다.
GameScene의 processBullet

Bullet 클래스의 처리 코드

방법 2.
총알이 화면 위로 나가면, 메인 루프의 특정 함수에게 자신을 삭제해달라고 요청하는 방식입니다.
GameScene의 processBullet

Bullet 클래스의 처리 코드

GameScene에서, 삭제 대상이 된 총알을 화면에서 제거하기 위한 추가 함수


1은 리턴값에 따라 다양한 처리가 가능하다는 장점이 있고 코드가 직관적이라 처음엔 좋습니다만, 리턴값 종류가 늘어나게 되면 코딩시 실수할 가능성이 높아지고 디버그가 불편해집니다.

2는 코드가 컴팩트해지고 나중에 추가 기능이 필요하다면 함수를 추가/삭제하여 오류가 적은 관리를 할 수 있습니다.

첨부한 샘플 프로젝트에서는, 방법 2에 관련된 내용은 주석처리해뒀으므로 직접 주석을 해제해 방법 1과 2를 번갈아 실행해 보면 코드를 이해하는데 도움이 될..지도 모르겠습니다 ^_^;

아, 참고로.
Vector는 데이타 싱크를 자체적으로 맞추지만 다른 형태의 Collector들 - ArrayList나 Map 등은 그렇지 못하므로 이렇게 콜렉터 관련된 루프를 도는 도중에 임의로 객체를 삭제하면 뻑납니다.

자바에서는 Vector를 쓰면 되니까 별 문제 없지만 나중에 C/C++ 쪽으로 가게 되면 이 점을 유의해야 합니다.



그럼 다음에는 적 캐릭터를 등장시키고 총알과의 충돌을 처리하는 부분까지 해 보겠습니다.



덧글

  • 질문자 2014/02/01 01:24 # 삭제 답글

    public final static int UP_PRESSED = 0x001;
    public final static int DOWN_PRESSED = 0x002;
    public final static int LEFT_PRESSED = 0x004;
    public final static int RIGHT_PRESSED = 0x008;
    public final static int FIRE_PRESSED = 0x010;
    이값들은 왜 이렇게 준 것인지 궁금합니다.
    1과 0x001은 다른건가요?
    keybuff |= LEFT_PRESSED;
    이것도 왜 이렇게 한건지 궁금합니다.
  • 펭귄대왕 2014/02/01 07:20 #

    변수 하나로 여러 가지 상태값을 중복 저장하기 위해 비트연산을 사용하는 것입니다.

    비트연산을 위해서는 2진수를 쓰지만 2진수를 직접 써 넣는 방법이 없으므로 비교적 대응하기 쉬운 16진수 표현으로 바꾼 것으로,

    UP = 0x001 은 2진수로 0001
    DOWN = 0x002 는 0010
    LEFT = 0x004 는 0100
    RIGHT = 0x008 은 1000
    FIRE = 0x0010은 10000

    keyBuff!=UP
    이렇게 연산하면 keyBuff의 내용이 0001이 되고,
    다시 여기에
    keyBuff!=LEFT
    이렇게 하면 0101이 됩니다.

    만일 이 이외의 값이라면 비트연산 시 값이 겹쳐져 버려서 식별이 불가능해집니다.

    이 keyBuff 값을 이후에

    if(keyBuff & UP == UP) pressedUp();
    if(keyBuff & DOWN == DOWN) pressedDown();

    이런 식으로 쓰면 됩니다.
    샘플의 keyProcess()는 좀 비효율적으로 처리했지만요.
  • 감사합니다 2014/02/01 13:29 # 삭제 답글

    감사합니다. 새해복 많이 받으세요
  • myframe 2014/02/01 18:05 # 삭제 답글

    myframe 값을 0 으로 해야지 그림이 제대로 나오구요.
    myframe을 0보다 큰값으로 하면 그림이 사라지는데 왜 이럴까요??
  • 펭귄대왕 2014/02/02 07:12 #

    샘플 소스를 바로 사용하신게 아니라 본문을 따라 하나씩 코딩하면서 테스트하셨다면 playerWidth 값을 지정해 줬는지 확인해 보세요.
    씬을 초기화할 때.
    playerWidth = player.getWidth(this) / 5;
    이런 식으로 애니메이션 프레임의 폭을 미리 지정해줘야 합니다.
    (비트맵 이미지 player에 다섯 장의 프레임이 가로로 나열되어 있기 때문에 이미지 폭의 /5를 해 줍니다)

    혹은
    manager.drawImageRect(gContext, player, myX, myY,(생략)
    부분에 커서를 가져가 ctrl+shift+b 를 눌러 브레이크 포인트를 지정하고
    F11 키를 눌러서 디버그 스타트하면 해당 위치에서 디버깅 브레이크가 걸리는데, 이때 마우스 커서를 각 변수 위에 가져가 보면 변수들의 현재 값이 나타납니다.

    각 변수들의 값을 보면서 뭔가 잘못된 수치가 들어가지 않았는가 확인해보고 원인을 찾으면 됩니다.
  • 질문 2014/02/02 11:43 # 삭제 답글

    playerWidth = player.getWidth(this);
    이런식으로 주었구요.
    manager.drawImageRect(gContext, player, playerX, playerY, myFrame
    * playerWidth, 0, player.getWidth(null),
    player.getHeight(null), manager.ANC_LEFT);
    여기서 myFrame * playerWidth 가 기울어짐을 조정하는 것 같은데 어떤 원리인지 설명해주실수있을까요??
  • 펭귄대왕 2014/02/02 13:33 #

    샘플프로젝트에서 rsc/game 폴더의 lyne.png 파일을 보시면, 한 장의 비트맵에 다섯 장(프레임)의 그림이 들어가 있습니다.

    프로그램으로 그림을 기울여준 것이 아니라, 기울어지는 그림 각각을 미리 그려서 따로 넣고,
    지금 보여줘야 할 기울기에 따라 어떤 그림을 보여줄 지 myFrame 값으로 지정하고 있는 것입니다. 정확히 말하면 애니메이션 프레임 번호인 셈입니다.

    player 비트맵 이미지에 한 장의 그림만 있는 이미지를 사용하셨다면 myFrame 값은 의미가 없으므로 그냥 0으로만 쓰면 됩니다.
  • 질문 2014/02/03 14:28 # 삭제 답글

    감사합니다.
    그럼 같은 효과를 주려면 포토샵으로 그림을 5개 복사해서 조금씩 다르게 넣으면 될까요?
    떨어져 있는 그림들을 프레임으로 인식하기 위해서 간격이 같아야 한다던지, 저 5개의 그림간의 좌표도 파악해둬야 한다던지, 그런것들이있나요?
  • 펭귄대왕 2014/02/03 15:37 #

    예제 소스를 그대로 사용하면서 그림만 바꾸려고 한다면

    왼쪽으로 많이 기울어진 그림-왼쪽으로 조금 기울어진 그림-중립 그림-오른쪽으로 조금 기울어진 그림-오른쪽으로 많이 기울어진 그림

    이 순서로 그림을 가로로 나열해서 한 장의 png로 만들면 됩니다.
    이때 각 그림은 여백을 포함해서 같은 너비를 가져야 합니다.

    예제 소스에서는 별도의 그림 간 좌표관계-앵커(anchor)라고 보통 말합니다-를 따로 지정하지 않고, 그림에 여백을 넉넉하게 줘서 동일한 좌표에 다른 프레임을 그려주기만 하면 자연스럽게 현재 위치에서 기울어지는 것 처럼 보이게 했습니다.

    만일 프레임을 여백 없이 각각 따로 만든다면 앵커 정보를 지정해서 표시할 때 반영해 줘야 합니다.
  • 질문 2014/02/09 15:56 # 삭제 답글

    답변 감사합니다.
    마우스로 그림을 회전하려고 하는데요, 구글링을 조금 해본 결과
    grphics2d를 이용한 회전밖에 나오지 않는데,
    graphics로는 회전하려면 어떤걸 이용해야 하나요??
  • 펭귄대왕 2014/02/09 17:33 #

    awt에는 이미지를 회전시키는 api가 없습니다..
  • 질문 2014/02/09 18:04 # 삭제 답글

    그런가요?? ㅠㅠ ㅣ -> ㅡ
    이런식으로 회전하는건데 없나요?? ㅠㅠ
  • 질문 2014/02/09 18:25 # 삭제 답글

    캔버스에 abstract public void dblpaint2D(Graphics2D gContext); 이렇게 오버라이드했구요
    매니저에 public void drawImageCircle(Graphics2D gc, Image img, int x, int y, int r,
    int dir) {
    gc.drawImage(img, x-r, y-r, 2*r, 2*r, this);
    }
    게임화면에
    @Override
    public void dblpaint2D(Graphics2D gContext) {
    // TODO Auto-generated method stub
    manager.drawImageCircle(gContext, player, playerX, playerY, playerR, 0);
    }
    이렇게 했는데 그림이 그려지지 않네요 ㅠ

    http://stackoverflow.com/questions/8437764/rotate-graphics2d
    graphics 2d 로테이트에 관한 내용입니다. 회전은 제가 찾아볼게요. 그런데 그림이 안그려지네요 ㅠㅠ graphics에서 graphic2d로 바꾸니
  • 질문 2014/02/09 22:55 # 삭제 답글

    update에 dblpaint2D를 넣지 않아 player가 뜨지 않았는데, update에 dblpaint2D 추가해주고 nullpointerexception 처리 하는것을 dblpaint함수와 동일하게 해 놓았더니 배경 화면까지 같이 없어져 검은 화면만 뜨네요 ㅠㅠ
  • 질문 2014/02/09 23:08 # 삭제 답글

    괜찮으시면 소스를 한번 봐주실 수 있을까요?? ㅠㅠ
    여기저기 많이 찾아 봤는데 도무지 해결이 되질 않아서요.. ㅠㅠ
    메일주소 남겨주시면 메일로 소스 보내겠습니다ㅜ
  • 펭귄대왕 2014/02/10 00:14 #

    valpa네이버닷컴입니다
    지금 일본인 관계로 돌아가서나 확인해볼 수 있을 것 같네요.
  • 질문 2014/02/10 00:25 # 삭제 답글

    감사합니다. 메일 보냈습니다.
  • 질문 2014/02/10 01:36 # 삭제 답글

    public void update(Graphics g) {

    if (gContext == null)
    return;
    dblpaint(gContext);// 오프스크린 버퍼에 그리기

    g.drawImage(dblbuff, 0, 0, this);// 오프스크린 버퍼를 메인화면에 그린다.
    }

    public void update(Graphics2D g) {

    if (g2 == null)
    return;
    dblpaint2D(g2);// 오프스크린 버퍼에 그리기

    g2.drawImage(dblbuff, 0, 0, this);// 오프스크린 버퍼를 메인화면에 그린다.
    }

    이렇게 함수를 아래 하나 더 오버로드 했는데, 디버깅 해보니 update(graphics2D)는 실행이 되질 않는군요ㅠ
  • 질문 2014/02/10 01:41 # 삭제 답글

    public void paint(Graphics g) {
    if (gContext == null) {
    dblbuff = createImage(manager.SCREEN_WIDTH, manager.SCREEN_HEIGHT);
    gContext = dblbuff.getGraphics();
    return;
    }

    update(g);
    }

    public void paint(Graphics2D g) {
    if (g2 == null) {
    dblbuff = createImage(manager.SCREEN_WIDTH, manager.SCREEN_HEIGHT);
    g2 = (Graphics2D) dblbuff.getGraphics();
    return;
    }

    update(g);
    }

    paint부분도 이렇게 오버라이드 했는데,
    paint를 실행하는 것이 repaint()인가요???
  • 펭귄대왕 2014/02/10 01:45 #

    Graphics2D는 이용해본적이 없어서 좀 알아봐야 하겠습니다..
  • 펭귄대왕 2014/02/17 14:37 #

    답변이 많이 늦어서 죄송합니다. Graphics2D는 써 본 적이 없어서 저도 좀 공부해서 알아왔습니다.

    아래 소스(일부)처럼 구현하면 아마 되는 것 같습니다만..

    Image testImg;
    Image dblbuff;
    Graphics2D g2Context;
    public void paint(Graphics g){

    if(g2Context==null) {
    dblbuff=createImage( 640, 480);
    g2Context=(Graphics2D)dblbuff.getGraphics();
    return;
    }

    update(g);

    }

    public void update(Graphics g){

    if(g2Context==null) return;

    g2Context.rotate(Math.toRadians(-10));
    g2Context.drawImage(testImg, 100,20, this);

    g.drawImage(dblbuff,0,0,this);//오프스크린 버퍼를 메인화면에 그린다.
    }

    paint나 update는 Graphics만 인수로 받고 Graphics2D는 아닙니다. Graphics2D를 인수로 주면 프레임이 호출하는 것과는 아예 다른 별개의 함수가 됩니다.

    그때문에 Graphics2D의 api를 사용하려면 별도로 Graphics2D 객체를 얻어내서 사용해야 합니다.
  • CIH 2014/06/12 21:24 # 삭제 답글

    안녕하세요 과제하려다가 혼자 하기 어려워 결국 이곳을 찾았습니다.

    혹시 실례가 되지 않는다면 올려주신 자료, 사용해도 되겠습니까?
  • 펭귄대왕 2014/06/13 08:20 #

    넵 자유롭게 사용해 주세요.
  • 송파로봇 2014/10/11 17:13 # 삭제 답글

    이제 총알 자동발사 시키는 것까지 따라했는데 공부 많이 되네요.
    중간에 약간 소스를 변형하며 하다보니 총알이 나오질 않아 고생했는데 다행히 에러를 찾아서 해결됬습니다. 한 고비 한 고비 겪으며 문제를 해결하고 그 결과로 얻게 되는 성취감이 프로그램밍하는 재미인 것 같습니다.^^
  • 펭귄대왕 2014/10/11 21:57 #

    의도한 결과가 잘 나와주는 것도 재미있지만 문제가 생겼다가 문제의 원인을 정확히 파악해서 수정했을때의 쾌감이 각별하죠 ^_^
  • 123 2015/10/16 21:19 # 삭제 답글

    미사일파워를 올리려면 숫자를 어떻게 바꿔야하나요
    new Rectangle(3-powRange, 1, 15+powRange*2, 33), _x, _y, 1+manager.powlevel) 이부분에서

  • 펭귄대왕 2015/10/16 22:21 #

    rectangle은 충돌체크를 위한 사각형이기 때문에 거기에는 총알의 파워에 관련된 정보는 없습니다. 현재 파워에 연동해서 총알의 크기가 커지기 때문에 충돌 범위도 조금씩 키워주는 것 뿐이죠.

    총알의 위력을 바꾸려면 powlevel을 직접 건드리거나, new Bullet을 찾아서 생성자를 참고하면 되겠습니다.
댓글 입력 영역


Books

Geek라이프

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

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

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

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