I've made some changes to your code which now make it work correctly as you would expect.
Essentially the problem was that if 2 keys are pressed fast enough before the timer ticks and snakeMove
gets called, you would overwrite the direction
variable and thus a key will be "missed".
So imagine these steps take place:
direction
is V, the snake is going to the left
- Timer ticks and
snakeMove
is called, the method evaluates direction
which is V so snake continues to move to the left
- Before the timer ticks again I press up + right at the "same time". Thus 2 events take place before the timer ticks again:
- 1st key is processed so
direction
is set to up direction == "U"
- 2nd key is processed so
direction
is set to right direction == "H"
- Now only the timer ticks again and
snakeMove
is called. The method evaluates direction
in a switch statement and direction == "H"
, thus we have "missed" the direction == "U"
as it was overwritten by the second key press in keyPressed
method the before the timer ticked
To overcome this as I suggested in your previous question to use a FIFO (first in first out) list to correctly process all keys so they are never "missed".
This can be done using a LinkedList which has the pop()
function we need.
So in your code I renamed the global variable direction
to currentDirection
:
private String currentDirection;
and removed the static
modifier as this is not needed, I also removed the static
modifier on snakeMove
as again this is not needed and stops us from accessing instance variables i.e. currentDirection
. I also changed the scope to private
as in the snippet you showed it was unnecessary for it to be public
, but these changes are just for more correctness of code. I then created a global variable:
private LinkedList<String> directions = new LinkedList<>();
Then everywhere (except in the snakeMove
method) I removed currentDirection =
and replaced it with directions.add(...)
, thus we are no longer changing a single variable, but rather adding each direction to our FIFO/LinkedList
. I also removed some if checks you did in the keyPressed
as this was not needed, even if the snake is going in the same direction as the key that is pressed - who cares, just add it to the list of keys pressed so we can process it later in snakeMove
Then in your snakeMove
I did this:
public void snakeMove() {
...
if (!directions.isEmpty()) { // if its not empty we have a new key(s) to process
// proccess the keys pressed from the oldest to the newest and set the new direction
currentDirection = directions.pop(); // takes the first oldest key from the queue
}
switch (currentDirection) {
...
}
}
and that solves the problem mentioned above.
Here is the code with these changes implemented:
import javax.sound.sampled.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.util.LinkedList;
public class FirstGraphic extends JPanel implements ActionListener, KeyListener {
//Storlek p? f?nstret
static int WIDTH = 800, HEIGHT = 840;
Timer tmMove = new Timer(150, this);
private JFrame window;
static int bodySize = 40, xNormalFruit = 0, yNormalFruit = 0, gameSquares = (WIDTH * HEIGHT) / bodySize,
snakeParts = 7, score = 0, restartButtonWIDTH = 190, restartButtonHEIGHT = 50;
static int x[] = new int[gameSquares];
static int y[] = new int[gameSquares];
private String currentDirection;
boolean gameRunning = false, gameStarted = false, instructions = false, isDead = false;
public static JButton restartButton = new JButton("STARTA OM"), toInstructionsButton = new JButton("N?sta");
private LinkedList<String> directions = new LinkedList<>();
public static void main(String[] args) {
JFrame window = new JFrame("Snake Game");
FirstGraphic content = new FirstGraphic(window);
window.setContentPane(content);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setResizable(false);
window.pack();
restartButton.setBounds((WIDTH / 2) - (restartButtonWIDTH) / 2, (HEIGHT - 40) / 2 + 100, restartButtonWIDTH, restartButtonHEIGHT);
restartButton.setBackground(new Color(48, 165, 55));
restartButton.setFont(new Font("Arial", Font.BOLD, 20));
window.setLocationRelativeTo(null);
window.setVisible(true);
content.setUp();
}
public FirstGraphic(JFrame window) {
super();
setPreferredSize(new Dimension(WIDTH, HEIGHT));
setFocusable(true);
requestFocus();
this.window = window;
}
public void setUp() {
addKeyListener(this);
setFocusable(true);
setFocusTraversalKeysEnabled(false);
restartButton.addActionListener(this);
directions.add("H");
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (gameRunning) {
g.setColor(new Color(0, 0, 0));
g.fillRect(0, 0, WIDTH, (HEIGHT - 40));
g.setColor(new Color(63, 116, 41, 255));
g.fillRect(0, HEIGHT - 40, WIDTH, (2));
g.setColor(new Color(0, 0, 0, 240));
g.fillRect(0, HEIGHT - 38, WIDTH, 38);
g.setColor(new Color(0, 0, 0));
}
draw(g);
}
public void draw(Graphics g) {
if (gameRunning) {
g.setColor(new Color(35, 179, 52, 223));
g.fillOval(xNormalFruit, yNormalFruit, bodySize, bodySize);
g.setColor(new Color(44, 141, 23, 255));
g.setFont(new Font("Arial", Font.BOLD, 18));
for (int i = 0; i < snakeParts; i++) {
if (i == 0) {
g.setColor(Color.RED);
g.fillOval(x[i], y[i], bodySize, bodySize);
} else {
g.setColor(Color.PINK);
g.fillOval(x[i], y[i], bodySize, bodySize);
}
}
} else if (!gameRunning && gameStarted) {
gameOver(g);
} else if (!instructions) {
startScene(g);
} else {
instructions(g);
}
}
public void startScene(Graphics g) {
g.setColor(Color.BLACK);
g.fillRect(0, 0, WIDTH, HEIGHT);
g.setColor(Color.WHITE);
g.setFont(new Font("Arial", Font.BOLD, 85));
g.drawString("Ormen Olle's", 150, 170);
g.drawString("?ventyr", 235, 254);
window.add(toInstructionsButton);
toInstructionsButton.setBounds(240, 660, 300, 100);
toInstructionsButton.setBackground(new Color(48, 165, 55));
toInstructionsButton.setForeground(Color.BLACK);
toInstructionsButton.setFont(new Font("Arial", Font.BOLD, 60));
toInstructionsButton.addActionListener(this);
}
public void instructions(Graphics g) {
g.setFont(new Font("Arial", Font.BOLD, 85));
g.setColor(new Color(14, 69, 114));
g.drawString("PRESS SPACE", 210, 720);
}
public void gameOver(Graphics g) {
g.setColor(Color.BLACK);
g.fillRect(0, 0, WIDTH, HEIGHT);
g.setColor(Color.red);
g.setFont(new Font("Arial", Font.BOLD, 65));
FontMetrics metrics = getFontMetrics(g.getFont());
g.drawString("Du dog!", (WIDTH - metrics.stringWidth("Du dog!")) / 2, (HEIGHT - 40) / 2);
g.setColor(new Color(44, 141, 23, 255));
g.setFont(new Font("Arial", Font.BOLD, 20));
FontMetrics metrics2 = getFontMetrics(g.getFont());
g.drawString("SCORE: " + score, (WIDTH - metrics2.stringWidth("SCORE: " + score)) / 2, 50);
window.add(restartButton);
}
public void checkFruit() {
if ((x[0] == xNormalFruit) && (y[0] == yNormalFruit)) {
snakeParts++;
score++;
newFruit();
}
for (int v = 1; v < snakeParts; v++) {
if ((x[v] == xNormalFruit) && y[v] == yNormalFruit) {
newFruit();
}
}
}
public void checkCollisions() {
for (int i = snakeParts; i > 0; i--) {
if ((x[0] == x[i]) && (y[0] == y[i])) {
gameRunning = false;
isDead = true;
}
}
if (x[0] < 0) {
gameRunning = false;
isDead = true;
}
if (x[0] == WIDTH) {
gameRunning = false;
isDead = true;
}
if (y[0] < 0) {
gameRunning = false;
isDead = true;
}
if (y[0] > (HEIGHT - 40) - bodySize) {
gameRunning = false;
isDead = true;
}
if (!gameRunning) {
tmMove.stop();
}
}
public void snakeMove() {
for (int i = snakeParts; i > 0; i--) {
x[i] = x[i - 1];
y[i] = y[i - 1];
}
if (!directions.isEmpty()) {
currentDirection = directions.pop();
}
switch (currentDirection) {
case "H":
x[0] = x[0] + bodySize;
break;
case "V":
x[0] = x[0] - bodySize;
break;
case "U":
y[0] = y[0] - bodySize;
break;
case "N":
y[0] = y[0] + bodySize;
break;
}
}
public static void newFruit() {
xNormalFruit = (rollDice(WIDTH / bodySize) * bodySize) - bodySize;
yNormalFruit = (rollDice((HEIGHT - 40) / bodySize) * bodySize) - bodySize;
}
public static int rollDice(int numberOfSides) {
//Kastar en t?rning med ett specifikt antal sidor.
return (int) (Math.random() * numberOfSides + 1);
}
@Override
public void actionPerformed(ActionEvent actionEvent) {
if (actionEvent.getSource() == restartButton && isDead) {
isDead = false;
for (int i = 0; i < snakeParts; i++) {
if (i == 0) {
x[i] = 0;
y[i] = 0;
} else {
x[i] = 0 - bodySize;
y[i] = 0;
}
}
gameRunning = true;
tmMove.start();
//direction = "H";
directions.clear();
directions.a