자바 게임 프로그래밍 강좌 6
하이텔 자바 동호회 김명현 (MadOx@hitel.net)님의 글입니다.
이글은 김명현님의 동의없이 함부로 배포하실 수 없습니다.
안녕하세요? 친소맨입니다. 이번 강좌는 소스가 길어서 내용은 별루 안되는 데, 길이만 길군요.. 앞으로도 계속 이런 사태가 벌어질것 같아서 걱정입니 다. 대신 주석을 너무하다 싶을정도로 붙여놓은 소스이므로 참고 하시면 많 은 도움이 될듯 합니다. * 본 문서는 Java Development Kit 1.0.2 를 기준으로 작성되었습니다. * Java Development Kit 1.1.x 버젼을 사용할 경우 일부 소스가 실행이 되지 않을 수 있습니다. 3.1.3 마음대로 움직이는 탱크를 만들어 봅시다. 지금까지 배운 것을 모두 총괄 복습하는 의미에서 다소 덩치가 있는 프로 그램을 만들어 보도록 하겠습니다. 제목에서 알 수 있듯이 우리가 임의로 조정가능한 탱크 애플릿을 만들어 볼 것입니다. 우선 간단하게 계획부터 세 우기로 합시다. ● 탱크 애플릿의 목적 "화면상에 탱크 이미지를 출력하고, 우리가 누르는 방향키에 따라 상/하/ 좌/우 네방향으로 이동 하도록한다. 각각의 방향에 따라 서로 다른 이미 지가출력되도록 한다." ● 계략적인 프로그램의 흐름 ?start method ① 애플릿에서 사용할 이미지 데이터를 서버로부터 전송해 온다. ② 전송해 온 이미지를 CropImageFilter를 이용해 4방향 각각의 이미지 로 분리한다. ③ 실제 작업을 할 메모리 버퍼(더블 버퍼링용)를 생성한다. ④ 애플릿의 메인 쓰레드를 생성하고 활성화 시킨다. ⑤ 각종 변수들을 초기화 한다. (탱크의 초기 위치, 탱크의 방향 등) ?run method ⑥ 화면을 갱신하고 일정기간 쓰레드를 쉬게 한다. ⑦ 키보드가 PUSH 됐다면, 해당 키 값에 따라 탱크를 이동시킨다. ⑧ ⑥번으로 이동한다. ?paint method 탱크 이미지가 로드 완료 되었나? 예) 탱크의 방향에 맞춰 이미지를 탱크의 현재 위치에 출력한다. 아니오) "Loading Image..."를 출력해 이미지 로드중임을 표시한다. ?keyDown method PUSH 된 키 값이 상/하/좌/우 중 한 값이면 해당 키보드 플랙을 true로 셋팅한다. 탱크의 현재 방향을 설정한다. ?keyUp method POP된 키보드 값이 상/하/좌/우 중 한 값이면 해당 키보드 플랙을 fal- se로 셋팅한다. 탱크 애플릿의 수행 절차입니다. keyDown메소드와 keyUp메소드, paint 메 소드는 OS로 부터 통보 되는 이벤트에 의해 불려지는 핸들러 이므로 우리가 따로 불러주지 않더라도 자동으로 불려지게 됩니다. 탱크의 이동처리를 ke- yDown메소드에서 직접 처리하지 않는 이유는 keyDown메소드로는 연속적으로 눌려져 있는 키 값을 처리하지 못하기 때문입니다. (KeyDown 이벤트는 키가 눌려지는 시점에서 단 1회 발생합니다) 이 절차에 따라 만들어진 소스가 아 래 [소스2]입니다. ! 주의 : 아래의소스중 라인번호가 붙어 있지 않은 라인은 원래는 위의줄 과 한 라인이나, 편집을 위해 나눠 놓은 것입니다. 001: import java.awt.*; 002: import java.applet.*; 003: import java.awt.image.*; 004: import java.lang.Runnable; 005: 006: public class Tank extends Applet implements Runnable 007: { 008: // 애플릿의 주 쓰레드 009: private Thread trdMain; 010: 011: private MediaTracker tracker; 012: //서버로 부터 받을 이미지 013: private Image imgTankAll; 014: // 각 방향의 이미지 015: private Image imgUp; 016: private Image imgLeft; 017: private Image imgRight; 018: private Image imgDown; 019: 020: // 탱크 이미지를 그릴 메모리 버퍼 021: private Image imgOffScr; 022: private Graphics grpOffScr; 023: 024: // 탱크의 방향과 위치 025: private int tankDir; 026: private int tankXp, tankYp; 027: 028: // 탱크의 방향 설정및 체크에 사용할 상수 029: private final int UP = 1; 030: private final int LEFT = 2; 031: private final int RIGHT = 3; 032: private final int DOWN = 4; 033: 034: // UP/DOWN/LEFT/RIGHT 키의 상태를 저장할 Flag 035: private boolean Key_UP = false; 036: private boolean Key_DOWN = false; 037: private boolean Key_LEFT = false; 038: private boolean Key_RIGHT = false; 039: 040: public void start() 041: { 042: ImageProducer producer; 043: ImageFilter filter; 044: 045: // 4방향의 탱크 이미지를 서버로 부터 가져온다 046: tracker = new MediaTracker(this); 047: imgTankAll = getImage(getCodeBase(), "Tank.GIF"); 048: 049: tracker.addImage(imgTankAll, 0); 050: 051: producer = imgTankAll.getSource(); 052: 053: // 서버로부터가져온 이미지에서 4방향이미지를 잘라낸다 054: filter = new CropImageFilter(1, 1, 136, 136); 055: imgUp = createImage(new FilteredImageSource(producer, filter)); 056: tracker.addImage(imgUp, 1); 057: 058: filter = new CropImageFilter(138, 1, 136, 136); 059: imgRight = createImage(new FilteredImageSource( producer, filter)); 060: tracker.addImage(imgRight, 1); 061: 062: filter = new CropImageFilter(275, 1, 136, 136); 063: imgDown = createImage(new FilteredImageSource( producer, filter)); 064: tracker.addImage(imgDown, 1); 065: 066: filter = new CropImageFilter(412, 1, 136, 136); 067: imgLeft = createImage(new FilteredImageSource( producer, filter)); 068: tracker.addImage(imgLeft, 1); 069: 070: // 애플릿의 크기에 맞춰 더블버퍼링을 할 버퍼를 만든다 071: imgOffScr = createImage(size().width, size().height); 072: grpOffScr = imgOffScr.getGraphics(); 073: 074: // 탱크의 초기 방향을 위로 정하고, 탱크를 화면 중앙에 위치시키기 075: // 위해 좌표를 설정한다. (탱크 이미지의 가로, 세로 크기가 136 Pixel임) 076: tankDir = UP; 077: tankXp = (size().width - 136) / 2; 078: tankYp = (size().height - 136) / 2; 079: 080: // 메인 쓰레드를 생성하고 활성화 시킨다 081: if (trdMain == null) { 082: trdMain = new Thread(this); 083: trdMain.start(); 084: } 085: 086: // 애플릿에게 키보드 이벤트가 발생하도록 OS에게 Focus를 요청한다 087: requestFocus(); 088: } 089: 090: public void run() 091: { 092: // 그림이 완전히 로드될때 가지 기다린다 093: try { 094: tracker.waitForAll(); 095: } catch (InterruptedException ie) {} 096: 097: // 주 루프 098: while (true) { 099: repaint(); // 화면을 갱신한다 100: try { 101: Thread.sleep(50); // 0.05초간 쉰다 102: } catch (InterruptedException ie) {} 103: 104: // 방향에 따라 탱크를 이동시킨다 105: if (Key_UP) 106: tankYp -= 2; 107: if (Key_DOWN) 108: tankYp += 2; 109: if (Key_LEFT) 110: tankXp -= 2; 111: if (Key_RIGHT) 112: tankXp += 2; 113: } 114: } 115: 116: public void stop() 117: { 118: // 쓰레드를 종료한다 119: if (trdMain != null) { 120: trdMain.stop(); 121: trdMain = null; 122: } 123: } 124: 125: public void update(Graphics g) 126: { 127: // update의 고약한 버릇을 없애기 위해 덮어썼음 128: paint(g); 129: } 130: 131: public void paint(Graphics g) 132: { 133: // 이미지가 완전리 로드 되었는지 체크 134: if ((tracker.statusAll(true) & MediaTracker.COMPLETE) != 0) { 135: // 이전 위치의 탱크를 지우기 위해 메모리 버퍼를 배경색으로 136: // 깨끗히 지운다. 137: grpOffScr.setColor(getBackground()); 138: grpOffScr.fillRect(0, 0, size().width, size().height); 139: // 탱크의 현재 방향에 따라 탱크의 이미지를 메모리 버퍼에 그린다 140: switch (tankDir) { 141: case UP: 142: grpOffScr.drawImage( imgUp, tankXp, tankYp, this); 143: break; 144: case RIGHT: 145: grpOffScr.drawImage( imgRight, tankXp, tankYp, this); 146: break; 147: case DOWN: 148: grpOffScr.drawImage( imgDown, tankXp, tankYp, this); 149: break; 150: case LEFT: 151: grpOffScr.drawImage( imgLeft, tankXp, tankYp, this); 152: break; 153: } 154: // 메모리 버퍼의 내용을 화면으로 옮긴다 155: g.drawImage(imgOffScr, 0, 0, this); 156: } else 157: // 이미지가 로드 중이면 메시지를 출력한다 158: g.drawString("Loading Image....", 20, 20); 159: } 160: 161: public boolean keyDown(Event evt, int key) 162: { 163: // PUSH된 키값에 따라 해당키의 플랙을 true로 설정하고 164: // 탱크의 방향을 바꾼다 165: switch (key) { 166: case 1004: 167: Key_UP = true; 168: tankDir = UP; 169: break; 170: case 1005: 171: Key_DOWN = true; 172: tankDir = DOWN; 173: break; 174: case 1006: 175: Key_LEFT = true; 176: tankDir = LEFT; 177: break; 178: case 1007: 179: Key_RIGHT = true; 180: tankDir = RIGHT; 181: break; 182: } 183: return true; // 처리 완료! 184: } 185: 186: public boolean keyUp(Event evt, int key) 187: { 188: // POP된 키의 플랙을 false 로 설정한다 189: switch (key) { 190: case 1004: 191: Key_UP = false; 192: break; 193: case 1005: 194: Key_DOWN = false; 195: break; 196: case 1006: 197: Key_LEFT = false; 198: break; 199: case 1007: 200: Key_RIGHT = false; 201: break; 202: } 203: return true; 204: } 205: } [소스2] Tank.java 소스가 다소 길지만 지금까지 해온 내용들을 이해하고 오셨다면 보시는데 큰 무리는 없었을 것입니다. Tank애플릿을 실행해 보면 화면 중앙에 탱크가 나올 것입니다. 상/하/좌/우키를 누르면 탱크는 방향을 바꾸고 앞으로 전진 을 할 것입니다. ☞ 새로 등장한 것들 ◇ final [소스2]의 29-32라인에서 사용된 final 키워드는 상수를 정의 하거나 더 이상 서브클래스에서 바꿀수 없는 메소드를 만들고 싶을때 사용합니 다. final 키워드가 붙은 데이터멤버는 우리가 임의로 바꿀 수 없고 참 조만 가능합니다. 만약 우리가 데이터 멤버의 값을 바꾸려 한다면 Java 컴파일러는 컴파일을 거부할 것입니다. ◇ Component.size() 이 메소드는 해당 Component 객체의 크기를 Dimension 객체형태로 되 돌립니다. Dimension 클래스는 width 와 height 데이터 멤버를 가지며, 각각 너비와 높이에 대한 데이터를 가지고 있습니다. [소스2]에서는 애 플릿이 홈페이지 상에서 얼마 만큼의 크기를 가지는지를 알아내기 위해 사용하고 있습니다. ◇ Component.requestFocus() requestFocus 메소드는 OS에게 Focus를 요청합니다. Focus를 가진 윈 도우만이 키보드 입력을 받을 수 있으므로 Tank애플릿이 키보드 입력을 받을 수 있게 하기위해 사용되었습니다. ◇ Component.keyDown(Event evt, int key) Component.keyUp(Event evt, int key) 키보드가 PUSH 되거나 POP 될 때 OS에 의해 불려지는 핸들러 입니다. 첫번째 인수로 발생된 이벤트에 대한 정보가 넘어오고, 두번째 인수로 는 PUSH/POP된 키의 코드가 전달 됩니다. 이 두메소드는 boolean 값을 리턴하는데 true 는 모든 처리가 끝났음을 알리는 것이고, false 는 두 메소드가 포함된 Component 의 부모 Component 에게 이벤트를 넘기라는 의미 입니다. 보통 true를 사용합니다. 3.2 애플릿에서 마우스는 어떻게 처리합니까? 입력장치 제어 방법 두번째로 마우스에 대해서 알아보겠습니다. 마우스는 키보드에 비해서 훨씬 다양한 이벤트를 발생시킵니다. 마우스의 버튼이 PU- SH/POP될 때, 마우스 커서가 애플릿 영역을 돌아다닐 때, 마우스 버튼을 누 른채로 애플릿 영역안을 돌아다닐 때, 마우스 커서가 애플릿 영역을 빠져나 가고 들어올 때 각각 OS에 의해 이벤트가 발생됩니다. 이 이벤트들에 대한 핸들러는 Component에 다음과 같이 정의 되어 있습니다. public boolean mouseEnter(Event evt, int x, int y) : 마우스 커서가 애플릿안으로 들어옴 public boolean mouseExit(Event evt, int x, int y) : 마우스 커서가 애플릿 밖으로 나감 public boolean mouseMove(Event evt, int x, int y) : 마우스 커서가 애플릿 내를 돌아다님 public boolean mouseDown(Event evt, int x, int y) : 마우스 버튼이 눌려짐 public boolean mouseDrag(Event evt, int x, int y) : 버튼을 누른채로 마우스를 움직임 public boolean mouseUp(Event evt, int x, int y) : 마우스 버튼을 놓음 [표2] 마우스 이벤트 처리 핸들러 마우스 이벤트 핸들러들은 공통적인 인수를 가지고 있는데, 두 번째와 세 번째 인수인 (x, y)는 해당 이벤트가 발생한 시점에서 마우스커서의 애플릿 내의 위치를 뜻합니다. 연습삼아 간단한 프로그램을 만들어 보도록 하겠습 니다. 화면내에 푸른색 상자가 하나 디스플레이 되고, 윈도우즈에서 아이콘 을 옮기듯이 마우스로 옮길 수 있도록 해보죠. 이것을 구현하는 방법은 상자의 내부에서 마우스의 버튼이 눌려지면 상자 는 이동 가능한 상태로 변하고, 마우스 드래깅(버튼을 누르고 커서를 옮김) 이 일어나면 상자의 위치를 변화시키면 될것입니다. 현재 상자의 상태가 이 동가능한 상태가 아닐 경우 마우스 드래깅에는 별 반응을 보이지 않아야 합 니다. 소스를 보시기 바랍니다. 01: import java.awt.*; 02: import java.applet.*; 03: 04: public class MouseTest extends Applet 05: { 06: private Rectangle box; // 화면에 나타날 상자 07: private Color boxColor; // 상자의 색깔 08: private boolean boxFlag; // 상자의 상태 플랙 09: 10: public void start() 11: { 12: int x, y, w, h; 13: 14: // 애플릿의 크기에 따라 상자의 크기를 조절 (애플릿 크기의 1/10으로) 15: w = size().width / 10; 16: h = size().height / 10; 17: x = (size().width - w) / 2; 18: y = (size().height- h) / 2; 19: 20: // 상자를 만든다 21: box = new Rectangle(x, y, w, h); 22: 23: // 상자의 초기 색깔과 상태를 설정 24: boxColor = Color.blue; 25: boxFlag = false; 26: } 27: 28: public void update(Graphics g) 29: { 30: // 나쁜버릇 제거.. 31: paint(g); 32: } 33: 34: public void paint(Graphics g) 35: { 36: // 화면을 바탕색으로 지우고 37: g.setColor(getBackground()); 38: g.fillRect(0, 0, size().width, size().height); 39: // 상자를 그린다 40: g.setColor(boxColor); 41: g.fillRect(box.x, box.y, box.width, box.height); 42: } 43: 44: public boolean mouseDown(Event e, int x, int y) 45: { 46: // 상자위에서 버튼이 PUSH됐으면 47: if (box.inside(x, y)) { 48: // 상자의 색깔을 바꾸고 눌려짐 상태를 true로 한다 49: boxFlag = true; 50: boxColor = Color.red; 51: repaint(); 52: } 53: return true; 54: } 55: 56: public boolean mouseUp(Event e, int x, int y) 57: { 58: // 버튼이 POP 됐으면 상자 상태를 false로 하고 색깔을 바꾼다 59: boxFlag = false; 60: boxColor = Color.blue; 61: repaint(); 62: return true; 63: } 64: 65: public boolean mouseDrag(Event e, int x, inty) 66: { 67: // 상자의 눌려짐 상태가 true일 경우 드래그가 발생하면 68: if (boxFlag) { 69: // 상자를 이동시킨다 70: box.x = x - box.width / 2; 71: box.y = y - box.height / 2; 72: repaint(); 73: } 74: return true; 75: } 76: } [소스3] MouseTest.java 앞의 설명대로 상자는 현재의 상태를 boxFlag에 기록하고 있습니다. 사용 자에 의해 버튼이 눌려져 44번 라인의 mouseDown 메소드가 호출되면 버튼이 눌려진 위치가 상자의 영역에 포함되는지 검사합니다. 만약 포함이 되어 있 다면 상자의 상태를 이동가능한 상태로 바꾸고, 상자의 색상을 붉은 색으로 바꿔준 후 화면을 갱신합니다. 눌려진 버튼이 놓여지면 56번 라인의 mouse- Up 메소드가 호출되고, 상자의 상태를 이동 불가능 상태로 바꾸고, 색깔을 푸른색으로 되돌리고 화면을 갱신합니다. 드래깅을 했을 경우 65번 라인의 mouseDrag 메소드가 호출되고 현재 상자 의 상태가 이동 가능한 상태이면 상자의 위치를 이동시키고, 이동 불가능한 상태라면 그냥 무시합니다. ☞ 새로 등장한 것들 ◇ Rectangle - Rectangle(int x, int y, int w, int h) 이 클래스는 사각형을 정의하고 사각형에 대한 각종 연산을 제공합니 다. 생성시에 사각형의 좌측 상단좌표 (x, y)와 사각형의 가로 세로 길 이 (w, h)를 넘겨줘야 합니다. Rectangle클래스의 데이터 멤버는 x, y, width, height 등이 있습니다. ◇ Rectangle.inside(int x, int y) 주어진 좌표 (x, y)가 사각형내에 포함되는지 여부를 체크합니다. 사 각형 내에 포함되면 true값을 리턴하고, 포함되지 않으면 false값을 리 턴합니다. 이상으로 화면처리에 관한 것은 모두 끝이 났습니다. 이정도로 게임에 사 용되는 모든 그래픽 기법을 배운 것은 아니지만, 앞으로 공부해 나갈것들의 기본이 되는 내용들이므로 무척 중요한 내용들 입니다. 뒷장을 공부하면서 이해가 잘 안되는 부분이나 막히는 부분이 있을 때는 다시 차분히 이 절을 읽어 보시면 많은 도움이 될 것입니다.
'자바 > 자바게임' 카테고리의 다른 글
자바게임프로그래밍#5 (0) | 2010.10.28 |
---|---|
자바게임프로그래밍#4 (0) | 2010.10.28 |
자바게임프로그래밍#3 (0) | 2010.10.28 |
자바게임프로그래밍#2 (1) | 2010.10.28 |
자바게임프로그래밍#1 (1) | 2010.10.28 |