I have made a Snake clone using Java 14 and JavaFX 14. My game uses an anonymous instance of the AnimationTimer class as the game loop. The basic UI for the start screen uses FXML, but all of the UI elements in the actual game Scene were added programmatically.
The game board is stored as both a GridPane and as a 2D array of Square objects. Each Square extends javafx.scene.control.Label. The GridPane is used to display the game to the user, and the 2D array is used internally to handle the game logic. Every instance of Square in addition to being a Label, also has added instance variables whose getters and setters are used in conjunction with the GameLogic class. An instance of the GameLogic class is created by the GUI class, which handles the UI.
The basic idea of the program is that each Square stores the direction that the snake body part on that Square should move in when the next frame loads. The head of Snake assigns these directions. The direction that the snake head assigns to the next Square is based on which arrow key the user has most recently pressed. The head of the snake is also used to determine the game over conditions based on whether it has hit the edge of the board or another snake body part. The tail of the snake can either leave its former Square empty or not depending on whether the head has "eaten" the apple. That's how the snake gets longer when an apple is eaten. The snake is defined as the Squares on the board that are also contained in a particular List<Square>. The head is the Square in the List<Square> located at index 0. The tail is located at index size() - 1.
Thus, the structure of my program can be summarized as follows: At the top level is a GUI class which contains an instance of the GameLogic class which includes a 2D array of Square objects. The GUI class is called by a start screen which is controlled by a Main class and an FXML file called start.fxml.
I am going to outline the five files of this program. All but one, start.fxml, are .java files. Feel free to look at them all together, or just review an individual file. The main files in this game are GameLogic.java and GUI.java, which control the internal logic of the game and the user interface, respectively.
First, the start screen: Main.java
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class Main extends Application {
@Override
public void start(Stage stage) throws IOException {
// Stage set up
// Add title
stage.setTitle("Snake");
// Create root from FXML file
Parent root = FXMLLoader.load(getClass().getResource("start.fxml"));
// Create a Scene using that root, and set that as the Scene for the Stage
stage.setScene(new Scene(root));
// Show the Stage to the user
stage.show();
}
@FXML
private void startButtonClicked(ActionEvent actionEvent) {
// This method starts the game when the user clicks on the Button
// First we get the Button that the user clicked
Node source = (Node) (actionEvent.getSource());
// We use that button to get the Stage
Stage stage = (Stage) (source.getScene().getWindow());
// We get the game Scene from GUI.java, and set that as the Scene for the Stage
stage.setScene(GUI.getGameScene());
}
public static void main(String[] args) {
launch(args); // launch the program
}
}
Most of this is just JavaFX boilerplate code. This class is both the point of entry for the program, and the controller for start.fxml.
Which brings us to: start.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<VBox fx:controller="Main" alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="111.0" prefWidth="296.0" spacing="20.0" style="-fx-background-color: rgb(30, 30, 30);" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
<Label alignment="CENTER" text="Welcome to Snake" textAlignment="CENTER" textFill="WHITE">
<font>
<Font name="Century Gothic" size="20.0" />
</font>
</Label>
<Button alignment="CENTER" mnemonicParsing="false" onAction="#startButtonClicked" style="-fx-background-color: transparent; -fx-border-color: white;" text="Start" textAlignment="CENTER" textFill="WHITE">
<font>
<Font name="Century Gothic" size="15.0" />
</font>
</Button>
</VBox>
I wasn't able to add comments to the code because I do not know how to write XML. This code was written with the JavaFX SceneBuilder.
Now for the game itself. I'm going to work bottom up, posting Square.java, then GameLogic.java and lastly GUI.java. But first, I need to point out that I am using the following enum throughout the program.
Direction.java
public enum Direction {
UP, DOWN, RIGHT, LEFT
}
Square.java
import javafx.scene.control.Label;
public class Square extends Label {
// Stores the Square's location in the 2D array
private final int row;
private final int column;
// The board has a checkerboard patter, some some Squares are white and some are black
private final boolean white;
// The user controls the snake and attempts to get to a special square which is an apple. This boolean determines if this is that Square
private boolean apple;
// This is the direction that the particular snake body part should travel in if it is on this square
private Direction direction;
/*The rest of the methods are the standard constructor, getters, and setters*/
public Square(int row, int column, boolean white) {
super();
this.row = row;
this.column = column;
this.white = white;
apple = false;
direction = null;
setMaxHeight(15);
setMaxWidth(15);
setMinWidth(15);
setMinHeight(15);
}
public int getRow() {
return row;
}
public int getColumn() {
return column;
}
public boolean isWhite() {
return white;
}
public boolean isApple() {
return apple;
}
public void setApple(boolean apple) {
this.apple = apple;
}
public Direction getDirection() {
return direction;
}
public void setDirection(Direction direction) {
this.direction = direction;
}
}
The GameLogic class contains both a 2D array of Square objects, and a special List of Square objects which identifies those Squares that the snake is currently on.
GameLogic.java
import java.util.List;
import java.util.Random;
public class GameLogic {
// The game board
private final Square[][] board;
// The particular Squares on the game board that the snake is on
// The Square at index 0 is always the head
// The Square at index snake.size() - 1 is always the tail
private final List<Square> snake;
// Standard constructor
public GameLogic(Square[][] board, List<Square> snake) {
this.board = board;
this.snake = snake;
}
// Change the direction that the head of the snake should move in
public void changeDirection(Direction direction) {
Square head = snake.get(0);
if ((head.getDirection() == Direction.UP && direction == Direction.DOWN) ||
(head.getDirection() == Direction.DOWN && direction == Direction.UP) ||
(head.getDirection() == Direction.RIGHT && direction == Direction.LEFT) ||
(head.getDirection() == Direction.LEFT && direction == Direction.RIGHT)) return;
head.setDirection(direction);
}
// This method increments the game by performing the next move
public boolean nextMove() {
// Identify the row and column of the head
int row = snake.get(0).getRow();
int column = snake.get(0).getColumn();
// Create a variable that each square on the snake should replace itself with when the snake moves
Square nextSquare = null;
// Has the snake eaten an apple this move? Assume no at first
boolean ateTheApple = false;
// Determine which direction the snake should move in
// I will only add comments to the first case, since they all function in the exact same way
switch (snake.get(0).getDirection()) {
case UP:
// If the snake is trying to move off the board, or if the place it is moving to is on its body, game over
if (row == 0 || snake.contains(board[row - 1][column])) return false;
// Otherwise, we can now instantiate nextSquare
nextSquare = board[row - 1][column];
// Thee head is the only body part that passes its direction to nextSquare
nextSquare.setDirection(snake.get(0).getDirection());
// Set nextSquare to be the head
snake.set(0, nextSquare);
break;
case DOWN:
if (row == board.length - 1 || snake.contains(board[row + 1][column])) return false;
nextSquare = board[row + 1][column];
nextSquare.setDirection(snake.get(0).getDirection());
snake.set(0, nextSquare);
break;
case RIGHT:
if (column == board[0].length - 1 || snake.contains(board[row][column + 1])) return false;
nextSquare = board[row][column + 1];
nextSquare.setDirection(snake.get(0).getDirection());
snake.set(0, nextSquare);
break;
case LEFT:
if (column == 0 || snake.contains(board[row][column - 1])) return false;
nextSquare = board[row][column - 1];
nextSquare.setDirection(snake.get(0).getDirection());
snake.set(0, nextSquare);
break;
}
// If the nextSquare variable is an apple
if (nextSquare.isApple()) {
// We don't want this Square to be an apple in the next frame, as the snake's head is currently on it
nextSquare.setApple(false);
// We have eaten the apple
ateTheApple = true;
}
// Loop through the rest of the body parts except for the tail
for (int i = 1; i < snake.size() - 1; i++) {
switch (snake.get(i).getDirection()) {
case UP:
nextSquare = board[snake.get(i).getRow() - 1][snake.get(i).getColumn()];
break;
case DOWN:
nextSquare = board[snake.get(i).getRow() + 1][snake.get(i).getColumn()];
break;
case RIGHT:
nextSquare = board[snake.get(i).getRow()][snake.get(i).getColumn() + 1];
break;
case LEFT:
nextSquare = board[snake.get(i).getRow()][snake.get(i).getColumn() - 1];
break;
}
// Move the body part to nextSquare
snake.set(i, nextSquare);
}
// Identify the tail
Square tail = snake.get(snake.size() - 1);
switch (tail.getDirection()) {
case UP:
nextSquare = board[tail.getRow() - 1][tail.getColumn()];
break;
case DOWN:
nextSquare = board[tail.getRow() + 1][tail.getColumn()];
break;
case RIGHT:
nextSquare = board[tail.getRow()][tail.getColumn() + 1];
break;
case LEFT:
nextSquare = board[tail.getRow()][tail.getColumn() - 1];
break;
}
// Move the tail
snake.set(snake.size() - 1, nextSquare);
// If we ate the apple
if (ateTheApple) {
// Add the former tail right back to increase the length of the tail
snake.add(tail);
// Find a random spot to place the new apple
Random random = new Random();
int r, c;
while (true) {
r = random.nextInt(board.length);
c = random.nextInt(board[0].length);
if (!snake.contains(board[r][c])) {
board[r][c].setApple(true);
break;
}
}
}
// Were done. The move worked, so we return true
return true;
}
// Given the current state of the new board, repaint all the Squares
public void paintBoard() {
for (Square[] row : board) {
for (Square square : row) {
if (square == null) {
System.out.println("Square is null");
return;
}
if (snake.contains(square)) {
square.setStyle("-fx-background-color: green;");
continue;
}
if (square.isApple()) {
square.setStyle("-fx-background-color: red;");
continue;
}
square.setStyle("-fx-background-color: " + (square.isWhite()? "rgb(200, 200, 200)" : "rgb(50, 50, 50)") + ";");
}
}
}
}
Finally, an instance of the GameLogic class is created by the GUI class which displays the game to the user
GUI.java
import javafx.animation.AnimationTimer;
import javafx.scene.Scene;
import javafx.scene.layout.GridPane;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class GUI {
public static Scene getGameScene() {
// This GridPane stores the board
GridPane grid = new GridPane();
// This 2D array also stores the board
Square[][] board = new Square[30][30];
// This identifies which Squares are on the snake
List<Square> snake = new ArrayList<>();
// Loop through the board and initialize the Squares
int count = 0, i, j;
for (i = 0; i < board.length; i++) {
for (j = 0; j < board[0].length; j++) {
board[i][j] = new Square(i, j, (i + count) % 2 == 0);
count++;
grid.add(board[i][j], j, i);
// If the Square is add the starting location, place a snake body part there
// and set its direction to be RIGHT by default
if (i == 10 && j >= 10 && j <= 12) {
board[i][j].setDirection(Direction.RIGHT);
snake.add(0, board[i][j]);
}
}
}
// Place the apple somewhere random
Random random = new Random();
int r, c;
while (true) {
r = random.nextInt(30);
c = random.nextInt(30);
if (!snake.contains(board[r][c])) {
board[r][c].setApple(true);
break;
}
}
// Create an instance of GameLogic. Pass it the board and the list of snake body parts
GameLogic snakeGame = new GameLogic(board, snake);
// Paint the initial board
snakeGame.paintBoard();
// Create a scene and add the GridPane to it
Scene scene = new Scene(grid);
// Store the user inputs
List<String> input = new ArrayList<>();
// Get the inputs to store from the scene
scene.setOnKeyPressed(keyEvent -> {
String code = keyEvent.getCode().toString();
if (input.size() == 0) {
input.add(code);
}
});
scene.setOnKeyReleased(keyEvent -> {
String code = keyEvent.getCode().toString();
input.remove(code);
});
// Start time for animation timer
final long[] lastTime = {System.nanoTime()};
// The game loop
new AnimationTimer() {
@Override
public void handle(long currentTime) {
// If the user has requested a change of direction, do it now
if (input.size() != 0) {
snakeGame.changeDirection(Direction.valueOf(input.get(0)));
}
// Calculate how much time has elapsed since the last frame
double elapsedTime = (currentTime - lastTime[0]) / 1000000000.0;
// If it is time to launch a new frame, do it
if (elapsedTime >= 0.2) {
// Reset the time
lastTime[0] = System.nanoTime();
// Attempt the move
boolean move = snakeGame.nextMove();
// Repaint the board
snakeGame.paintBoard();
// If the user got out, end the game
if (!move) {
grid.setDisable(true);
stop();
}
}
}
}.start(); // Start the game loop
// Finally, return this Scene to to the stage in Main.java
return scene;
}
}
That's it. I'm relatively new to JavaFX so I don't really know how game development with it is supposed to work. I used this article as my starting point.
The start screen:
The game in progress:

