Post

[Java] Java GUI로 게임 만들기 2

📌 들어가며

저번의 간단한 게임을 이번엔 제법 그럴듯하게 발전시킨다. 7개 클래스로 나뉘며, 추상 클래스 상속·게임 쓰레드(루프)·어댑터·익명 클래스같은 중요한 개념이 총출동한다.

1
2
3
4
5
6
7
GameMain (JFrame, 메뉴바)
   └─ GamePanel (JPanel, 게임 쓰레드 + paintComponent)
        └─ GameComponents (추상, 공통 필드/메소드)
             ├─ GameBg   (배경)
             ├─ Hero     (주인공)
             ├─ Enemy    (적군)
             └─ Bullet   (총알)

1. GameMain — 메뉴바 + 익명 클래스 리스너

메뉴바로 게임 시작/일시정지/종료를 제어한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class GameMain extends JFrame {
    JMenuBar bar;
    JMenu menu;
    JMenuItem[] item = new JMenuItem[3];
    GamePanel gamePanel;

    public GameMain() {
        bar = new JMenuBar();
        menu = new JMenu("게임설정");
        item[0] = new JMenuItem("게임시작");
        item[1] = new JMenuItem("Pause");
        item[2] = new JMenuItem("게임종료");
        gamePanel = new GamePanel();

        for (JMenuItem obj : item) menu.add(obj);
        bar.add(menu);
        setJMenuBar(bar);
        add(gamePanel);
        pack(); setVisible(true);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);

        // 익명 클래스로 리스너 즉석 구현
        item[0].addActionListener(new ActionListener() {
            @Override public void actionPerformed(ActionEvent e) { gamePanel.check = true; }
        });
        item[1].addActionListener(new ActionListener() {
            @Override public void actionPerformed(ActionEvent e) { gamePanel.check = false; }
        });
        item[2].addActionListener(new ActionListener() {
            @Override public void actionPerformed(ActionEvent e) { System.exit(0); }
        });
    }
    public static void main(String[] args) { new GameMain(); }
}

💡 리스너를 implements하지 않고 new ActionListener() { ... }로 만든 게 익명 클래스다. 재사용하지 않고 일시적으로 쓸 때 유용하다. 이름이 없어 다시 참조할 수 없다.

⚠️ 메인 쓰레드의 역할은 프로그램 운영(GUI 이벤트 감지, 그래픽 처리)이다. 절대 대기 상태나 무한 루프에 빠지면 안 된다. (스마트폰 개발에선 메인 쓰레드의 루프 자체가 에러다.)


2. GameComponents — 공통을 모은 추상 클래스

배경·주인공·적군·총알은 좌표·크기·속도·이미지가 공통이다. 중복을 없애려 추상 클래스로 묶는다.

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class GameComponents {
    int x, y, width, height, velX, velY;   // 공통 필드
    Image img;

    public GameComponents(int x, int y, int width, int height, int velX, int velY, Image img) {
        super();
        this.x = x; this.y = y; this.width = width; this.height = height;
        this.velX = velX; this.velY = velY; this.img = img;
    }
    public abstract void tick();              // 운동량 변경 (자식마다 구현)
    public abstract void render(Graphics g);  // 화면에 그리기
}
메소드역할
tick()물리값(좌표) 변경
render(g)변경된 상태를 화면에 그림

이제 모든 게임 요소가 이를 상속해 자기만의 tick/render를 구현한다. 예를 들어 배경은 계속 왼쪽으로 흐르다 끝에 닿으면 되돌아온다.

1
2
3
4
5
6
7
8
9
10
public class GameBg extends GameComponents {
    public GameBg(int x, int y, int w, int h, int vx, int vy, Image img) {
        super(x, y, w, h, vx, vy, img);
    }
    public void tick() {
        this.x += this.velX;
        if (this.x < -width) this.x = width;   // 끝에 닿으면 되돌리기 (무한 스크롤)
    }
    public void render(Graphics g) { g.drawImage(img, x, y, width, height, null); }
}

💡 Hero, Enemy, Bullet도 구조가 거의 같다. 공통을 부모(추상)에 몰아넣어 중복을 최소화한 것이 이 설계의 핵심이다.


3. GamePanel — 게임의 심장(쓰레드 + 루프)

쓰레드란?

실행 중인 프로그램이 프로세스이고, 그 안에서 독립적으로 동시에 실행되는 세부 단위쓰레드다. OS가 프로세스를 관리하듯, 자바 쓰레드는 JVM(소형 OS)에 맡겨야 동시 실행된다.

생성자 — 게임 쓰레드 + 어댑터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public GamePanel() {
    setPreferredSize(new Dimension(WIDTH, HEIGHT));
    setFocusable(true);
    createBg(); createHero(); createEnemy();   // 생성은 메소드로 분리

    gameThread = new Thread() {
        @Override
        public void run() {
            while (true) {
                if (check) gameLoop();          // check가 true일 때만 루프
                try { Thread.sleep(10); }        // FPS 조절 (1000분의 10초)
                catch (InterruptedException e) {}
            }
        }
    };
    gameThread.start();   // JVM에 맡김

    // 어댑터로 필요한 키 이벤트만 구현
    this.addKeyListener(new KeyAdapter() {
        public void keyPressed(KeyEvent e) {
            switch (e.getKeyCode()) {
                case KeyEvent.VK_LEFT:  hero.velX = -5; break;
                case KeyEvent.VK_RIGHT: hero.velX =  5; break;
                case KeyEvent.VK_UP:    hero.velY = -5; break;
                case KeyEvent.VK_DOWN:  hero.velY =  5; break;
                case KeyEvent.VK_SPACE: createBullet();  break;
            }
        }
        public void keyReleased(KeyEvent e) {
            switch (e.getKeyCode()) {   // 키 떼면 정지
                case KeyEvent.VK_LEFT: case KeyEvent.VK_RIGHT: hero.velX = 0; break;
                case KeyEvent.VK_UP:   case KeyEvent.VK_DOWN:  hero.velY = 0; break;
            }
        }
    });
}

💡 게임 루프: gameLoop()은 내부적으로 repaint()만 호출한다. 쓰레드가 이걸 계속 반복하면 모든 요소의 좌표 변경이 다시 그려진다 — 멈추지 않는 심장이다. Pause 메뉴가 check=false로 만들면 루프가 멈춘다.

💡 어댑터: KeyListener를 직접 구현하면 3개 메소드를 다 구현해야 하지만, KeyAdapter(모두 빈 구현)를 상속하면 필요한 것만 재정의할 수 있다. Thread.sleep(10)은 CPU가 너무 빨라 게임이 미친 듯 빨라지는 걸 막는 FPS 조절이다.

이미지 로드 (classpath)

1
2
3
4
5
6
7
public void createBullet() {
    try {
        BufferedImage buffImg = ImageIO.read(
            this.getClass().getClassLoader().getResource("ball.png"));
        bulletArray.add(new Bullet(hero.x + 100, hero.y + 50, 20, 20, 15, 0, buffImg));
    } catch (IOException e) { e.printStackTrace(); }
}

💡 로컬 디렉터리 경로 대신 getClassLoader().getResource()로 이미지를 클래스패스에서 가져온다. 총알은 스페이스바를 누를 때마다 생기므로 ArrayList<Bullet>에 누적한다.

paintComponent — 모든 요소 그리기

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override  // repaint() → update() → paint()의 업그레이드판
public void paintComponent(Graphics g) {
    g.setColor(Color.YELLOW);
    g.fillRect(0, 0, WIDTH, HEIGHT);

    gameBg[0].tick(); gameBg[0].render(g);
    gameBg[1].tick(); gameBg[1].render(g);
    hero.tick();      hero.render(g);

    for (Bullet b : bulletArray) { b.tick(); b.render(g); }   // 누적 총알
    for (Enemy en : enemy)       { en.tick(); en.render(g); }
}
public void gameLoop() { repaint(); }   // 심장

Desktop View

주인공·적군·배경이 움직이고, 스페이스바로 총알도 날아간다.


📝 정리

1
2
3
4
5
6
슈팅 게임 (심화)
├─ 익명 클래스  메뉴 리스너를 일회용으로 구현
├─ 추상 클래스  GameComponents로 공통(좌표·속도·render) 통합
├─ 게임 쓰레드  while + sleep(FPS) + gameLoop(repaint) = 심장
├─ 어댑터       KeyAdapter로 필요한 키 이벤트만 구현
└─ 이미지       getClassLoader().getResource()로 classpath 로드
개념한 줄 정의
추상 클래스 상속공통 필드/동작을 부모에 몰아 중복 제거
게임 루프쓰레드가 repaint를 반복 (FPS는 sleep)
어댑터필요한 리스너 메소드만 재정의
익명 클래스일회용 리스너 구현

이번 게임은 “공통은 추상 클래스로, 반복 렌더링은 쓰레드 루프로, 입력은 어댑터로”라는 세 축이 맞물린다. 반드시 반복해서 보고 스스로 만들 수 있을 때까지 내 것으로 만들어야 할 예제다.

This post is licensed under CC BY 4.0 by the author.

Comments powered by Disqus.