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