자바 기반 스네이크 게임 구현

Java Swing을 활용하여 고전적인 '스네이크' 게임을 개발하는 방법에 대해 설명합니다. 이 프로젝트는 그래픽 요소와 사용자 입력 처리, 타이머 기반 애니메이션, 충돌 감지 등을 포함하며, GUI 기반의 간단한 게임 로직을 이해하는 데 적합한 예제입니다.

게임 구성 요소

프로젝트는 Snakes라는 패키지 내에 두 개의 주요 클래스로 구성됩니다. 하나는 게임을 실행하는 시작 클래스이며, 다른 하나는 게임 화면과 로직을 담당하는 패널 클래스입니다.

주요 클래스: SnakeGamePanel

다음은 게임의 핵심 로직을 담당하는 패널 클래스입니다. 이 클래스는 JPanel을 상속하고, ActionListener 인터페이스를 구현하여 주기적인 리프레시를 처리합니다.

package Snakes;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.JOptionPane;
import javax.swing.Timer;

public class SnakeGamePanel extends JPanel implements ActionListener {
    private boolean isRunning;          // 게임 실행 상태
    private int gameSpeed;              // 게임 속도 (ms)
    private boolean foodExists;         // 음식 존재 여부
    private int foodX, foodY;           // 음식 위치
    private ArrayList<int[]> snakeBody;// 뱀의 몸체 좌표 목록
    private String currentDir;          // 현재 방향
    private String nextDir;             // 다음 방향 (입력 버퍼링용)
    private int score;                  // 점수
    private int foodCount;              // 먹은 음식 수
    private Random random;

    // 이미지 리소스
    private ImageIcon headUp, headDown, headLeft, headRight;
    private ImageIcon bodyImage, foodImage, titleImage;

    // 타이머
    private Timer gameTimer;

    public SnakeGamePanel(int speed) {
        this.gameSpeed = speed;
        setPreferredSize(new java.awt.Dimension(900, 700));
        setBackground(Color.BLACK);
        setFocusable(true);

        initializeResources();
        initializeGame();
        addKeyListener(new InputHandler());
    }

    private void initializeResources() {
        // 이미지 경로는 프로젝트 리소스 폴더 기준으로 수정 가능
        headUp = new ImageIcon("images/up.png");
        headDown = new ImageIcon("images/down.png");
        headLeft = new ImageIcon("images/left.png");
        headRight = new ImageIcon("images/right.png");
        bodyImage = new ImageIcon("images/body.png");
        foodImage = new ImageIcon("images/food.png");
        titleImage = new ImageIcon("images/title.jpg");

        random = new Random();
        gameTimer = new Timer(gameSpeed, this);
    }

    private void initializeGame() {
        isRunning = false;
        currentDir = "U";
        nextDir = "U";
        score = 0;
        foodCount = 0;
        foodExists = false;

        snakeBody = new ArrayList<>();
        // 초기 뱀 길이 (머리 + 2개의 몸통)
        snakeBody.add(new int[]{150, 200}); // 머리
        snakeBody.add(new int[]{150, 225});
        snakeBody.add(new int[]{150, 250});

        generateFood();
        gameTimer.start();
    }

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

        // 제목 표시
        titleImage.paintIcon(this, g, 25, 10);

        // 게임 영역 테두리
        g.setColor(Color.WHITE);
        g.drawRect(25, 75, 850, 600);

        // 음식 그리기
        if (!foodExists) {
            generateFood();
        }
        drawFood(g);

        // 뱀 머리 그리기
        drawSnakeHead(g);
        // 뱀 몸통 그리기
        drawSnakeBody(g);

        // UI 정보 표시
        g.setColor(Color.GREEN);
        g.setFont(new Font("Dialog", Font.BOLD, 16));
        g.drawString("길이: " + (snakeBody.size() - 1), 30, 50);
        g.drawString("점수: " + score, 120, 50);

        // 일시정지 상태 표시
        if (!isRunning) {
            g.setColor(Color.YELLOW);
            g.setFont(new Font("Dialog", Font.BOLD, 24));
            g.drawString("시작하려면 아무 키나 누르세요", 200, 350);
            gameTimer.stop();
        } else {
            gameTimer.start();
        }
    }

    private void generateFood() {
        boolean validPosition;
        do {
            validPosition = true;
            foodX = 25 + random.nextInt(33) * 25;
            foodY = 75 + random.nextInt(23) * 25;

            for (int[] part : snakeBody) {
                if (part[0] == foodX && part[1] == foodY) {
                    validPosition = false;
                    break;
                }
            }
        } while (!validPosition);
        foodExists = true;
    }

    private void drawFood(Graphics g) {
        // 특별 음식: 5개마다 등장
        if (foodCount % 5 == 0) {
            g.setColor(Color.YELLOW);
            g.fillRect(foodX, foodY, 25, 25);
        } else {
            foodImage.paintIcon(this, g, foodX, foodY);
        }
    }

    private void drawSnakeHead(Graphics g) {
        ImageIcon headIcon = switch (currentDir) {
            case "R" -> headRight;
            case "L" -> headLeft;
            case "U" -> headUp;
            case "D" -> headDown;
            default -> headUp;
        };
        headIcon.paintIcon(this, g, snakeBody.get(0)[0], snakeBody.get(0)[1]);
    }

    private void drawSnakeBody(Graphics g) {
        for (int i = 1; i < snakeBody.size(); i++) {
            bodyImage.paintIcon(this, g, snakeBody.get(i)[0], snakeBody.get(i)[1]);
        }
    }

    private boolean checkSelfCollision() {
        int headX = snakeBody.get(0)[0];
        int headY = snakeBody.get(0)[1];

        // 머리 다음 부분부터 충돌 검사
        for (int i = 1; i < snakeBody.size(); i++) {
            if (snakeBody.get(i)[0] == headX && snakeBody.get(i)[1] == headY) {
                return true;
            }
        }
        return false;
    }

    private void moveSnake() {
        if (checkSelfCollision()) {
            JOptionPane.showMessageDialog(this, "게임 오버! 최종 점수: " + score);
            initializeGame();
            repaint();
            return;
        }

        // 새로운 머리 위치 계산
        int[] newHead = snakeBody.get(0).clone();

        switch (currentDir) {
            case "R" -> newHead[0] = (newHead[0] + 25) % 875 + 25;
            case "L" -> newHead[0] = (newHead[0] - 25 + 875) % 875 + 25;
            case "U" -> newHead[1] = (newHead[1] - 25 + 600) % 600 + 75;
            case "D" -> newHead[1] = (newHead[1] + 25) % 600 + 75;
        }

        // 음식 먹었는지 확인
        if (newHead[0] == foodX && newHead[1] == foodY) {
            foodExists = false;
            foodCount++;
            score += (foodCount % 5 == 0) ? 50 : 10;
            snakeBody.add(0, newHead); // 머리 앞에 추가
        } else {
            snakeBody.add(0, newHead);
            snakeBody.remove(snakeBody.size() - 1);
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (isRunning) {
            moveSnake();
        }
        repaint();
    }

    private class InputHandler extends KeyAdapter {
        @Override
        public void keyPressed(KeyEvent e) {
            int key = e.getKeyCode();

            // 방향 전환 (뒤로 가는 것은 금지)
            if ((key == KeyEvent.VK_LEFT || key == KeyEvent.VK_A) && !currentDir.equals("R")) {
                nextDir = "L";
            } else if ((key == KeyEvent.VK_RIGHT || key == KeyEvent.VK_D) && !currentDir.equals("L")) {
                nextDir = "R";
            } else if ((key == KeyEvent.VK_UP || key == KeyEvent.VK_W) && !currentDir.equals("D")) {
                nextDir = "U";
            } else if ((key == KeyEvent.VK_DOWN || key == KeyEvent.VK_S) && !currentDir.equals("U")) {
                nextDir = "D";
            } else if (key == KeyEvent.VK_SPACE) {
                isRunning = !isRunning;
            }

            // 일시정지 상태에서 아무 키나 누르면 시작
            if (!isRunning && key != KeyEvent.VK_SPACE) {
                isRunning = true;
            }
            repaint();
        }
    }
}

실행 클래스: GameLauncher

다음은 게임을 시작하고 창을 생성하는 메인 클래스입니다.

package Snakes;

import javax.swing.JFrame;
import javax.swing.JOptionPane;

public class GameLauncher {
    public static void main(String[] args) {
        String input = JOptionPane.showInputDialog(null,
                "게임 속도를 선택하세요 (1~5):", "설정", JOptionPane.QUESTION_MESSAGE);

        int speedLevel = 3; // 기본값
        try {
            speedLevel = Integer.parseInt(input);
            if (speedLevel < 1 || speedLevel > 5) speedLevel = 3;
        } catch (NumberFormatException e) {
            speedLevel = 3;
        }

        int delay = 200 - (speedLevel - 1) * 30; // 레벨에 따라 속도 조절

        JFrame frame = new JFrame("스네이크 게임");
        SnakeGamePanel gamePanel = new SnakeGamePanel(delay);

        frame.add(gamePanel);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

이 구현은 기본적인 스네이크 게임의 모든 요소를 포함하고 있으며, 이미지 리소스를 활용해 시각적 완성도를 높였습니다. 벽 통과 기능, 점수 시스템, 특수 음식, 입력 지연 방지 등의 기능을 통해 플레이 경험을 향상시켰습니다.

태그: java swing game-development snake-game Graphics

7월 27일 08:56에 게시됨