Snake game in Java but my restart button does not work
My restart button of the game does not work and it multiples when it is clicked. I do not understand Java perfectly I am considering myself good.
Main of the game
package snake_game;
public class snake {
public static void main(String arg[]) {
new GameFrame();
// is exacly the same as frame f = new frame();
// this is shorter and does the same job
}
}
GamePanel
package snake_game;
// import java.awt.event.ActionEvent;
// import java.awt.event.ActionListener;
// import java.awt.Graphics;
// import java.awt.event.KeyAdapter;
// import java.awt.event.KeyEvent;
// or I could write simply
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.Random;
import javax.swing.JPanel;
public class GamePanel extends JPanel implements ActionListener {
// panal dimentions
static final int SCREEN_WIDTH = 600;
static final int SCREEN_HEIGHT = 600;
// panal dimentions
// size
static final int UNIT_SIZE = 25;
// size to make 600 * 600 = 1200 px equel between 25 px
static final int GAME_UNITS = (SCREEN_WIDTH * SCREEN_HEIGHT) / UNIT_SIZE;
// size
// delay how fast the game will be
static final int DELAY = 75;
// delay
// dimentions
final int x[] = new int[GAME_UNITS];
final int y[] = new int[GAME_UNITS];
// dimentions
// snake
int bodyParts = 6;
// snake
// apple
int appleEaten;
int appleX;
int appleY;
// apple
char direction = 'R';
boolean running = false;
Timer timer;
Random random;
GamePanel game;
JButton resetButton;
GamePanel() {
random = new Random();
this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
this.setBackground(Color.black);
this.setFocusable(true);
this.addKeyListener(new MyKeyAdapter());
startGame();
// when we want to make the program to continie we must say what the programm
// must execute next
}
public void startGame() {
newApple();
running = true;
timer = new Timer(DELAY, this);
timer.start();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
draw(g);
}
public void draw(Graphics g) {
if (running) {
for (int i = 0; i < SCREEN_HEIGHT / UNIT_SIZE; i++) {
g.drawLine(i * UNIT_SIZE, 0, i * UNIT_SIZE, SCREEN_HEIGHT);
g.drawLine(0, i * UNIT_SIZE, i * SCREEN_WIDTH, i * UNIT_SIZE);
}
g.setColor(Color.red);
g.fillOval(appleX, appleY, UNIT_SIZE, UNIT_SIZE);
for (int i = 0; i < bodyParts; i++) {
if (i == 0) {
g.setColor(Color.green);
g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
} else {
g.setColor(new Color(45, 180, 0));
// random color
g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255)));
// random color
g.fillRect(x[i], y[i], UNIT_SIZE, UNIT_SIZE);
}
}
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 30));
FontMetrics metrics = getFontMetrics(g.getFont());
g.drawString("SCORE:" + appleEaten, (SCREEN_WIDTH - metrics.stringWidth("SCORE:" + appleEaten)) / 2,
g.getFont().getSize());
} else {
gameOver(g);
}
}
public void newApple() {
appleX = random.nextInt((int) (SCREEN_WIDTH / UNIT_SIZE)) * UNIT_SIZE;
appleY = random.nextInt((int) (SCREEN_HEIGHT / UNIT_SIZE)) * UNIT_SIZE;
}
public void move() {
for (int i = bodyParts; i > 0; i--) {
x[i] = x[i - 1];
y[i] = y[i - 1];
}
switch (direction) {
case 'U':
y[0] = y[0] - UNIT_SIZE;
break;
case 'D':
y[0] = y[0] + UNIT_SIZE;
break;
case 'L':
x[0] = x[0] - UNIT_SIZE;
break;
case 'R':
x[0] = x[0] + UNIT_SIZE;
break;
}
}
public void checkApple() {
if ((x[0] == appleX) && (y[0] == appleY)) {
bodyParts++;
appleEaten++;
newApple();
}
}
public void checkCollisions() {
// check if head collides with body
for (int i = bodyParts; i > 0; i--) {
if ((x[0] == x[i]) && (y[0] == y[i])) {
running = false;
}
}
// check if head touches left border
if (x[0] < 0) {
running = false;
}
// check if head touches right border
if (x[0] > SCREEN_WIDTH) {
running = false;
}
// check if head touches top border
if (y[0] < 0) {
running = false;
}
// check if head touches bottom border
if (y[0] > SCREEN_HEIGHT) {
running = false;
}
if (!running) {
timer.stop();
}
}
public void gameOver(Graphics g) {
// score
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 30));
FontMetrics metrics1 = getFontMetrics(g.getFont());
g.drawString("SCORE:" + appleEaten, (SCREEN_WIDTH - metrics1.stringWidth("SCORE:" + appleEaten)) / 2,
g.getFont().getSize());
// game over text
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 75));
FontMetrics metrics2 = getFontMetrics(g.getFont());
g.drawString("Game Over", (SCREEN_WIDTH - metrics2.stringWidth("Game Over")) / 2, SCREEN_HEIGHT / 2);
// restart button
resetButton = new JButton();
resetButton.setText("Restart");
resetButton.setSize(100, 50);
resetButton.setLocation(150, 150);
resetButton.addActionListener(this);
game = new GamePanel();
this.add(resetButton);
this.add(game);
this.setVisible(true);
// restart button
}
@Override
public void actionPerformed(ActionEvent e) {
if (running) {
move();
checkApple();
checkCollisions();
}
repaint();
// restart button
if (e.getSource() == resetButton) {
this.remove(game);
game = new GamePanel();
this.add(game);
resetButton.setVisible(false);
SwingUtilities.updateComponentTreeUI(this);
// restart button
}
}
public class MyKeyAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_LEFT:
if (direction != 'R') {
direction = 'L';
}
break;
case KeyEvent.VK_RIGHT:
if (direction != 'L') {
direction = 'R';
}
break;
case KeyEvent.VK_UP:
if (direction != 'D') {
direction = 'U';
}
break;
case KeyEvent.VK_DOWN:
if (direction != 'U') {
direction = 'D';
}
break;
}
}
}
}
GameFrame
package snake_game;
import javax.swing.JFrame;
public class GameFrame extends JFrame {
GameFrame() {
GamePanel panel = new GamePanel();
this.add(panel);
this.setTitle("Snake");
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setResizable(false);
this.pack();
this.setVisible(true);
this.setLocationRelativeTo(null);
this.setUndecorated(false);
}
}
Solution 1:
Introduction
I copied your code into my Eclipse IDE and ran it as is. I received the following runtime error.
Exception in thread "main" java.awt.IllegalComponentStateException: The frame is displayable.
at java.desktop/java.awt.Frame.setUndecorated(Frame.java:926)
at com.ggl.testing.SnakeGame$GameFrame.<init>(SnakeGame.java:41)
at com.ggl.testing.SnakeGame.main(SnakeGame.java:24)
Oracle has a helpful tutorial, Creating a GUI With Swing. Skip the Learning Swing with the NetBeans IDE section. Pay close attention to the Concurrency in Swing section.
I've been writing Swing code for over 10 years, and I have the Oracle website bookmarked in my browser. I still look up how to use certain components to make sure I'm using them correctly.
The first thing I did was to slow down your snake so I could test the game. I changed the delay of 75 to a delay of 750. Here's a screenshot of your current GUI.
Looking over your code, you extend a JFrame
. You don't need to extend a JFrame
. You're not changing any JFrame
functionality. It's much simpler to use a JFrame
. This leads to one of my Java rules.
Don't extend a Swing component, or any Java class, unless you intend to override one or more of the class methods.
You do extend a JPanel
. That's fine because you override the paintComponent
method.
Finally, your JPanel
class is doing too much work. You also make heavy use of static fields. Even though you'll only create one JPanel
, it's a good habit to treat every class as if you will create multiple instances of the class. This creates fewer problems for yourself down the road.
You have the right idea, creating three classes.
So let's rework your code, using some basic patterns and Swing best practices. At this time, I don't know how many classes we'll wind up creating.
Explanation
When I write a Swing GUI, I use the model–view–controller (MVC) pattern. The name implies that you create a model first, then the view, then the controller(s).
An application model consists of one or more plain Java getter/setter classes.
A view consists of a JFrame
, one or more JPanels
, and any other necessary Swing components.
The controller consists of one or more Actions
or ActionListeners
. In Swing, there's usually not one controller to "rule them all".
To summarize:
- The view reads information from the model
- The view does not update the model
- The controllers update the model and repaint/revalidate your view.
Model
I created two model classes, SnakeModel
and Snake
.
The SnakeModel
class is a plain Java getter/setter class that holds one Snake
instance, the apple eaten count, the apple location, the size of the game area, and a couple of booleans. One boolean indicates whether or not the game loop is running and the other boolean indicates whether or not the game is over.
The game area uses a java.awt.Dimension
to hold the width and height of the game area. The width and the height do not have to have the same value. The game area can be rectangular.
The game area is measured in units. In the view, I convert the units into pixels. That's the opposite of what you did. If you want to change the game area, all you have to do is change the dimensions in the SnakeModel
class. Everything in the view is based on the game area dimension.
The Snake
class holds a java.util.List
of java.awt.Point
objects and a char
direction. A java.awt.Point
object holds an X and Y value. Since we're dealing with objects, rather than int values, we have to be careful to clone the object when we want a new Point
.
View
All Swing applications must start with a call to the SwingUtilities
invokeLater
method. This method ensures that the Swing components are created and executed on the Event Dispatch Thread.
I created a JFrame
, a drawing JPanel
, and a separate button JPanel
. Generally, it's not a good idea to add Swing components to a drawing JPanel
. By creating a separate button JPanel
, I get the added feature of a "Start Game" button at almost no extra cost. The button is disabled while the game is running.
The JFrame
methods must be called in a specific order. The setVisible
method must be called last.
I made the drawing JPanel
more complicated by adding a separate area for the score.
I made the drawing JPanel
less complicated by only drawing the state of the game, based on the application model. Period. Nothing else.
I limited the random colors to the white end of the color spectrum to maintain the contrast between the snake and the drawing JPanel
background.
I used key bindings instead of a key listener. One advantage is that the drawing JPanel
doesn't have to be in focus. Since I have a separate button JPanel
, the drawing JPanel
doesn't have focus.
Another advantage is that I can add the WASD keys with four lines of additional code.
One disadvantage is that the key bindings code looks more complicated than a key listener. Once you've coded a few key bindings, you'll appreciate the advantages.
Controller
I created three controller classes, ButtonListener
, TimerListener
, and MovementAction
.
The ButtonListener
class implements ActionListener
. The ButtonListener
class initializes the game model and restarts the timer.
The TimerListener
class implements ActionListener
. The TimerListener
class is the game loop. This class moves the snake, checks to see if the apple is eaten, checks to see if the snake has moved outside the game area or touched itself, and repaints the drawing JPanel
. I used your code as a model for the code in this class.
The MovementAction
class extends AbstractAction
. The AbstractAction
class implements Action
. This class changes the direction of the snake, based on the key presses.
I create four instances of the MovementAction
class, one for each direction. This makes the actionPerformed
method of the class much simpler.
Images
Here's what the revised GUI looks like when you start the game.
Here's the revised GUI during the game.
Here's the revised GUI when the game is over.
Code
Here's the complete runnable code. I made all the additional classes inner classes so I could post this code as one block.
You should put the separate classes in separate files.
When setting up a Swing GUI project, I create separate packages for the model, view, and controller. This helps me keep the code organized.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class SnakeGame implements Runnable {
public static void main(String arg[]) {
SwingUtilities.invokeLater(new SnakeGame());
}
private final GamePanel gamePanel;
private final JButton restartButton;
private final SnakeModel model;
public SnakeGame() {
this.model = new SnakeModel();
this.restartButton = new JButton("Start Game");
this.gamePanel = new GamePanel(model);
}
@Override
public void run() {
JFrame frame = new JFrame("Snake");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(gamePanel, BorderLayout.CENTER);
frame.add(createButtonPanel(), BorderLayout.SOUTH);
frame.pack();
frame.setResizable(false);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private JPanel createButtonPanel() {
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panel.setBackground(Color.black);
restartButton.addActionListener(new ButtonListener(this, model));
panel.add(restartButton);
return panel;
}
public JButton getRestartButton() {
return restartButton;
}
public void repaint() {
gamePanel.repaint();
}
public class GamePanel extends JPanel {
private static final long serialVersionUID = 1L;
private final int margin, scoreAreaHeight, unitSize;
private final Random random;
private final SnakeModel model;
public GamePanel(SnakeModel model) {
this.model = model;
this.margin = 10;
this.unitSize = 25;
this.scoreAreaHeight = 36 + margin;
this.random = new Random();
this.setBackground(Color.black);
Dimension gameArea = model.getGameArea();
int width = gameArea.width * unitSize + 2 * margin;
int height = gameArea.height * unitSize + 2 * margin + scoreAreaHeight;
this.setPreferredSize(new Dimension(width, height));
setKeyBindings();
}
private void setKeyBindings() {
InputMap inputMap = this.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
ActionMap actionMap = this.getActionMap();
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "up");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "down");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "left");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "right");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), "up");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0), "down");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), "left");
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0), "right");
actionMap.put("up", new MovementAction(model, 'U', 'D'));
actionMap.put("down", new MovementAction(model, 'D', 'U'));
actionMap.put("left", new MovementAction(model, 'L', 'R'));
actionMap.put("right", new MovementAction(model, 'R', 'L'));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Dimension gameArea = model.getGameArea();
drawHorizontalGridLines(g, gameArea);
drawVerticalGridLines(g, gameArea);
drawSnake(g);
drawScore(g, gameArea);
if (model.isGameOver) {
drawGameOver(g, gameArea);
} else {
drawApple(g);
}
}
private void drawHorizontalGridLines(Graphics g, Dimension gameArea) {
int y1 = scoreAreaHeight + margin;
int y2 = y1 + gameArea.height * unitSize;
int x = margin;
for (int index = 0; index <= gameArea.width; index++) {
g.drawLine(x, y1, x, y2);
x += unitSize;
}
}
private void drawVerticalGridLines(Graphics g, Dimension gameArea) {
int x1 = margin;
int x2 = x1 + gameArea.width * unitSize;
int y = margin + scoreAreaHeight;
for (int index = 0; index <= gameArea.height; index++) {
g.drawLine(x1, y, x2, y);
y += unitSize;
}
}
private void drawApple(Graphics g) {
// Draw apple
g.setColor(Color.red);
Point point = model.getAppleLocation();
if (point != null) {
int a = point.x * unitSize + margin + 1;
int b = point.y * unitSize + margin + scoreAreaHeight + 1;
g.fillOval(a, b, unitSize - 2, unitSize - 2);
}
}
private void drawScore(Graphics g, Dimension gameArea) {
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 36));
FontMetrics metrics = getFontMetrics(g.getFont());
int width = 2 * margin + gameArea.width * unitSize;
String text = "SCORE: " + model.getApplesEaten();
int textWidth = metrics.stringWidth(text);
g.drawString(text, (width - textWidth) / 2, g.getFont().getSize());
}
private void drawSnake(Graphics g) {
// Draw snake
Snake snake = model.getSnake();
List<Point> cells = snake.getCells();
Point cell = cells.get(0);
drawSnakeCell(g, cell, Color.green);
for (int index = 1; index < cells.size(); index++) {
// Color color = new Color(45, 180, 0);
// random color
Color color = new Color(getColorValue(), getColorValue(),
getColorValue());
cell = cells.get(index);
drawSnakeCell(g, cell, color);
}
}
private void drawSnakeCell(Graphics g, Point point, Color color) {
int x = margin + point.x * unitSize;
int y = margin + scoreAreaHeight + point.y * unitSize;
if (point.y >= 0) {
g.setColor(color);
g.fillRect(x, y, unitSize, unitSize);
}
}
private int getColorValue() {
// White has color values of 255
return random.nextInt(64) + 191;
}
private void drawGameOver(Graphics g, Dimension gameArea) {
g.setColor(Color.red);
g.setFont(new Font("Ink Free", Font.BOLD, 72));
FontMetrics metrics = getFontMetrics(g.getFont());
String text = "Game Over";
int textWidth = metrics.stringWidth(text);
g.drawString(text, (getWidth() - textWidth) / 2, getHeight() / 2);
}
}
public class ButtonListener implements ActionListener {
private final int delay;
private final SnakeGame view;
private final SnakeModel model;
private final Timer timer;
public ButtonListener(SnakeGame view, SnakeModel model) {
this.view = view;
this.model = model;
this.delay = 750;
this.timer = new Timer(delay, new TimerListener(view, model));
}
@Override
public void actionPerformed(ActionEvent event) {
JButton button = (JButton) event.getSource();
String text = button.getText();
if (text.equals("Start Game")) {
button.setText("Restart Game");
}
button.setEnabled(false);
model.initialize();
timer.restart();
}
}
public class TimerListener implements ActionListener {
private final SnakeGame view;
private final SnakeModel model;
public TimerListener(SnakeGame view, SnakeModel model) {
this.view = view;
this.model = model;
}
@Override
public void actionPerformed(ActionEvent event) {
moveSnake();
checkApple();
model.checkCollisions();
if (model.isGameOver()) {
Timer timer = (Timer) event.getSource();
timer.stop();
model.setRunning(false);
view.getRestartButton().setEnabled(true);
}
view.repaint();
}
private void moveSnake() {
Snake snake = model.getSnake();
Point head = (Point) snake.getHead().clone();
switch (snake.getDirection()) {
case 'U':
head.y--;
break;
case 'D':
head.y++;
break;
case 'L':
head.x--;
break;
case 'R':
head.x++;
break;
}
snake.removeTail();
snake.addHead(head);
// System.out.println(Arrays.toString(cells.toArray()));
}
private void checkApple() {
Point appleLocation = model.getAppleLocation();
Snake snake = model.getSnake();
Point head = snake.getHead();
Point tail = (Point) snake.getTail().clone();
if (head.x == appleLocation.x && head.y == appleLocation.y) {
model.incrementApplesEaten();
snake.addTail(tail);
model.generateRandomAppleLocation();
}
}
}
public class MovementAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private final char newDirection, oppositeDirection;
private final SnakeModel model;
public MovementAction(SnakeModel model, char newDirection,
char oppositeDirection) {
this.model = model;
this.newDirection = newDirection;
this.oppositeDirection = oppositeDirection;
}
@Override
public void actionPerformed(ActionEvent event) {
if (model.isRunning()) {
Snake snake = model.getSnake();
char direction = snake.getDirection();
if (direction != oppositeDirection && direction != newDirection) {
snake.setDirection(newDirection);
// System.out.println("New direction: " + newDirection);
}
}
}
}
public class SnakeModel {
private boolean isGameOver, isRunning;
private int applesEaten;
private Dimension gameArea;
private Point appleLocation;
private Random random;
private Snake snake;
public SnakeModel() {
this.random = new Random();
this.snake = new Snake();
this.gameArea = new Dimension(24, 24);
}
public void initialize() {
this.isRunning = true;
this.isGameOver = false;
this.snake.initialize();
this.applesEaten = 0;
Point point = generateRandomAppleLocation();
// Make sure first apple isn't under snake
int y = (point.y == 0) ? 1 : point.y;
this.appleLocation = new Point(point.x, y);
}
public void checkCollisions() {
Point head = snake.getHead();
// Check for snake going out of the game area
if (head.x < 0 || head.x > gameArea.width) {
isGameOver = true;
return;
}
if (head.y < 0 || head.y > gameArea.height) {
isGameOver = true;
return;
}
// Check for snake touching itself
List<Point> cells = snake.getCells();
for (int index = 1; index < cells.size(); index++) {
Point cell = cells.get(index);
if (head.x == cell.x && head.y == cell.y) {
isGameOver = true;
return;
}
}
}
public Point generateRandomAppleLocation() {
int x = random.nextInt(gameArea.width);
int y = random.nextInt(gameArea.height);
this.appleLocation = new Point(x, y);
return getAppleLocation();
}
public void incrementApplesEaten() {
this.applesEaten++;
}
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
public boolean isGameOver() {
return isGameOver;
}
public void setGameOver(boolean isGameOver) {
this.isGameOver = isGameOver;
}
public Dimension getGameArea() {
return gameArea;
}
public int getApplesEaten() {
return applesEaten;
}
public Point getAppleLocation() {
return appleLocation;
}
public Snake getSnake() {
return snake;
}
}
public class Snake {
private char direction;
private List<Point> cells;
public Snake() {
this.cells = new ArrayList<>();
initialize();
}
public void initialize() {
this.direction = 'R';
cells.clear();
for (int x = 5; x >= 0; x--) {
cells.add(new Point(x, 0));
}
}
public void addHead(Point head) {
cells.add(0, head);
}
public void addTail(Point tail) {
cells.add(tail);
}
public void removeTail() {
cells.remove(cells.size() - 1);
}
public Point getHead() {
return cells.get(0);
}
public Point getTail() {
return cells.get(cells.size() - 1);
}
public char getDirection() {
return direction;
}
public void setDirection(char direction) {
this.direction = direction;
}
public List<Point> getCells() {
return cells;
}
}
}