Issue
I am trying to make a snake game using maven project on JavaFX. In my code snake was moving but the previous coordinates are not cleared on the scene. Are there any solution for this? Here is my code:
public class Main extends Application {
int sceneX = 600;
int sceneY = 400;
ArrayList <Point> snake = new ArrayList<>();
private GraphicsContext gc;
private void run(GraphicsContext gc) {
drawSnake(gc);
for (int i = snake.size()-1; i>=1; i--) {
snake.get(i).x = snake.get(i - 1).x;
snake.get(i).y = snake.get(i - 1).y;
}
snake.get(0).x++;
}
private void drawSnake(GraphicsContext gc) {
for (int i =0; i<3; i++) {
gc.fillRoundRect(snake.get(i).getX(), snake.get(i).getY(),5,5,3,3);
}
}
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SNAKE");
Group root = new Group();
Canvas canvas = new Canvas(sceneX, sceneY);
root.getChildren().add(canvas);
Scene scene = new Scene (root);
primaryStage.setScene(scene);
primaryStage.show();
gc = canvas.getGraphicsContext2D();
for (int i = 0; i<3; i++) {
snake.add(new Point(300, 300));
}
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(130), e -> run(gc)));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
}
Solution
I agree with what @jewelsea mentioned in the comments.
If you don't have very complex drawings/painting on the canvas, I think using scenegraph approach should make things simple.
If you opt for canvas, one possible drawback I can think of is : you need to redraw the entire canvas. Because clearing only part of the rectangle will not be a solution if you have some layered drawings (like background.. etc)
private void run(GraphicsContext gc) {
for (int i = 0; i < snake.size(); i++) {
gc.clearRect(snake.get(i).x, snake.get(i).y, size, size);
}
int lastIndex = snake.size() - 1;
for (int i = 0; i < lastIndex; i++) {
snake.get(i).x = snake.get(i + 1).x;
}
snake.get(lastIndex).x += size;
drawSnake(gc);
}
In the below gif for canvas approach, clearing the snake alone is not sufficient if I have some background.
On the other hand, if I use scenegraph approach, once we include the building blocks of the snake, all we need to do is to update the position of the building blocks.
private void run() {
int lastIndex = shapes.size() - 1;
for (int i = 0; i < lastIndex; i++) {
shapes.get(i).setLayoutX(shapes.get(i + 1).getLayoutX());
}
shapes.get(lastIndex).setLayoutX(shapes.get(lastIndex).getLayoutX() + size);
}
Below is the complete demo differentiating the two approaches:
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
public class SnakeGameDemo extends Application {
int width = 300;
int height = 200;
int startX = 100;
int startY = 100;
int size = 5;
int speed = 250;
List<Point> snake = new ArrayList<>();
List<Rectangle> shapes = new ArrayList<>();
@Override
public void start(Stage primaryStage) {
primaryStage.setTitle("SNAKE");
GridPane root = new GridPane();
root.setPadding(new Insets(10));
root.setHgap(10);
root.setVgap(10);
root.addRow(0, new Label("Canvas"), new Label("SceneGraph"));
root.addRow(1, canvasApproach(), scenegraphApproach());
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
private Node canvasApproach() {
Canvas canvas = new Canvas(width, height);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.setFill(Color.RED);
gc.fillRoundRect(0, 0, width, height, 0, 0);
gc.setFill(Color.GREEN);
for (int i = 0; i < 3; i++) {
snake.add(new Point(startX + (i * size), startY));
}
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(speed), e -> run(gc)));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
return canvas;
}
private void run(GraphicsContext gc) {
for (int i = 0; i < snake.size(); i++) {
gc.clearRect(snake.get(i).x, snake.get(i).y, size, size);
}
int lastIndex = snake.size() - 1;
for (int i = 0; i < lastIndex; i++) {
snake.get(i).x = snake.get(i + 1).x;
}
snake.get(lastIndex).x += size;
drawSnake(gc);
}
private void drawSnake(GraphicsContext gc) {
snake.forEach(block -> {
gc.fillRoundRect(block.getX(), block.getY(), size, size, 3, 3);
});
}
private Node scenegraphApproach() {
Pane pane = new Pane();
pane.setStyle("-fx-background-color:red;");
pane.setPrefSize(width, height);
for (int i = 0; i < 3; i++) {
Rectangle block = new Rectangle(size, size, Color.GREEN);
// Position the shapes in the Pane using the layoutX/Y properties.
block.setLayoutX(startX + (i * size));
block.setLayoutY(startY);
pane.getChildren().add(block);
shapes.add(block);
}
Timeline timeline = new Timeline(new KeyFrame(Duration.millis(speed), e -> run()));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
return pane;
}
private void run() {
int lastIndex = shapes.size() - 1;
for (int i = 0; i < lastIndex; i++) {
shapes.get(i).setLayoutX(shapes.get(i + 1).getLayoutX());
}
shapes.get(lastIndex).setLayoutX(shapes.get(lastIndex).getLayoutX() + size);
}
}
Answered By - Sai Dandem
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)