4
\$\begingroup\$

I completed two intro computer science courses on the basics of Java programming and data structures in university, and have continued to study programming on my own over the past few months.

This is my first attempt at programming a playable game. It was written in Java 8 using the MVC design pattern, and contains difficulty settings, persistent stats, and sounds.

The most challenging part of writing this program involved managing communication between classes while adhering to the MVC design pattern. For example, managing key presses was accomplished by writing a ViewListener class that forwards key presses and game state information from the view to the controller. Game stats persist by storing data in a text file.

Github: https://github.com/jbberinger/Snake

Playable Jar: https://mega.nz/#!G18gDIwK!d07ObgjcKFs1skKMUVjhtI45Y1sRUJS63HNPRU91NIc


Controller

Controller.java

package controller;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import javax.swing.Timer;
import model.Model;
import util.Direction;

/**
 * Main controller as part of MVC design pattern.
 *
 * @author Justin Beringer
 */
public class Controller {

    private static int TICKS_PER_SECOND;
    private static boolean buttonPressed = false;
    private static boolean isGameOver = false;
    private static boolean isNewGame = true;
    private static boolean isPaused = false;
    private boolean isChoosingDifficulty;

    private static final Model model = new Model();
    private static ActionListener taskPerformer;
    private static Direction direction;
    private static Timer timer;

    public static void main(String[] args) {
        taskPerformer = (ActionEvent e) -> {
            model.moveSnake();
            buttonPressed = false;
        };
        timer = new Timer(TICKS_PER_SECOND, taskPerformer);
        timer.setInitialDelay(0);
    }

    /**
     * Maps key presses.
     */
    public void directionInput(KeyEvent key) {
        int keyCode = key.getKeyCode();

        if (isNewGame) {
            isNewGame = false;
            model.chooseDifficulty();
            return;
        }

        if (isChoosingDifficulty) {
            switch (keyCode) {
                case KeyEvent.VK_1:
                    setDifficulty(0);
                    break;
                case KeyEvent.VK_2:
                    setDifficulty(1);
                    break;
                case KeyEvent.VK_3:
                    setDifficulty(2);
                    break;
                default:
                    return;
            }
            isChoosingDifficulty = false;
            model.continueGame();
            timer.start();
            return;
        }

        if (isGameOver) {
            if (keyCode == KeyEvent.VK_Y) {
                model.chooseDifficulty();
            } else if (keyCode == KeyEvent.VK_N) {
                model.quit();
            } else {
                return;
            }
            isGameOver = false;
            return;

        }

        switch (key.getKeyCode()) {
            case KeyEvent.VK_UP:
                if (direction != Direction.DOWN && !buttonPressed) {
                    direction = Direction.UP;
                }
                buttonPressed = true;
                break;
            case KeyEvent.VK_DOWN:
                if (direction != Direction.UP && !buttonPressed) {
                    direction = Direction.DOWN;
                }
                buttonPressed = true;
                break;
            case KeyEvent.VK_LEFT:
                if (direction != Direction.RIGHT && !buttonPressed) {
                    direction = Direction.LEFT;
                }
                buttonPressed = true;
                break;
            case KeyEvent.VK_RIGHT:
                if (direction != Direction.LEFT && !buttonPressed) {
                    direction = Direction.RIGHT;
                }
                buttonPressed = true;
                break;
            case KeyEvent.VK_P:
                if (!isPaused) {
                    isPaused = true;
                    model.stopMusic();
                    timer.stop();
                } else {
                    isPaused = false;
                    model.playMusic();
                    timer.start();
                }
                break;
            default:
                break;
        }

        if (direction != null) {
            model.setDirection(direction);
        }
    }

    public void setGameOver(boolean isGameOver) {
        timer.stop();
        Controller.isGameOver = isGameOver;
    }

    public void setNewGame(boolean isNewGame) {
        direction = Direction.UP;
        Controller.isNewGame = isNewGame;
    }

    public void setChoosingDifficulty(boolean isChoosingDifficulty) {
        timer.stop();
        this.isChoosingDifficulty = isChoosingDifficulty;
    }

    public void setDifficulty(int difficulty) {
        switch (difficulty) {
            case 0:
                TICKS_PER_SECOND = 1000 / 8;
                timer = new Timer(TICKS_PER_SECOND, taskPerformer);
                break;
            case 1:
                TICKS_PER_SECOND = 1000 / 15;
                timer = new Timer(TICKS_PER_SECOND, taskPerformer);
                break;
            case 2:
                TICKS_PER_SECOND = 1000 / 20;
                timer = new Timer(TICKS_PER_SECOND, taskPerformer);
                break;
            default:
                break;
        }
        model.setDifficulty(difficulty);
    }

}

Model

Model.java

package model;

import java.awt.Point;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import util.Direction;
import view.View;

/**
 * Main model class as part of MVC design pattern.
 *
 * @author Justin Beringer
 */
public final class Model {

    private final int WIDTH = 600;
    private final int HEIGHT = 600;
    private final int SCALE = 15;
    private final int GROWTH_SPURT = 8;

    private final int MAX_INDEX_X = WIDTH / SCALE;
    private final int MAX_INDEX_Y = HEIGHT / SCALE;

    private int squaresToGrow;
    private int applesEaten = 0;
    private Direction direction = Direction.UP;
    private int difficulty;

    private final View view;
    private final Point apple = new Point();
    private final Random random = new Random();
    private final Deque<Point> snakeBody = new ArrayDeque<>();
    private final Set<Point> occupiedPositions = new LinkedHashSet();
    private Clip gameOverSound, eatAppleSound, gameMusicSound;

    private final String[] difficulties = {"n00b", "quick", "crazy"};

    private final String HIGH_SCORE_N00B = "High Score " + difficulties[0] + ": ";
    private final String HIGH_SCORE_SPEED = "High Score " + difficulties[1] + ": ";
    private final String HIGH_SCORE_CRAZY = "High Score " + difficulties[2] + ": ";
    private final String GAMES_PLAYED = "Games Played: ";
    private final String APPLES_EATEN = "Apples Eaten: ";

    private final String[] dataID = {HIGH_SCORE_N00B, HIGH_SCORE_SPEED,
                                     HIGH_SCORE_CRAZY, GAMES_PLAYED, APPLES_EATEN};

    private final int[] data = new int[dataID.length];

    private final int TOTAL_GAMES_PLAYED_LOC = 3;
    private final int TOTAL_APPLES_EATEN_LOC = 4;

    public Model() {
        loadData();
        occupiedPositions.add(apple);
        view = new View(WIDTH, HEIGHT, SCALE, snakeBody, apple);
        initSounds();
    }

    /**
     * Generates snake at center of grid moving in upward direction. Consider
     * initializing the snake position and direction randomly.
     */
    private void generateSnakeAtCenter() {
        int x = WIDTH / 2;
        int y = HEIGHT / 2;
        snakeBody.add(new Point(x, y));
        occupiedPositions.add(snakeBody.getFirst());
        squaresToGrow += GROWTH_SPURT;
    }

    /**
     * Generates an apple position randomly and avoids collisions.
     *
     * Originally tried to use HashSet.contains() to check for collisions, but
     * it wasn't catching collisions.
     */
    private void generateApple() {

        int x = 0;
        int y = 0;
        boolean spaceIsOccupied;
        do {
            spaceIsOccupied = false;
            x = random.nextInt((int) MAX_INDEX_X) * SCALE;
            y = random.nextInt((int) MAX_INDEX_Y) * SCALE;
            for (Point point : occupiedPositions) {
                if (point.getX() == x && point.getY() == y) {
                    System.out.println("wtf");
                    spaceIsOccupied = true;
                }
            }
        } while (spaceIsOccupied);
        apple.setLocation(x, y);
    }

    /**
     * Moves snake by moving tail position one grid square in front of the head
     * in the current direction. The translated tail is then dequeued before
     * being queued as the new head. If an apple is eaten, the snake is extended
     * by not dequeuing the tail.
     */
    public void moveSnake() {

        int nextHeadX = (int) snakeBody.getFirst().getX();
        int nextHeadY = (int) snakeBody.getFirst().getY();

        switch (direction) {
            case UP:
                nextHeadY -= SCALE;
                break;
            case DOWN:
                nextHeadY += SCALE;
                break;
            case LEFT:
                nextHeadX -= SCALE;
                break;
            case RIGHT:
                nextHeadX += SCALE;
                break;
            default:
                break;
        }

        if (collided(nextHeadX, nextHeadY)) {
            stopMusic();
            playGameOverSound();
            data[difficulty] = Math.max(applesEaten, data[difficulty]);
            view.update(difficulties[difficulty], applesEaten, data[difficulty]);
            direction = Direction.UP; // consider making this random
            view.gameOver();
        }

        snakeBody.getLast().setLocation(nextHeadX, nextHeadY);
        if (ateApple()) {
            playEatAppleSound();
            snakeBody.addFirst(new Point(nextHeadX, nextHeadY));
            occupiedPositions.add(snakeBody.getFirst());
            generateApple();
            applesEaten++;
            data[TOTAL_APPLES_EATEN_LOC]++;
            squaresToGrow += GROWTH_SPURT - 1;
        } else if (squaresToGrow > 0) {
            snakeBody.addFirst(new Point(nextHeadX, nextHeadY));
            occupiedPositions.add(snakeBody.getFirst());
            squaresToGrow--;
        } else {
            snakeBody.addFirst(snakeBody.removeLast());
        }

        view.updateView(snakeBody, apple, difficulties[difficulty], data[difficulty], applesEaten); 

    }

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

    private boolean ateApple() {
        return snakeBody.getFirst().equals(apple);
    }

    /**
     * Checks for snake collision with self and edges
     */
    private boolean collided(int nextHeadX, int nextHeadY) {
        boolean snakeCollision = snakeBody.contains(new Point(nextHeadX, nextHeadY));
        boolean leftEdgeCollision = nextHeadX < 0;
        boolean rightEdgeCollision = nextHeadX >= WIDTH;
        boolean topEdgeCollision = nextHeadY < 0;
        boolean bottomEdgeCollision = nextHeadY >= HEIGHT;
        return snakeCollision
                || leftEdgeCollision
                || rightEdgeCollision
                || topEdgeCollision
                || bottomEdgeCollision;
    }

    public void newGame() {
        view.newGame();
    }

    public void continueGame() {
        clearModel();
        generateSnakeAtCenter();
        generateApple();
        playMusic();
        view.updateView(snakeBody, apple, difficulties[difficulty], data[difficulty], applesEaten); 
        view.continueGame();
        data[TOTAL_GAMES_PLAYED_LOC]++;
        saveData();
    }

    public void chooseDifficulty() {
        view.chooseDifficulty();
    }

    public void setDifficulty(int difficulty) {
        this.difficulty = difficulty;
    }

    public void clearModel() {
        occupiedPositions.clear();
        snakeBody.clear();
        apple.setLocation(0, 0);
        applesEaten = 0;
    }

    private void initSounds() {
        try {
            URL url = this.getClass().getClassLoader().getResource("sound/gameOver.wav");
            AudioInputStream audioIn = AudioSystem.getAudioInputStream(url);
            gameOverSound = AudioSystem.getClip();
            gameOverSound.open(audioIn);

            url = this.getClass().getClassLoader().getResource("sound/eatApple.wav");
            audioIn = AudioSystem.getAudioInputStream(url);
            eatAppleSound = AudioSystem.getClip();
            eatAppleSound.open(audioIn);

            url = this.getClass().getClassLoader().getResource("sound/gameMusic.wav");
            audioIn = AudioSystem.getAudioInputStream(url);
            gameMusicSound = AudioSystem.getClip();
            gameMusicSound.open(audioIn);
        } catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) {
        }
    }

    public void pauseMusic() {
        if (gameMusicSound.isRunning()) {
            gameMusicSound.stop();
        } else {
            gameMusicSound.loop(100);
            gameMusicSound.start();
        }
    }

    public void playMusic() {
        gameMusicSound.setMicrosecondPosition(0);
        gameMusicSound.loop(100);
        gameMusicSound.start();
    }

    public void stopMusic() {
        gameMusicSound.stop();
    }

    public void playGameOverSound() {
        gameOverSound.setMicrosecondPosition(0);
        gameOverSound.start();
    }

    public void playEatAppleSound() {
        eatAppleSound.setMicrosecondPosition(0);
        eatAppleSound.start();
    }

    /**
     * Loads game stats from text file. If no text file exists, a new one is
     * created.
     *
     */
    public void loadData() {
        Path path = Paths.get("./SnakeData.txt");
        String line;
        int dataIndex = 0;
        try (InputStream in = Files.newInputStream(path)) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            while ((line = reader.readLine()) != null) {
                data[dataIndex] = Integer.parseInt(line.replaceAll(dataID[dataIndex], ""));
                dataIndex++;
            }
            reader.close();
        } catch (Exception ex) {
            Logger.getLogger(Model.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Saves game stats to text file.
     */

    public void saveData() {
        for (int datum : data) {
            System.out.println(datum);
        }
        Path path = Paths.get("./SnakeData.txt");
        try (OutputStream out = Files.newOutputStream(path)) {
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
            for (int i = 0; i < data.length; i++) {
                writer.write(dataID[i] + Integer.toString(data[i]));
                writer.newLine();
            }
            writer.close();
        } catch (IOException ex) {
            Logger.getLogger(Model.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    public void quit() {
        saveData();
        System.exit(0);
    }

}

View

View.java

package view;

import java.awt.Color;
import java.awt.Image;
import java.awt.Point;
import java.net.URL;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * Main view as part of MVC design pattern.
 *
 * @author Justin Beringer
 */
public final class View {

    private final GamePanel gamePanel;
    private final GameHeaderPanel gameHeaderPanel;
    private final ViewListener viewListener = new ViewListener();
    private final GameOverPanel gameOverPanel;
    private final NewGamePanel newGamePanel;
    private final DifficultyPanel difficultyPanel;
    private JFrame frame;
    private JPanel content;
    private final int scale;
    private final List<Image> icons = new ArrayList<>();

    public View(int width, int height, int scale, Deque<Point> snakeBody, Point apple) {
        gamePanel = new GamePanel(width, height, scale, snakeBody, apple);
        newGamePanel = new NewGamePanel(width, height, scale);
        gameOverPanel = new GameOverPanel(width, height, scale);
        difficultyPanel = new DifficultyPanel(width, height, scale);
        gameHeaderPanel = new GameHeaderPanel(width, height, scale);
        this.scale = scale;
        initIcons();
        initGridView();
    }

    /**
     * Initializes GUI.
     */
    private void initGridView() {
        frame = new JFrame("Snake");
        frame.addKeyListener(viewListener);

        content = new JPanel();
        content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
        content.setBorder(BorderFactory.createEmptyBorder(scale, scale, scale, scale));
        content.setBackground(Color.black);
        content.add(gameHeaderPanel);
        content.add(gamePanel);
        frame.add(content);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.pack();
        newGame();
        frame.setIconImages(icons);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public void updateView(Deque<Point> snakeBody, Point apple, String difficulty, int highScore, int applesEaten) {
        gamePanel.setSnakeBody(snakeBody, apple);
        gameHeaderPanel.update(difficulty, highScore, applesEaten);
        gameHeaderPanel.repaint();
        gamePanel.repaint();
    }

    public void gameOver() {
        System.out.println("GAME OVER");
        viewListener.setGameOver(true);
        content.removeAll();
        content.add(gameOverPanel);
        content.validate();
        content.repaint();
    }

    public void newGame() {
        System.out.println("NEW GAME");
        viewListener.setNewGame(true);
        content.removeAll();
        content.add(newGamePanel);
        content.validate();
        content.repaint();
    }

    public void chooseDifficulty() {
        System.out.println("CHOOSE DIFFICULTY");
        viewListener.setChoosingDifficulty(true);
        content.removeAll();
        content.add(difficultyPanel);
        content.validate();
        content.repaint();
    }

    public void continueGame() {
        System.out.println("CONTINUE GAME");
        viewListener.setGameOver(false);
        viewListener.setNewGame(false);
        viewListener.setChoosingDifficulty(false);
        content.removeAll();
        content.add(gameHeaderPanel);
        content.add(gamePanel);
        content.validate();
        content.repaint();
    }

    public void update(String difficulty, int applesEaten, int highScore) {
        gameOverPanel.update(difficulty, applesEaten, highScore);
    }

    private void initIcons() {
        try {
            URL url = this.getClass().getClassLoader().getResource("icon/Icon_128.png");
            icons.add((new ImageIcon(url)).getImage());
        } catch (Exception e) {
        }
    }

}

GamePanel.java

package view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.util.Deque;
import java.util.Random;
import javax.swing.JPanel;

/**
 * Displayed while the game is being played. Responsible for the look and feel
 * of the game, as well as drawing the snake and apple. Methods exist for a grid
 * layout and a dot layout.
 *
 * @author Justin Beringer
 */
public class GamePanel extends JPanel {

    private Deque<Point> snakeBody;
    private final Point apple;
    private final int width, height, scale;
    private int r = 0;
    private int g = 255;
    private int b = 0;
    private final Random random = new Random();
    private Color snakeColor = new Color(0, 255, 0);
    private Graphics2D g2d;

    GamePanel(int width, int height, int scale, Deque<Point> snakeBody, Point apple) {
        this.snakeBody = snakeBody;
        this.width = width;
        this.height = height;
        this.scale = scale;
        this.apple = apple;
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(width, height);
    }

    @Override
    public Color getBackground() {
        return Color.black;
    }

    @Override
    public boolean isOpaque() {
        return true;
    }

    /**
     * Ensures GUI is painted when the window is moved or hidden.
     */
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        paintDots();
        paintApple();
        paintSnake();

    }

    public void setSnakeBody(Deque<Point> snakeBody, Point apple) {
        this.snakeBody = snakeBody;
    }

    public void paintApple() {
        g2d.setStroke(new BasicStroke(1.5f));
        int xPos = (int) apple.getX();
        int yPos = (int) apple.getY();
        g2d.setColor(Color.green);
        g2d.drawLine(xPos + scale / 2, yPos, xPos + scale / 2, yPos - 1);
        g2d.drawLine(xPos + scale / 2, yPos, xPos + scale - 1, yPos - 2);
        g2d.setColor(Color.red);
        g2d.fillOval(xPos + 2, yPos + 2, scale - 4, scale - 3);
        g2d.fillOval(xPos + 4, yPos + 2, scale - 4, scale - 3);
    }

    public void paintSnake() {
        int xPos, yPos;
        for (Point position : snakeBody) {
            g2d.setColor(snakeColor);
            xPos = (int) position.getX();
            yPos = (int) position.getY();
            g2d.drawRoundRect(xPos + 2, yPos + 2, scale - 4, scale - 4, 2, 2);
        }
        System.out.println(snakeColor);
        r = 0;
        g = 255;
        b = 0;
    }

    public void paintDots() {
        g2d.setStroke(new BasicStroke(0.25f));
        g2d.setColor(Color.gray);
        for (int i = 0; i <= width / scale; i++) {
            for (int j = 0; j <= height / scale; j++) {
                if (i * scale == width) {
                    g2d.fillRect(i * scale - 1, j * scale, 1, 1);
                } else if (j * scale == height) {
                    g2d.fillRect(i * scale, j * scale - 1, 1, 1);
                } else {
                    g2d.fillRect(i * scale, j * scale, 1, 1);
                }
            }
        }
        g2d.fillRect(width - 1, height - 1, 1, 1);

    }

    public void paintGrid() {
        g2d.setStroke(new BasicStroke(0.25f));
        g2d.setColor(Color.gray);
        for (int i = 1; i < width / scale; i++) {
            g2d.drawLine(i * scale, 0, i * scale, height);
        }
        for (int i = 1; i < height / scale; i++) {
            g2d.drawLine(0, i * scale, width, i * scale);

        }
    }

}

GameHeaderPanel.java

package view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import javax.swing.JPanel;

/**
 * Displays live game stats.
 *
 * @author Justin Beringer
 */
public class GameHeaderPanel extends JPanel {

    private final int width, height, scale;
    String difficulty, applesEaten, highScore;
    Graphics2D g2d;

    GameHeaderPanel(int width, int height, int scale) {
        this.width = width;
        this.height = scale * 2;
        this.scale = scale;
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(width, height);
    }

    @Override
    public Color getBackground() {
        return Color.black;
    }

    /**
     * Ensures GUI is painted when the window is moved or hidden.
     */
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        paintHeader();

    }

    public void paintHeader() {
        g2d.setColor(Color.white);
        Font font = new Font("Monospaced", Font.PLAIN, scale);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc,
                "Difficulty: " + difficulty + "    High Score: " + highScore
                + "    Apples Eaten: " + applesEaten);
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height / 2 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void update(String difficulty, int highScore, int applesEaten) {
        this.difficulty = difficulty;
        this.highScore = String.valueOf(highScore);
        this.applesEaten = String.valueOf(applesEaten);
    }

}

NewGamePanel.java

package view;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import javax.swing.JPanel;

/**
 * Drawn when game is first opened.
 *
 * @author Justin Beringer
 */
public class NewGamePanel extends JPanel {

    private final int width, height, scale;
    Graphics2D g2d;

    NewGamePanel(int width, int height, int scale) {
        this.width = width;
        this.height = height;
        this.scale = scale;
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(width, height);
    }

    @Override
    public Color getBackground() {
        return Color.black;
    }

    /**
     * Ensures GUI is painted when the window is moved or hidden.
     */
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        //paintDots();
        paintTitle();
        paintSubtitle();
    }

    public void paintTitle() {
        g2d.setColor(Color.green);
        Font font = new Font("Monospaced", Font.PLAIN, width / 10);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "Speed Snake");
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height / 2 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void paintSubtitle() {
        g2d.setColor(Color.red);
        Font font = new Font("Monospaced", Font.PLAIN, width / 25);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "press any key to continue");
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 6 / 10 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void paintDots() {
        g2d.setStroke(new BasicStroke(0.25f));
        g2d.setColor(Color.gray);
        for (int i = 0; i <= width / scale; i++) {
            for (int j = 0; j <= height / scale; j++) {
                if (i * scale == width) {
                    g2d.fillRect(i * scale - 1, j * scale, 1, 1);
                } else if (j * scale == height) {
                    g2d.fillRect(i * scale, j * scale - 1, 1, 1);
                } else {
                    g2d.fillRect(i * scale, j * scale, 1, 1);
                }
            }
        }
        g2d.fillRect(width - 1, height - 1, 1, 1);

    }

}

DifficultyPanel.java

package view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import javax.swing.JPanel;

/**
 * Displays available difficulties before starting game.
 *
 * @author Justin Beringer
 */
public class DifficultyPanel extends JPanel {

    private final int width, height, scale;
    Graphics2D g2d;

    DifficultyPanel(int width, int height, int scale) {
        this.width = width;
        this.height = height;
        this.scale = scale;
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(width, height);
    }

    @Override
    public Color getBackground() {
        return Color.black;
    }

    /**
     * Ensures GUI is painted when the window is moved or hidden.
     */
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        paintTitle();
        paintDifficulties();
    }

    public void paintTitle() {
        g2d.setColor(Color.green);
        Font font = new Font("Monospaced", Font.PLAIN, width / 10);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "Difficulty");
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 2 / 5 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void paintDifficulties() {
        g2d.setColor(Color.red);
        Font font = new Font("Monospaced", Font.PLAIN, width / 25);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "1 - N00b");
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 5 / 10 - ((int) gv.getVisualBounds().getHeight() / 2));
        gv = font.createGlyphVector(frc, "2 - quick");
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 6 / 10 - ((int) gv.getVisualBounds().getHeight() / 2));
        gv = font.createGlyphVector(frc, "3 - Crazy");
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 7 / 10 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

}

GameOverPanel.java

package view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import javax.swing.JPanel;

/**
 * Drawn to signify a Game Over due to collision.
 *
 * @author Justin Beringer
 */
public class GameOverPanel extends JPanel {

    private final int width, height, scale;
    int applesEaten, highScore;
    Graphics2D g2d;
    private String difficulty;

    GameOverPanel(int width, int height, int scale) {
        this.width = width;
        this.height = height;
        this.scale = scale;
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(width, height);
    }

    @Override
    public Color getBackground() {
        return Color.black;
    }

    /**
     * Ensures GUI is painted when the window is moved or hidden.
     */
    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        paintGameOver();
        paintDifficulty();
        paintScore();
        paintHighScore();
        paintPlayAgain();

    }

    public void paintGameOver() {
        g2d.setColor(Color.red);
        Font font = new Font("Monospaced", Font.PLAIN, width / 10);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "GAME OVER");
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 7 / 20 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void paintDifficulty() {
        g2d.setColor(Color.green);
        Font font = new Font("Monospaced", Font.PLAIN, width / 25);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "Difficulty: " + difficulty);
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 9 / 20 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void paintScore() {
        Font font = new Font("Monospaced", Font.PLAIN, width / 25);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "Score: " + applesEaten);
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 11 / 20 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void paintHighScore() {
        Font font = new Font("Monospaced", Font.PLAIN, width / 25);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "High Score: " + highScore);
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 13 / 20 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void paintPlayAgain() {
        g2d.setColor(Color.red);
        Font font = new Font("Monospaced", Font.PLAIN, width / 15);
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, "Play Again? Y/N");
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * 16 / 20 - ((int) gv.getVisualBounds().getHeight() / 2));
    }

    public void update(String difficulty, int applesEaten, int highScore) {
        this.difficulty = difficulty;
        this.applesEaten = applesEaten;
        this.highScore = highScore;
    }

}

ViewListener.java

package view;

import controller.Controller;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

/**
 * Passes keyPress and other actions from the view to the controller.
 * 
 * @author Justin Beringer
 */
public class ViewListener implements KeyListener {

    boolean isGameOver;
    Controller controller = new Controller();

    public ViewListener() {
        isGameOver = false;
    }

    @Override
    public void keyPressed(KeyEvent key) {
        //System.out.println(key.toString());
        controller.directionInput(key);
    }

    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    public void setGameOver(boolean isGameOver) {
        controller.setGameOver(isGameOver);
    }

    public void setNewGame(boolean isNewGame) {
        controller.setNewGame(isNewGame);
    }

    public void setChoosingDifficulty(boolean isChoosingDifficulty) {
        controller.setChoosingDifficulty(isChoosingDifficulty);
    }

}

Util

Direction.java

package util;

public enum Direction {
    UP,
    DOWN,
    LEFT,
    RIGHT;
}
\$\endgroup\$
3
\$\begingroup\$

you could change

case KeyEvent.VK_UP:
    if (direction != Direction.DOWN && !buttonPressed) {
        direction = Direction.UP;
    }
    buttonPressed = true;
    break;

to

case KeyEvent.VK_UP:
    if (direction != Direction.DOWN && !buttonPressed) {
        direction = Direction.UP;
        buttonPressed = true;
    }
    break;

for a very tiny increase in performance, lol.

Maybe change the method name from directionInput to respondToInput since it does more than just changing direction.


I would remove the static keyword from everything

public class Controller {

private static int TICKS_PER_SECOND;
private static boolean buttonPressed = false;
private static boolean isGameOver = false;
private static boolean isNewGame = true;
private static boolean isPaused = false;
private boolean isChoosingDifficulty;

private static final Model model = new Model();
private static ActionListener taskPerformer;
private static Direction direction;
private static Timer timer;

since you are creating an instance of Controller


Maybe add a panel class that you extend, like:

public class GraphicPanel extends JPanel {

    private int width, height, scale;
    Graphics2D g2d;


    public GraphicPanel() {
    }

    public GraphicPanel(LayoutManager layout) {
        super(layout);
     }

    public GraphicPanel(boolean isDoubleBuffered) {
        super(isDoubleBuffered);
     }

    public GraphicPanel(LayoutManager layout, boolean isDoubleBuffered) {
        super(layout, isDoubleBuffered);
    }


    public void set_g2d(Graphics g) {
        g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    }

    public Font getFont(int widthDividedBy) {
        return new Font("Monospaced", Font.PLAIN, width / widthDividedBy);        
    }

    public void drawGlyphVector(int position, Font font, String text) {
        FontRenderContext frc = g2d.getFontRenderContext();
        GlyphVector gv = font.createGlyphVector(frc, text);
        g2d.drawGlyphVector(gv,
                width / 2 - ((int) gv.getVisualBounds().getWidth() / 2),
                height * position / 10 - ((int) gv.getVisualBounds().getHeight() / 2));
    }


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

so you can shorten your paint methods to:

public void paintTitle() {
    g2d.setColor(Color.green);
    drawGlyphVector(4, getFont(10), "Difficulty");
}

public void paintDifficulties() {
    g2d.setColor(Color.red);
    Font font = getFont(25);
    drawGlyphVector(5, font, "1 - N00b");
    drawGlyphVector(6, font, "2 - quick");
    drawGlyphVector(7, font, "3 - Crazy");
}
\$\endgroup\$
  • \$\begingroup\$ Thanks for the response. In the first part, why does that yield a performance increase? In the second part, some of those variables need to be static since they are used in the main method? The third part is a good idea. \$\endgroup\$ – CompEng Jun 21 '18 at 2:31
  • \$\begingroup\$ oops, I forgot to make things protected scope in the helper class The tiny performance increase is because buttonPressed = true is only set if (direction != Direction.DOWN && !buttonPressed). but it's such a tiny amount that it really doesn't matter, hence the LOL. I've just had some more time to go over your code. I would completely separate the view from the model. The model should be able to function without the view at all. The controller controls the model, then updates the view. Your main should create a new Controller, In my opinion \$\endgroup\$ – Zachary Thomas Jun 21 '18 at 5:27
  • \$\begingroup\$ Use main to construct the Controller. That constructor should create the model and the view, and then start the timer. The timer should move the snake, then tell the view to update. the controller should probably have a method void moveSnake() { model.moveSnake(); buttonPressed = false; } and taskPerformer = (ActionEvent e) -> { moveSnake(); updateView(); }; and modify your View so it doesn't call the controller at all. The controller can ask the view for the keypress through a public method. The controller can choose to ignore keypresses and do predefined moves for testing purposes \$\endgroup\$ – Zachary Thomas Jun 21 '18 at 5:47
  • \$\begingroup\$ You could also pass the view to the model and let the model tell the view when to update if view != null, so it can still function without a view. So the Controller controls the model and the model controls the view, if a view exists. But no matter what, the view should not be controlling the controller. Only constants should be static. Everything else should be instance. \$\endgroup\$ – Zachary Thomas Jun 21 '18 at 6:04
  • 1
    \$\begingroup\$ I was able to significantly streamline my view by using inheritance like you suggested, so thank you for that. However, I'm still a bit confused about your MVC suggestions. When you say my main should be creating the controller, are you suggesting I write a separate class completely outside of the MVC that initializes the controller? \$\endgroup\$ – CompEng Jun 21 '18 at 22:59
1
\$\begingroup\$

This answer is really a reply to a comment in my previous answer because I need to show some code but comments remove all my beautiful formatting.

The question was: "I'm still a bit confused about your MVC suggestions. When you say my main should be creating the controller, are you suggesting I write a separate class completely outside of the MVC that initializes the controller?"

/**
 * Main controller as part of MVC design pattern.
 *
 * @author Justin Beringer, Modified by ZRudzik
 */
public class Controller {

    //nothing here is static
    private Model model;
    private View view;
    private ViewListener viewListener;

    private boolean isGameOver = false;
    private boolean isNewGame = true;
    private boolean isPaused = false;
    private boolean isChoosingDifficulty;

    private boolean buttonPressed = false;
    private Direction direction;

    private ActionListener taskPerformer;
    private int TICKS_PER_SECOND;
    private Timer timer;


    //this launches the program
    public static void main(String[] args) {
        //Start the controller
        Controller controller = new Controller();
    }



    //constructor
    public Controller() {
        //the controller controls everything!
        viewListener = new ViewListener(this) /* pass the controller to the listener if you really want the listener to direct the controller. */ 
        view = new View(viewListener)       
        //pass the view to the model, or null if you don't want a view
        model = new Model(view)

        taskPerformer = (ActionEvent e) -> {
            model.moveSnake();//the model will tell the view when to update and give it all important info          
            buttonPressed = false;
        };

        timer = new Timer(TICKS_PER_SECOND, taskPerformer);
        timer.setInitialDelay(0);
    }
\$\endgroup\$
  • 1
    \$\begingroup\$ I've never initialized a class within itself like you are in the main method. Is this common practice? \$\endgroup\$ – CompEng Jun 25 '18 at 20:51
  • \$\begingroup\$ Yep! That's how a lot of people do it! You can also put main in each class for testing purposes and then you choose which main to launch. If you are in Eclipse you just make sure your cursor is in the main that you wish to launch. Netbeans you have to tell it which project and main is the default to launch. Some people create a Main class so it's obvious which class starts the program and the Main constructor launches the project and main is just public static void main(String[] args) { new Main() } \$\endgroup\$ – Zachary Thomas Jun 27 '18 at 22:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.