Search code examples
javaswingtimerkeypresspaintcomponent

Problem with snake colliding with itself (snake game)


I've got this problem with my snake game, it runs into itself when you for example are going left and press up and right fast, because i only told the game that i can't press right if it's going left, and therefor if I press up right before i press right it allows me to which makes the snake go into itself.

So when you run the program just press Next and press space and the game should start then when you are going left just press up and right quickly after that and see it for yourself. I'm not sure how to fix this unfortunatly since we've only learned Java for like 6 months and we only really learned the basics like if etc. If you have any questions I am answering quick.


Solution

  • 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:

    1. direction is V, the snake is going to the left
    2. Timer ticks and snakeMove is called, the method evaluates direction which is V so snake continues to move to the left
    3. Before the timer ticks again I press up + right at the "same time". Thus 2 events take place before the timer ticks again:
      1. 1st key is processed so direction is set to up direction == "U"
      2. 2nd key is processed so direction is set to right direction == "H"
    4. 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.add("H");
                window.remove(restartButton);
                score = 0;
                snakeParts = 7;
                newFruit();
                repaint();
            }
            if (actionEvent.getSource() == toInstructionsButton && !instructions) {
                instructions = true;
                window.remove(toInstructionsButton);
                repaint();
            }
            if (actionEvent.getSource() == tmMove) {
                if (gameRunning) {
                    snakeMove();
                    checkFruit();
                    checkCollisions();
                } else {
                    repaint();
                }
                repaint();
            }
        }
    
        @Override
        public void keyTyped(KeyEvent ke) {
        }
    
        @Override
        public void keyPressed(KeyEvent ke) {
    
            if (ke.getKeyCode() == KeyEvent.VK_SPACE && !gameRunning && instructions) {
                snakeMove();
                checkFruit();
                checkCollisions();
                newFruit();
                gameRunning = true;
                instructions = false;
            }
            if (ke.getKeyCode() == KeyEvent.VK_SPACE && gameRunning) {
                if (gameStarted) {
                    gameStarted = false;
                    tmMove.stop();
                } else {
                    tmMove.start();
                    gameStarted = true;
                }
            }
            if (gameStarted) {
                switch (ke.getKeyCode()) {
                    case KeyEvent.VK_RIGHT:
                    case KeyEvent.VK_D:
                        directions.add("H");
                        break;
                    case KeyEvent.VK_LEFT:
                    case KeyEvent.VK_A:
                        directions.add("V");
                        break;
                    case KeyEvent.VK_UP:
                    case KeyEvent.VK_W:
                        directions.add("U");
                        break;
                    case KeyEvent.VK_DOWN:
                    case KeyEvent.VK_S:
                        directions.add("N");
                        break;
                    case KeyEvent.VK_E:
                        tmMove.setDelay(200);
                        break;
                    case KeyEvent.VK_M:
                        tmMove.setDelay(150);
                        break;
                    case KeyEvent.VK_H:
                        tmMove.setDelay(100);
                        break;
                }
            }
        }
    
        @Override
        public void keyReleased(KeyEvent ke) {
        }
    }
    

    Some other notes to name a few are:

    1. All swing components should be created on the EDT via SwingUtilities.invokeLater
    2. Dont use setBounds or setSize, use an appropriate layout manager and call JFrame#pack() after all components are added and before making it visible
    3. Override getPreferredSize as opposed to calling setPreferredSize
    4. You don't need to use graphics to do simple stuff like drawing a string, simply use a JLabel and add it to a JPanel with an appropriate layout
    5. You can cast your Graphics object to a Graphics2D object and some anti-aliasing and other rendering hints to your drawing so it looks sharper
    6. You should use KeyBindings instead of a KeyListener
    7. Separate your codes concerns, don't use one actionPerformed for all purposes such as timer and buttons callbacks, use anyomous classes to make the code more clean and concise