Musings about Java and Swing

Retro Snake Game

I have recently seen posts on Stack Overflow about creating the original Atari Snake game, or one of the many offshoots of that game. I decided to create my own version. Here’s a screen shot from the end of one of my recent games.

Retro Snake Game

Here are the rules that I used for the game:

  1. The snake starts with 4 segments.
  2. There is one apple in the grass.
  3. The left, right, up, and down controls are from the perspective of the player looking at the screen, not the snake.
  4. The WASD keys can be used by left-handed players. Right handed players can use either set of arrow keys on the right of the keyboard.
  5. To start the game, left click on the “Start Game” button. The game will wait 3 seconds before starting, to allow you to get your hand in position on the keyboard.
  6. After eating an apple, the snake grows a segment, and another apple appears in a random location.
  7. Each apple is worth the square of the apple count. In other words, the first apple is worth 1 point, the second apple is worth 4 points, the third apple is worth 9 points, and so on.
  8. The game is over when the head of the snake runs off the grass, or into one of the snake’s own segments.
  9. The “Pause Game” is a toggle button that allows you to pause and resume the game, respectively.

I used Java 7 and a Windows Vista computer to code the game. I used the model / view / controller pattern to create the code for the game. There are 4 model classes, 4 view classes, and 3 controller classes. There’s also the animation runnable and the code to start the Java application.

When I use the model / view / controller pattern in a Java Swing application, what I mean is:

  1. The view may read values from the model.
  2. The view may not update values in the model.
  3. The controller will update values in the model.
  4. The controller will repaint the view.

I used an apple image because I thought it looked more realistic than anything I could draw.

Apple Image

So, let’s get into the code. This is the SnakeGame class, which starts the Java Swing application.

package com.ggl.snake.game;

import javax.swing.SwingUtilities;

import com.ggl.snake.game.model.SnakeGameModel;
import com.ggl.snake.game.view.SnakeGameFrame;

public class SnakeGame implements Runnable {

	public static void main(String[] args) {
		SwingUtilities.invokeLater(new SnakeGame());
	}

	@Override
	public void run() {
		new SnakeGameFrame(new SnakeGameModel());
	}

}

This short class does 3 things.

  1. Puts the Swing components on the Event Dispatch thread (EDT) by executing the invokeLater method of SwingUtilities.
  2. Instantiates the SnakeGameModel class.
  3. Instantiates the SnakeGameFrame class.

A version of this class is how I start all of my Swing applications.

Next, let’s look at the model classes. The first model class we will look at is SnakeGameModel.

package com.ggl.snake.game.model;

import java.awt.Dimension;
import java.awt.Image;
import java.io.IOException;
import java.text.NumberFormat;

import javax.imageio.ImageIO;

public class SnakeGameModel {

	private static final int SQUARE_WIDTH = 32;
	private static final int CELL_WIDTH = 25;
	private static final int CELL_HEIGHT = 15;

	private boolean firstTimeSwitch;
	private boolean gameActive;
	private boolean gameOver;

	private int score;

	private long sleepTime = 300L;

	private Apple apple;

	private Image appleImage;

	private Snake snake;

	public SnakeGameModel() {
		this.score = 0;
		this.firstTimeSwitch = false;
		this.gameActive = false;
		this.gameOver = false;
		this.snake = new Snake();
		setAppleImage();
		this.apple = new Apple(appleImage, snake.getRandomNonSnakeLocation());
	}

	public void init() {
		if (firstTimeSwitch) {
			this.score = 0;
			snake.createSnake();
			apple.setPoints(1);
			setAppleLocation();
		} else {
			firstTimeSwitch = true;
		}
	}

	public Snake getSnake() {
		return snake;
	}

	public boolean isGameActive() {
		return gameActive;
	}

	public void setGameActive(boolean gameActive) {
		this.gameActive = gameActive;
	}

	public boolean isGameOver() {
		return gameOver;
	}

	public void setGameOver(boolean gameOver) {
		this.gameOver = gameOver;
		if (gameOver) {
			setGameActive(false);
		}
	}

	public String getFormattedScore() {
		NumberFormat nf = NumberFormat.getInstance();
		return nf.format(score);
	}

	public int getScore() {
		return score;
	}

	public void addScore(int score) {
		this.score += score;
	}

	public void setScore(int score) {
		this.score = score;
	}

	public long getSleepTime() {
		return sleepTime;
	}

	private void setAppleImage() {
		this.appleImage = null;
		String fileName = "/apple-icon.png";
		try {
			this.appleImage = ImageIO.read(getClass().getResource(fileName));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public Image getAppleImage() {
		return appleImage;
	}

	public Apple getApple() {
		return apple;
	}

	public void setApple(Apple apple) {
		this.apple = apple;
	}

	public void setAppleLocation() {
		this.apple.setLocation(snake.getRandomNonSnakeLocation());
	}

	public Dimension getPreferredSize() {
		int width = SQUARE_WIDTH * CELL_WIDTH + 5;
		int height = SQUARE_WIDTH * CELL_HEIGHT + 5;
		return new Dimension(width, height);
	}

	public static int getSquareWidth() {
		return SQUARE_WIDTH;
	}

	public static int getCellWidth() {
		return CELL_WIDTH;
	}

	public static int getCellHeight() {
		return CELL_HEIGHT;
	}

}

There’s nothing unusual in this model class. We keep an instance of the Snake class and an instance of the Apple class. There are some widths and lengths that set the size of the grid panel.

Next, let’s look at the Apple class.

package com.ggl.snake.game.model;

import java.awt.Image;
import java.awt.Point;

public class Apple {

	private final Image appleImage;

	private int points;

	private Point location;

	public Apple(Image appleImage, int x, int y) {
		this.appleImage = appleImage;
		setLocation(x, y);
		this.points = 1;
	}

	public Apple(Image appleImage, Point location) {
		this.appleImage = appleImage;
		this.location = location;
		this.points = 1;
	}

	public Image getAppleImage() {
		return appleImage;
	}

	public Point getLocation() {
		return location;
	}

	public void setLocation(int x, int y) {
		this.location = new Point(x, y);
	}

	public void setLocation(Point location) {
		this.location = location;
	}

	public void setPoints(int points) {
		this.points = points;
	}

	public int appleEaten(Point p) {
		if (p.equals(location)) {
			int score = points * points;
			points++;
			return score;
		} else {
			return 0;
		}
	}

}

Because of the image, we create one instance of this class and reuse it, by setting it to a different random location each time the apple is eaten by the snake.

Next, let’s look at the Snake class.

package com.ggl.snake.game.model;

import java.awt.Color;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class Snake {

	private List<Segment> snakeCells;

	private Point snakeDirection;

	private Random random;

	public Snake() {
		this.random = new Random();
		this.snakeCells = new ArrayList<Segment>();
		createSnake();
	}

	public void createSnake() {
		this.snakeCells.clear();
		this.snakeDirection = generateRandomDirection();

		int x = SnakeGameModel.getCellWidth() / 2;
		int y = SnakeGameModel.getCellHeight() / 2;

		Segment segment = new Segment(Color.RED);
		segment.setLocation(new Point(x, y));
		segment.setDirection(snakeDirection);
		snakeCells.add(segment);

		for (int i = 0; i < 3; i++) {
			x -= snakeDirection.x;
			y -= snakeDirection.y;
			Segment segment2 = new Segment(Color.RED);
			segment2.setLocation(new Point(x, y));
			segment2.setDirection(snakeDirection);
			snakeCells.add(segment2);
		}
	}

	public Point generateRandomDirection() {
		int a = 0;
		int b = 0;

		do {
			a = getRandomDirectionCoordinate();
			b = (a == 0) ? getRandomDirectionCoordinate() : 0;
		} while ((a == 0) && (b == 0));

		return new Point(a, b);
	}

	private int getRandomDirectionCoordinate() {
		return random.nextInt(3) - 1;
	}

	public void updatePosition() {
		Segment segment1 = null;

		for (int i = getSnakeLength() - 2; i >= 0; i--) {
			Segment segment2 = snakeCells.get(i + 1);
			segment1 = snakeCells.get(i);
			Point previousDirection = segment1.getDirection();
			segment2.setDirection(previousDirection);

			Point location = segment2.getLocation();
			location.x += previousDirection.x;
			location.y += previousDirection.y;
			segment2.setLocation(location);
		}

		segment1.setDirection(snakeDirection);

		Point location = segment1.getLocation();
		location.x += snakeDirection.x;
		location.y += snakeDirection.y;
		segment1.setLocation(location);
	}

	public void addSnakeTail() {
		Segment segment = snakeCells.get(getSnakeLength() - 1);

		Point direction = segment.getDirection();
		Point location = segment.getLocation();
		int x = location.x - direction.x;
		int y = location.y - direction.y;

		Segment segment2 = Segment.copy(segment);
		segment2.setLocation(new Point(x, y));

		snakeCells.add(segment2);
	}

	public List<Segment> getSnakeCells() {
		return snakeCells;
	}

	public int getSnakeLength() {
		return snakeCells.size();
	}

	public Point getSnakeHeadLocation() {
		return snakeCells.get(0).getLocation();
	}

	public void setSnakeDirection(Point snakeDirection) {
		this.snakeDirection = snakeDirection;
	}

	public Point getRandomNonSnakeLocation() {
		Point p = new Point(-1, -1);

		do {
			int x = random.nextInt(SnakeGameModel.getCellWidth());
			int y = random.nextInt(SnakeGameModel.getCellHeight());
			p = new Point(x, y);
		} while (isSnakeLocation(p));

		return p;
	}

	public boolean isSnakeLocation(Point p) {
		for (Segment segment : snakeCells) {
			if (segment.getLocation().equals(p)) {
				return true;
			}
		}

		return false;
	}

	public boolean isSnakeDead() {
		int segmentWidth = SnakeGameModel.getCellWidth();
		int segmentHeight = SnakeGameModel.getCellHeight();

		for (Segment segment : snakeCells) {
			Point p = segment.getLocation();
			if ((p.x < 0) || (p.x >= segmentWidth)) {
				return true;
			}
			if ((p.y < 0) || (p.y >= segmentHeight)) {
				return true;
			}
		}

		for (int i = 0; i < (getSnakeLength() - 1); i++) {
			Point s = snakeCells.get(i).getLocation();
			for (int j = (i + 1); j < getSnakeLength(); j++) {
				Point t = snakeCells.get(j).getLocation();
				if (s.equals(t)) {
					return true;
				}
			}
		}

		return false;
	}

}

The methods in this class handle the growth and movement of the snake. Getting the movement right took a bit of work.

This class also provides the random location for the apple. Since I didn’t want the apple to appear under the snake, this seemed like the logical class for the getRandomNonSnakeLocation method.

Next, let’s look at the Segment class. A segment is one piece of a snake’s body.

package com.ggl.snake.game.model;

import java.awt.Color;
import java.awt.Point;

public class Segment {

	private final Color color;

	private Point direction;
	private Point location;

	public Segment(Color color) {
		this.color = color;
	}

	public Point getDirection() {
		return direction;
	}

	public void setDirection(Point direction) {
		this.direction = direction;
	}

	public Point getLocation() {
		return location;
	}

	public void setLocation(Point location) {
		this.location = location;
	}

	public Color getColor() {
		return color;
	}

	public static Segment copy(Segment segment) {
		Segment segment2 = new Segment(segment.getColor());
		Point direction = segment.getDirection();
		segment2.setDirection(new Point(direction.x, direction.y));
		Point location = segment.getLocation();
		segment2.setLocation(new Point(location.x, location.y));

		return segment2;
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		builder.append("Segment [color=");
		builder.append(color);
		builder.append(", direction=");
		builder.append(direction);
		builder.append(", location=");
		builder.append(location);
		builder.append("]");
		return builder.toString();
	}

}

A segment consists of a location and a direction. This is what allows the snake to move in more than one direction at a time.

Next, let’s look at the view classes. The first view class is SnakeGameFrame.

package com.ggl.snake.game.view;

import java.awt.Point;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.NumberFormat;

import javax.swing.BoxLayout;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

import com.ggl.snake.game.controller.ArrowAction;
import com.ggl.snake.game.model.SnakeGameModel;
import com.ggl.snake.game.runnable.GameRunnable;

public class SnakeGameFrame {

	private static final NumberFormat NF = NumberFormat.getInstance();

	private ControlPanel controlPanel;

	private GameRunnable gameRunnable;

	private GridPanel gridPanel;

	private JFrame frame;

	private SnakeGameModel model;

	public SnakeGameFrame(SnakeGameModel model) {
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		frame = new JFrame();
		// frame.setIconImage(getFrameImage());
		frame.setTitle("Retro Snake Game");
		frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent event) {
				exitProcedure();
			}
		});

		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.LINE_AXIS));

		gridPanel = new GridPanel(model);
		mainPanel.add(gridPanel);

		controlPanel = new ControlPanel(this, model);
		mainPanel.add(controlPanel.getPanel());

		frame.add(mainPanel);
		frame.pack();

		setKeyBindings(gridPanel);

		frame.setLocationByPlatform(true);
		frame.getRootPane().setDefaultButton(controlPanel.getStartButton());
		frame.setVisible(true);

		gameRunnable = new GameRunnable(this, model);
		new Thread(gameRunnable).start();
	}

	private void setKeyBindings(JPanel gridPanel) {
		InputMap inputMap = gridPanel
				.getInputMap(JPanel.WHEN_IN_FOCUSED_WINDOW);
		inputMap.put(KeyStroke.getKeyStroke("W"), "up arrow");
		inputMap.put(KeyStroke.getKeyStroke("S"), "down arrow");
		inputMap.put(KeyStroke.getKeyStroke("A"), "left arrow");
		inputMap.put(KeyStroke.getKeyStroke("D"), "right arrow");

		inputMap.put(KeyStroke.getKeyStroke("UP"), "up arrow");
		inputMap.put(KeyStroke.getKeyStroke("DOWN"), "down arrow");
		inputMap.put(KeyStroke.getKeyStroke("LEFT"), "left arrow");
		inputMap.put(KeyStroke.getKeyStroke("RIGHT"), "right arrow");

		inputMap = gridPanel.getInputMap(JPanel.WHEN_FOCUSED);
		inputMap.put(KeyStroke.getKeyStroke("UP"), "up arrow");
		inputMap.put(KeyStroke.getKeyStroke("DOWN"), "down arrow");
		inputMap.put(KeyStroke.getKeyStroke("LEFT"), "left arrow");
		inputMap.put(KeyStroke.getKeyStroke("RIGHT"), "right arrow");

		gridPanel.getActionMap().put("up arrow",
				new ArrowAction(model, new Point(0, -1)));
		gridPanel.getActionMap().put("down arrow",
				new ArrowAction(model, new Point(0, 1)));
		gridPanel.getActionMap().put("left arrow",
				new ArrowAction(model, new Point(-1, 0)));
		gridPanel.getActionMap().put("right arrow",
				new ArrowAction(model, new Point(1, 0)));
	}

	private void exitProcedure() {
		gameRunnable.setRunning(false);
		frame.dispose();
		System.exit(0);
	}

	public void repaintGridPanel() {
		gridPanel.repaint();
	}

	public void setScoreText() {
		controlPanel.setScoreText(NF.format(model.getScore()));
	}

	public void setPauseButton() {
		controlPanel.setPauseButton(model.isGameActive());
	}
}

The createPartControl method is pretty much the same for all of my Java Swing applications. The only difference is the title, and the panel classes that are instantiated.

The setKeyBindings method allows me to set the WASD keys and the arrow keys to the same ArrowAction class. The only difference is the direction Point that I pass to the ArrowAction class.

I use a Point to specify the direction because all I have to do to move a snake segment is add the direction x and direction y to the location x and location y. This way, I don’t have to code a lot of if statements.

I hava a couple of convenience methods at the bottom of the class. The controllers use these methods so that the controllers don’t have to know about the view.

Next, let’s look at the ControlPanel class.

package com.ggl.snake.game.view;

import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;

import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JToggleButton;

import com.ggl.snake.game.controller.PauseButtonActionListener;
import com.ggl.snake.game.controller.StartButtonActionListener;
import com.ggl.snake.game.model.SnakeGameModel;

public class ControlPanel {

	private static final Insets normalInsets = new Insets(10, 10, 0, 10);

	private JButton startButton;

	private JPanel panel;

	private JTextField scoreField;

	private JToggleButton pauseButton;

	private SnakeGameFrame frame;

	private SnakeGameModel model;

	public ControlPanel(SnakeGameFrame frame, SnakeGameModel model) {
		this.frame = frame;
		this.model = model;
		createPartControl();
	}

	private void createPartControl() {
		panel = new JPanel();

		JPanel innerPanel = new JPanel();
		innerPanel.setLayout(new GridBagLayout());

		int gridy = 0;

		JLabel scoreLabel = new JLabel("Score");
		Font labelFont = innerPanel.getFont().deriveFont(36.0F);
		scoreLabel.setFont(labelFont);
		scoreLabel.setHorizontalAlignment(JLabel.CENTER);
		addComponent(innerPanel, scoreLabel, 0, gridy++, 1, 1, normalInsets,
				GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);

		scoreField = new JTextField(6);
		scoreField.setEditable(false);
		Font textFont = innerPanel.getFont().deriveFont(48.0F);
		scoreField.setFont(textFont);
		scoreField.setHorizontalAlignment(JTextField.CENTER);
		scoreField.setText(model.getFormattedScore());
		addComponent(innerPanel, scoreField, 0, gridy++, 1, 1, normalInsets,
				GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);

		startButton = new JButton("Start Game");
		startButton.addActionListener(new StartButtonActionListener(frame,
				model));
		addComponent(innerPanel, startButton, 0, gridy++, 1, 1, normalInsets,
				GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);

		pauseButton = new JToggleButton("Pause Game");
		pauseButton.addActionListener(new PauseButtonActionListener(model));
		addComponent(innerPanel, pauseButton, 0, gridy++, 1, 1, normalInsets,
				GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL);

		panel.add(innerPanel);
	}

	private void addComponent(Container container, Component component,
			int gridx, int gridy, int gridwidth, int gridheight, Insets insets,
			int anchor, int fill) {
		GridBagConstraints gbc = new GridBagConstraints(gridx, gridy,
				gridwidth, gridheight, 1.0D, 1.0D, anchor, fill, insets, 0, 0);
		container.add(component, gbc);
	}

	public JToggleButton getPauseButton() {
		return pauseButton;
	}

	public void setPauseButton(boolean selected) {
		this.pauseButton.setSelected(selected);
	}

	public JButton getStartButton() {
		return startButton;
	}

	public JPanel getPanel() {
		return panel;
	}

	public void setScoreText(String scoreText) {
		this.scoreField.setText(scoreText);
	}

}

This is a pretty standard class that uses a JPanel with a GridBagLayout to hold Swing components.

I create a separate GridBagConstraints for each Swing component. I don’t like to remember defaults, and I can adjust the GridBagConstraints for each Swing component without worrying about side effects.

Making the grid y (gridy) a field allows me to insert or remove Swing components from the layout without having to retype a bunch of y constants. Usually, I have no more than 3 grid x components, so I don’t worry about making grid x a field.

Next, let’s look at the GridPanel class.

package com.ggl.snake.game.view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.border.Border;

import com.ggl.snake.game.model.Apple;
import com.ggl.snake.game.model.Segment;
import com.ggl.snake.game.model.Snake;
import com.ggl.snake.game.model.SnakeGameModel;

public class GridPanel extends JPanel {

	private static final long serialVersionUID = 3259516267781813618L;

	private GameOverImage gameOverImage;

	private SnakeGameModel model;

	public GridPanel(SnakeGameModel model) {
		this.model = model;
		this.gameOverImage = new GameOverImage(model);
		this.gameOverImage.run();

		Border border = BorderFactory.createLineBorder(Color.BLACK, 5);
		this.setBorder(border);
		this.setPreferredSize(model.getPreferredSize());
	}

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

		int cellWidth = SnakeGameModel.getCellWidth();
		int cellHeight = SnakeGameModel.getCellHeight();
		int squareWidth = SnakeGameModel.getSquareWidth();

		int x = 0;
		int y = 0;

		for (int w = 0; w < cellWidth; w++) {
			for (int h = 0; h < cellHeight; h++) {
				g2d.setColor(Color.GREEN);
				g2d.fillRect(x, y, squareWidth, squareWidth);
				y += squareWidth;
			}
			x += SnakeGameModel.getSquareWidth();
			y = 0;
		}

		drawSnake(g2d, squareWidth);
		drawApple(g2d, squareWidth);

		if (model.isGameOver()) {
			g2d.drawImage(gameOverImage.getImage(), 0, 0, this);
		}
	}

	private void drawSnake(Graphics2D g2d, int squareWidth) {
		int x;
		int y;
		Snake snake = model.getSnake();
		List<Segment> segments = snake.getSnakeCells();
		int snakeWidth = squareWidth - 4;

		for (int i = 0; i < segments.size(); i++) {
			Segment segment = segments.get(i);
			Point p = segment.getLocation();
			x = p.x * squareWidth + 2;
			y = p.y * squareWidth + 2;

			g2d.setColor(segment.getColor());
			g2d.fillRoundRect(x, y, snakeWidth, snakeWidth, snakeWidth / 2,
					snakeWidth / 2);

			if (i == 0) {
				x += (squareWidth / 2) - 1;
				y += (squareWidth / 2) - 1;

				g2d.setColor(Color.BLACK);
				g2d.setStroke(new BasicStroke(3F));
				drawCircle(g2d, x, y, (squareWidth / 2) - 6);
			}
		}
	}

	private void drawCircle(Graphics2D g2d, int x, int y, int radius) {
		g2d.drawOval(x - radius, y - radius, radius + radius, radius + radius);
	}

	private void drawApple(Graphics2D g2d, int squareWidth) {
		Apple apple = model.getApple();
		Image image = apple.getAppleImage();
		Point p = apple.getLocation();

		int x = p.x * squareWidth + 1;
		int y = p.y * squareWidth + 1;
		g2d.drawImage(image, x, y, this);
	}
}

We extend a JPanel for this class because we want to override the paintComponent method. We paint the grass, the snake, and the apple, in that order.

The last view class we’ll look at is the GameOverImage class.

package com.ggl.snake.game.view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import com.ggl.snake.game.model.SnakeGameModel;

public class GameOverImage implements Runnable {

	private BufferedImage image;

	private SnakeGameModel model;

	public GameOverImage(SnakeGameModel model) {
		this.model = model;
	}

	@Override
	public void run() {
		String s = "Game Over";
		Dimension d = model.getPreferredSize();
		image = new BufferedImage(d.width, d.height,
				BufferedImage.TYPE_INT_ARGB);
		Graphics2D g = image.createGraphics();

		// Black with a 15% transparency
		Color c = new Color(0.0f, 0.0f, 0.0f, 0.15f);
		g.setColor(c);
		g.fillRect(0, 0, d.width, d.height);

		g.setColor(Color.BLUE);
		Font font = g.getFont();
		Font largeFont = font.deriveFont(72.0F);
		FontRenderContext frc = new FontRenderContext(null, true, true);
		Rectangle2D r = largeFont.getStringBounds(s, frc);
		int rWidth = (int) Math.round(r.getWidth());
		int rHeight = (int) Math.round(r.getHeight());
		int rX = (int) Math.round(r.getX());
		int rY = (int) Math.round(r.getY());

		int x = (d.width / 2) - (rWidth / 2) - rX;
		int y = (d.height / 2) - (rHeight / 2) - rY;

		g.setFont(largeFont);
		g.drawString(s, x, y);

		g.dispose();
	}

	public BufferedImage getImage() {
		return image;
	}

}

We create a translucent background by specifying a black color with a 15% transparency. We center the “Game Over” text using the font render context.

Let’s look at the controller classes. The first controller class is the StartButtonActionListener class.

package com.ggl.snake.game.controller;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.SwingUtilities;

import com.ggl.snake.game.model.SnakeGameModel;
import com.ggl.snake.game.view.SnakeGameFrame;

public class StartButtonActionListener implements ActionListener {

	private SnakeGameFrame frame;

	private SnakeGameModel model;

	public StartButtonActionListener(SnakeGameFrame frame, SnakeGameModel model) {
		this.frame = frame;
		this.model = model;
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		new Thread(new WaitToStartRunnable(frame, model)).start();
	}

	private class WaitToStartRunnable implements Runnable {

		private long sleepAmount = 3000L;

		private SnakeGameFrame frame;

		private SnakeGameModel model;

		public WaitToStartRunnable(SnakeGameFrame frame, SnakeGameModel model) {
			this.frame = frame;
			this.model = model;
		}

		@Override
		public void run() {
			model.setGameOver(false);
			model.setGameActive(false);
			model.init();
			repaint();
			sleep();
			model.setGameActive(true);
		}

		public void repaint() {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					frame.setScoreText();
					frame.repaintGridPanel();
				}
			});
		}

		public void sleep() {
			try {
				Thread.sleep(sleepAmount);
			} catch (InterruptedException e) {
				assert false;
			}
		}

	}

}

We set up the game, sleep for 3 seconds, and start the game. All of the methods used are defined wither in the model or the view. They are executed as a part of the controller process.

Next, let’s look at the PauseButtonActionListener class.

package com.ggl.snake.game.controller;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JToggleButton;

import com.ggl.snake.game.model.SnakeGameModel;

public class PauseButtonActionListener implements ActionListener {

	private SnakeGameModel model;

	public PauseButtonActionListener(SnakeGameModel model) {
		this.model = model;
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		JToggleButton button = (JToggleButton) event.getSource();
		if (button.isSelected()) {
			model.setGameActive(false);
		} else {
			model.setGameActive(true);
		}
	}

}

This class toggles the game active state in the model.

Next, let’s look at the ArrowAction class. This is the class executed when one of the arrow or WASD keys are pressed.

package com.ggl.snake.game.controller;

import java.awt.Point;
import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;

import com.ggl.snake.game.model.SnakeGameModel;

public class ArrowAction extends AbstractAction {

	private static final long serialVersionUID = 2023424583090620226L;

	private Point direction;

	private SnakeGameModel model;

	public ArrowAction(SnakeGameModel model, Point direction) {
		this.model = model;
		this.direction = direction;
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		if (model.isGameActive()) {
			model.getSnake().setSnakeDirection(direction);
		}
	}

}

This class sets the snake direction. The direction is based on the user looking at the screen, rather than the snake.

Finally, let’s look at the GameRunnable class.

package com.ggl.snake.game.runnable;

import javax.swing.SwingUtilities;

import com.ggl.snake.game.model.Apple;
import com.ggl.snake.game.model.Snake;
import com.ggl.snake.game.model.SnakeGameModel;
import com.ggl.snake.game.view.SnakeGameFrame;

public class GameRunnable implements Runnable {

	private volatile boolean running;

	private SnakeGameFrame frame;

	private SnakeGameModel model;

	public GameRunnable(SnakeGameFrame frame, SnakeGameModel model) {
		this.frame = frame;
		this.model = model;
		this.running = true;
	}

	@Override
	public void run() {
		while (running) {
			long startTime = System.currentTimeMillis();

			if (model.isGameActive()) {
				Snake snake = model.getSnake();
				Apple apple = model.getApple();
				snake.updatePosition();
				if (snake.isSnakeDead()) {
					model.setGameOver(true);
					model.setGameActive(false);
				}
				int points = apple.appleEaten(snake.getSnakeHeadLocation());
				if (points > 0) {
					model.addScore(points);
					setScoreText();
					apple.setLocation(snake.getRandomNonSnakeLocation());
					snake.addSnakeTail();
				}
				repaint();
			}

			long elapsedTime = System.currentTimeMillis() - startTime;
			long sleepTime = Math
					.max((model.getSleepTime() - elapsedTime), 10L);
			sleep(sleepTime);
		}

	}

	private void sleep(long sleepTime) {
		try {
			Thread.sleep(sleepTime);
		} catch (InterruptedException e) {

		}
	}

	private void setScoreText() {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				frame.setScoreText();
				;
			}
		});
	}

	private void repaint() {
		SwingUtilities.invokeLater(new Runnable() {
			@Override
			public void run() {
				frame.repaintGridPanel();
			}
		});
	}

	public synchronized void setRunning(boolean running) {
		this.running = running;
	}

}

This is the class that animates the snake movement. I subtract out the update time from the sleep time to try and keep the animation running at a smooth 300 milliseconds, or 3.3 frames per second. Since the snake is the only object on the screen moving, 3 frames per second is plenty fast enough.

Since this code is not running in the Event Dispatch tread, both calls to the JFrame are wrapped in a SwingUtilities invokeLater method. This ensures that the GUI updates happen on the Event Dispatch thread.

Thanks for reading this article. I hope you learned something about Swing animation.

Post a Comment

Your email is kept private. Required fields are marked *