蛇与自身碰撞的问题(贪吃蛇游戏)

Problem with snake colliding with itself (snake game)

我的贪吃蛇游戏遇到了这个问题,例如,当您向左移动并快速向上和向右按时,它 运行 会自行解决,因为我只告诉游戏我不能如果它向左移动,请按右,因此,如果我在按右之前按右,它允许我让蛇进入自身。

因此,当您 运行 程序时,只需按 Next 并按 space 游戏就会开始,然后当您向左移动时,只需快速按向上和向右按钮,然后您就可以看到它了。不幸的是,我不确定如何解决这个问题,因为我们只学习了 Java 大约 6 个月,而且我们只真正学习了 if 等基础知识。如果您有任何问题,我会快速回答。

这不是正确的解决方案吗?

如果有人转得太快,比如向左行驶,'up' 和 'right' 太快,蛇仍然在同一个 y 轴上,因为它还没有改变向上移动,并按下 'right' 键,蛇试图在 x 轴上移动,而 y 轴值仍然相同。

我在您的程序中捕获了 x 和 y 值,对于正常的 运行,它们在第一次转向向下时看起来像这样。开始后,我让蛇运行一直到右边的墙,在它撞到墙之前掉头:

x[0],y[0] | x[1],y[1]
720 , 0   | 680, 0     <-- coordinates before turn
720 , 40  | 720, 0     <-- coordinates after turn

这个,开始后,我让蛇运行翻墙,然后快速向下+向右转,导致游戏结束,坐标如下:

x[0],y[0] | x[1],y[1]
720 , 0   | 680, 0     <-- coordinates before turn
680 , 0   | 720, 0     <-- coordinates after down + right turn

在我看来,行为应该是这样的,除非我遗漏了什么。如果您想避免这种情况,请尝试在按键后添加一小部分延迟。

我对您的代码进行了一些更改,现在它可以像您期望的那样正常工作。

本质上的问题是,如果在计时器滴答和调用 snakeMove 之前足够快地按下 2 个键,您将覆盖 direction 变量,因此将“丢失”一个键。

想象一下发生了这些步骤:

  1. directionV,蛇向左走
  2. 计时器计时并调用 snakeMove,该方法计算 direction,即 V,因此蛇继续向左移动
  3. 在计时器再次计时之前,我“同时”按下 向上 + 向右。因此,在计时器再次计时之前发生了 2 个事件:
    1. 第一个密钥已处理,因此 direction 设置为 up direction == "U"
    2. 第二个键被处理所以 direction 被设置为 right direction == "H"
  4. 现在只有计时器再次计时并调用 snakeMove。该方法在 switch 语句中评估 directiondirection == "H",因此我们“错过”了 direction == "U",因为它被 keyPressed 方法中的第二次按键覆盖计时器已计时

为了克服这个问题,正如我在你之前的问题中所建议的那样,使用 FIFO(先进先出)列表来正确处理所有密钥,这样它们就不会“丢失”。

这可以使用 LinkedList 来完成,它具有我们需要的 pop() 功能。

所以在你的代码中我将全局变量 direction 重命名为 currentDirection:

private String currentDirection; 

并删除了 static 修饰符,因为这不是必需的,我还删除了 snakeMove 上的 static 修饰符,因为这也是不需要的,并且阻止我们访问实例变量,即currentDirection。我还将范围更改为 private,因为在您显示的代码段中没有必要将其设置为 public,但这些更改只是为了提高代码的正确性。然后我创建了一个全局变量:

private LinkedList<String> directions = new LinkedList<>();

然后到处(除了snakeMove方法)我删除了currentDirection = 并用directions.add(...)替换它,因此我们不再改变单个变量,而是添加每个方向到我们的 FIFO/LinkedList。我还删除了你在 keyPressed 中所做的一些 if 检查,因为这不是必需的,即使蛇与按下的键的方向相同 - 谁在乎,只需将它添加到按下的键列表中所以我们稍后可以在 snakeMove

中处理它

然后在你的 snakeMove 我这样做了:

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) {
        ...
    }
}

这样就解决了上面提到的问题。

以下是实施了这些更改的代码:


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) {
    }
}

其他一些注意事项如下:

  1. 应通过 SwingUtilities.invokeLater
  2. EDT 上创建所有 swing 组件
  3. 不要使用 setBoundssetSize,使用适当的 layout manager 并在添加所有组件之后并使其可见之前调用 JFrame#pack()
  4. 覆盖 getPreferredSize 而不是调用 setPreferredSize
  5. 您不需要使用图形来完成简单的事情,例如绘制字符串,只需使用 JLabel 并将其添加到具有适当布局的 JPanel
  6. 您可以将 Graphics 对象投射到 Graphics2D 对象,并将一些 anti-aliasing and other rendering hints 投射到您的绘图中,使其看起来更清晰
  7. 您应该使用 KeyBindings 而不是 KeyListener
  8. 分离您的代码问题,不要将一个 actionPerformed 用于所有目的,例如计时器和按钮回调,使用任意 类 以使代码更简洁