Java

Swing 예제, KeyListener를 활용해 방향키 기반 게임 만들기

데브노트 2025. 4. 30. 19:14

⭐Swing을 활용해 간단한 게임을 만들어봤다.

 

요청사항
----- ----- -----

캐릭터가 방향키에 맞게 움직이도록 만들것

움직이는 장애물을 만들것

캐릭터와 장애물이 충돌시 캐릭터가 삭제되도록 할 것

 

🤔기억해야 할 내용

쓰레드, While문을 활용해

사용자가 조작하지 않아도 스스로 움직이는 장애물을 설계할 수 있다.

 

1. 클래스 생성

JFrame 상속

KeyListener 구현

package _game;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class GameFrame extends JFrame implements KeyListener {

 

2. 변수 선언

이미지를 움직이기 위해

BufferImage 클래스를 불러왔다.

 

자동으로 움직이는 장애물을 구현하기 위해

내부클래스도 변수로 선언한다.

 

또한 컴포넌트들의 좌표값도 변수로 설정했다.

 

조건문 적용을 위해 플래그 값도 선언해둔다.

//변수
private BufferedImage backgroundImage;
private BufferedImage player1;
private BufferedImage player2;

private ImagePanel imagePanel;

private int playerX = 200;
private int playerY = 300;

private int player2X = 100;
private int player2Y = 300;

private boolean flag = true;

 

3. 생성자

생성자는 가장 먼저 실행되는 구문이다.

start() 구문으로 내부클래스 안에 만들어둔 run() 구문이 수행되도록 한다.

//생성자
public GameFrame() {
    initDate();
    setInitLayout();
    addEventListener();

    //메인작업자가 서브작업자를 생성
    Thread thread1 = new Thread(imagePanel);
    thread1.start();
    //imagePanel 안에 구현한 run() 메서드가 시작된다.
}

 

4. 메서드 - initData

생성자 호출시 실행

각 요소들을 생성한다.

 

ImageIO.read(new File(주소값))

은 주소값이 틀리면 런타임 오류를 발생시킨다.

try-catch 구문으로 예외처리한다.

//메서드
private void initDate() {
    setSize(1000, 600);
    setDefaultCloseOperation(3);
    setResizable(false);

    try {
        backgroundImage = ImageIO.read(new File("images/background.png"));
        player1 = ImageIO.read(new File("images/player1.png"));
        player2 = ImageIO.read(new File("images/player2.png"));

        //TODO player 이미지도 올려야 함
    } catch (IOException e) {
        e.printStackTrace();
    }

    imagePanel = new ImagePanel();

}//initDate

 

4-1. 메서드 setInitLayout

역시 생성자 호출시 실행

디자인을 정의한다.

 

setLayout을 지정하지 않았으므로

기본배치관리자인 BorderLayout이 적용됐다.

 

setLayout(new BorderLayout());

이런식으로 직접 넣어줘도 된다.

private void setInitLayout() {

    add(imagePanel);

    setVisible(true);

}//setInitLayout

 

4-2. 메서드 addEventListener

역시 생성자 호출시 실행

프로그램이 키 값을 인지하도록 만든다.

private void addEventListener() {

    /*
    keyListener는 인터페이스다.

    자바 문법에서는
    인터페이스나 추상클래스를
    구현클래스, 즉 객체로 사용하는 문법을 제공한다.

    new KeyListener() {
    추상 메서드를 구현 메서드로 오버라이드}
     */

    addKeyListener(this);
    /*
    JFrame 자체에
    KeyEventListener를 등록한다.
     */
}//addEventListener

 

5. 인터페이스의 추상메서드 오버라이드

키가 눌려질 때

KeyPressed

메서드에서 

정해진 키값(방향키)을 인식하면

캐릭터의 좌표값이 변화하도록 설계했다.

 

KeyEvent.VK_UP

은 위쪽방향키의 주소값을 가리킨다.

 

변한 값이 화면에 나타나도록

키를 입력할때마다

repaint() 가 호출되도록 한다.

@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}//keyReleased

@Override
public void keyPressed(KeyEvent e) {
    System.out.println("키 코드: " + e.getKeyCode());

    //TODO 화살표만 추출해 낼 예정

    int code = e.getKeyCode();

    if (code == KeyEvent.VK_UP) {
        playerY -= 10;
    } else if (code == KeyEvent.VK_DOWN) {
        playerY += 10;
    } else if (code == KeyEvent.VK_LEFT) {
        playerX -= 10;
    } else if (code == KeyEvent.VK_RIGHT) {
        playerX += 10;
    }

    //리페인트
    repaint();

}//keyPressed

 

6.내부 클래스

JPanel 상속

Runnable 구현

내부클래스를 만들고

여기에 서브작업자가 할 일을 명시했다.

//내부클래스
private class ImagePanel extends JPanel implements Runnable {

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        g.drawImage(backgroundImage, 0, 0, 1000, 600, null);
        g.drawImage(player1, playerX, playerY, 50, 50, null);
        g.drawImage(player2, player2X, player2Y, 50, 50, null);
    }

    @Override
    public void run() {
        //서브작업자가 해야하는 일을 명시하도록 약속돼 있다.

        boolean direction = false;
        //direction true 오른쪽, false 왼쪽

        while (flag) {

            if (direction == true) {
                player2X += 5;
            } else {
                player2X -= 5;
            }

            if (player2X >= 800) {
                direction = false;
            }

            if (player2X <= 100) {
                direction = true;
            }

            if (Math.abs(playerX - player2X) < 25 && Math.abs(playerY - player2Y) < 25) {
                player1 = null;
                System.out.println(playerX+ "," + playerY);
                System.out.println(player2X + "," + player2Y);
            }

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            repaint();

        }//end of while

    }//run

}//end of inner class

 

7. 메인함수

    //메인
    public static void main(String[] args) {

        new GameFrame();

    }//end of main

}//end of outer class

 

 

예제2)

부활버튼을 달아서 캐릭터가 죽어도 계속 살아날수 있게 해봤다.

 

바꿀 점

JButton을 추가

 

구조
===== ===== ===== ===== ===== ===== 
JFrame 상속/ KeyListener 구현

//변수
BufferImage (3), 내부클래스, 좌표변수, 플래그

//생성자 (3 + Thread)

//메서드 (3)

//오버라이드
KeyPressed

//내부클래스
JPanel 상속/ Runnable 구현

//내부클래스 오버라이드
paintComponent
run (While)

//메인

 

===== ===== ===== ===== ===== ===== 

JButton을 추가했을 뿐인데 KeyListener가 동작하지 않는 문제가 발생했다.

 

해결방법.

 

initData() 메서드에

setFocusable(true);
requestFocusInWindow();

구문을 넣어 포커스를 되돌렸다.

 

부활 뒤에도

requestFocusInWindow();

구문을 넣어 포커스를 되돌렸다.

 

챗지피티에 따르면

KeyListener가 아닌 KeyBinding을 사용하면

포커스 문제를 원천차단할 수 있다.

package _my;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

/**
 * 구조
 * =====
 * JFrame 상속/ KeyListener 구현
 * <p>
 * //변수
 * BufferImage (3), 내부클래스, 좌표변수, 플래그
 * <p>
 * //생성자 (3 + Thread)
 * <p>
 * //메서드 (3)
 * <p>
 * //오버라이드
 * KeyPressed
 * <p>
 * //내부클래스
 * JPanel 상속/ Runnable 구현
 * <p>
 * //내부클래스 오버라이드
 * paintComponent
 * run (While)
 * <p>
 * //메인
 */
public class Image_Game extends JFrame implements KeyListener, ActionListener {

    //멤버변수
    private JButton button;

    private BufferedImage backImg;
    private BufferedImage player1Img;
    private BufferedImage player2Img;

    private ImagePanel imagePanel;

    private int player1X = 400;
    private int player1Y = 50;

    private int player2X = 300;
    private int player2Y = 400;

    private boolean flag = true;

    //생성자
    public Image_Game() {
        initData();
        setInitLayout();
        addEventListener();
        addActionListener();

        Thread thread1 = new Thread(imagePanel);
        thread1.start();

    }

    //메서드
    private void initData() {
        setSize(1000, 600);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setResizable(false);

        button = new JButton("부활버튼");

        try {
            backImg = ImageIO.read(new File("images/background.png"));
            player1Img = ImageIO.read(new File("images/player1.png"));
            player2Img = ImageIO.read(new File("images/player2.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }

        imagePanel = new ImagePanel();

    }//initData

    private void setInitLayout() {

        add(button, BorderLayout.NORTH);

        add(imagePanel);

        setVisible(true);

        setFocusable(true);
        requestFocusInWindow();

    }//setInitLayout

    private void addEventListener() {

        addKeyListener(this);

    }//addEventListener

    private void addActionListener() {

        button.addActionListener(this);

    }


    @Override
    public void keyPressed(KeyEvent e) {
        System.out.println("키코드:" + e.getKeyCode());

        int keyNo = e.getKeyCode();

        if (keyNo == KeyEvent.VK_UP) {
            player1Y -= 10;
        } else if (keyNo == KeyEvent.VK_DOWN) {
            player1Y += 10;
        } else if (keyNo == KeyEvent.VK_LEFT) {
            player1X -= 10;
        } else if (keyNo == KeyEvent.VK_RIGHT) {
            player1X += 10;

        }

        repaint();

    }//keyPressed

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void actionPerformed(ActionEvent e) {

        player1X = 250; player1Y = 250;

        try {
            player1Img = ImageIO.read(new File("images/player1.png"));
            requestFocusInWindow();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }

    }


    //내부클래스
    private class ImagePanel extends JPanel implements Runnable {

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            g.drawImage(backImg, 0, 0, 1000, 600, null);
            g.drawImage(player1Img, player1X, player1Y, 100, 100, null);
            g.drawImage(player2Img, player2X, player2Y, 100, 100, null);
        }

        @Override
        public void run() {

            boolean direct = true;

            while (flag) {

                if (direct == true) {
                    player2X += 5;
                } else {
                    player2X -= 5;
                }

                if (player2X >= 800) {
                    direct = false;
                }

                if (player2X <= 100) {
                    direct = true;
                }

                if ((Math.abs(player1X - player2X) <= 25) && (Math.abs(player1Y - player2Y) <= 25)) {
                    System.out.println("사망지점:"+player1X +","+player1Y);
                    player1Img = null;
                }

                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                repaint();

            }//while
        }//run
    }//inner

    //메인
    public static void main(String[] args) {
        new Image_Game();
    }//end of main
}//end of GF

 

 

예제2-2) 챗지티피가 제안한 KeyBinding 방식의 코드

package _my;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class Image_Game2 extends JFrame implements ActionListener {

    //멤버변수
    private JButton button;

    private BufferedImage backImg;
    private BufferedImage player1Img;
    private BufferedImage player2Img;

    private ImagePanel imagePanel;

    private int player1X = 400;
    private int player1Y = 50;

    private int player2X = 300;
    private int player2Y = 400;

    private boolean flag = true;

    //생성자
    public Image_Game2() {
        initData();
        setInitLayout();
        addActionListener();
        initKeyBindings();

        Thread thread1 = new Thread(imagePanel);
        thread1.start();

    }

    //메서드
    private void initData() {
        setSize(1000, 600);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setResizable(false);

        button = new JButton("부활버튼");

        try {
            backImg = ImageIO.read(new File("images/background.png"));
            player1Img = ImageIO.read(new File("images/player1.png"));
            player2Img = ImageIO.read(new File("images/player2.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }

        imagePanel = new ImagePanel();

    }//initData

    private void setInitLayout() {

        add(button, BorderLayout.NORTH);

        add(imagePanel);

        setVisible(true);

    }//setInitLayout

    private void addActionListener() {

        button.addActionListener(this);
        
    }//addActionListener

    private void initKeyBindings() {
        InputMap inputMap = imagePanel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = imagePanel.getActionMap();

        inputMap.put(KeyStroke.getKeyStroke("UP"), "moveUp");
        inputMap.put(KeyStroke.getKeyStroke("DOWN"), "moveDown");
        inputMap.put(KeyStroke.getKeyStroke("LEFT"), "moveLeft");
        inputMap.put(KeyStroke.getKeyStroke("RIGHT"), "moveRight");

        actionMap.put("moveUp", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player1Y -= 10;
                repaint();
            }
        });

        actionMap.put("moveDown", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player1Y += 10;
                repaint();
            }
        });

        actionMap.put("moveLeft", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player1X -= 10;
                repaint();
            }
        });

        actionMap.put("moveRight", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                player1X += 10;
                repaint();
            }
        });
    }//initKeyBindings

    @Override
    public void actionPerformed(ActionEvent e) {

        player1X = 250; player1Y = 250;

        try {
            player1Img = ImageIO.read(new File("images/player1.png"));
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }//actionPerformed

    //내부클래스
    private class ImagePanel extends JPanel implements Runnable {

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            g.drawImage(backImg, 0, 0, 1000, 600, null);
            g.drawImage(player1Img, player1X, player1Y, 100, 100, null);
            g.drawImage(player2Img, player2X, player2Y, 100, 100, null);
        }//paintComponent

        @Override
        public void run() {

            boolean direct = true;

            while (flag) {

                if (direct == true) {
                    player2X += 5;
                } else {
                    player2X -= 5;
                }

                if (player2X >= 800) {
                    direct = false;
                }

                if (player2X <= 100) {
                    direct = true;
                }

                if ((Math.abs(player1X - player2X) <= 25) && (Math.abs(player1Y - player2Y) <= 25)) {
                    System.out.println("사망지점:"+player1X +","+player1Y);
                    player1Img = null;
                }

                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                repaint();

            }//while
        }//run
    }//inner

    //메인
    public static void main(String[] args) {
        new Image_Game2();
    }//end of main
}//end of GF

키바인딩 방식을 사용하면 매번 포커스를 되돌리지 않아도 된다.