【问题标题】:Problem with snake colliding with itself (snake game)蛇与自身相撞的问题(蛇游戏)
【发布时间】:2021-01-25 20:59:31
【问题描述】:

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

因此,当您运行程序时,只需按 Next 并按空格键,游戏就会开始,然后当您向左移动时,只需快速按向上和向右,然后自己查看。不幸的是,我不知道如何解决这个问题,因为我们只学习了 Java 6 个月,而且我们只真正学习了 if 等基础知识。如果您有任何问题,我会尽快回答。

【问题讨论】:

  • 这能回答你的问题吗? snake game: snake colliding with itself
  • @fantaghirocco 不,问题的原因不同。不过还是谢谢。
  • @fantaghirocco 我不确定您是在嘲讽还是真的感到困惑,但您提供的答案是 1)用另一种语言编写,2)与 OP 的问题不同。

标签: java swing timer keypress paintcomponent


【解决方案1】:

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

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

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

  1. directionV,蛇要往左边走
  2. 计时器滴答声和snakeMove 被调用,该方法评估direction 这是V 所以蛇继续向左移动
  3. 在计时器再次滴答作响之前,我在“同一时间”按up + right。因此,在计时器再次滴答之前发生了 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,因为您在 sn-p 中表明它没有必要成为public,但这些更改只是为了提高代码的正确性。然后我创建了一个全局变量:

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

然后到处(snakeMove 方法除外)我删除了currentDirection = 并将其替换为directions.add(...),因此我们不再更改单个变量,而是将每个方向添加到我们的 FIFO em>/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. 所有的swing组件都应该通过SwingUtilities.invokeLaterEDT上创建
  2. 不要使用setBoundssetSize,使用适当的layout manager 并在添加所有组件之后调用JFrame#pack(),然后再使其可见
  3. 覆盖getPreferredSize 而不是调用setPreferredSize
  4. 您不需要使用图形来执行简单的操作,例如绘制字符串,只需使用 JLabel 并将其添加到具有适当布局的 JPanel
  5. 您可以将Graphics 对象转换为Graphics2D 对象,并将一些anti-aliasing and other rendering hints 转换为您的绘图,使其看起来更清晰
  6. 您应该使用KeyBindings 而不是KeyListener
  7. 分离您的代码关注点,不要将一个actionPerformed 用于所有目的,例如计时器和按钮回调,使用任何类以使代码更干净简洁

【讨论】:

  • 该死,这真的很有用,我放学回家时一定要检查一下。真的很感激!
  • 我试过了,它完全按照我的意愿工作,除了一件小事。只是如果我按住左箭头,例如它会在队列中向 V 发送垃圾邮件并且它会中断,但我不能用 if 语句来修复它以查看队列是否包含按下的键吗?
  • 我只是想知道它是如何工作的。比如每次都清空列表是怎么回事?
  • 很高兴为您提供帮助。是的,您也许可以检查添加的最后一个键是否等于通过directions.getLast().equals(...) 按下的当前键。队列以 2 种方式清除 1. 通过在 snakeMove 方法中调用 LinkedList#pop()。从队列中弹出一个项目也会将其删除,并且 2. 重新启动游戏时使用 LinkedList#clear()
  • 不要觉得你偷了任何东西,你问了一个关于代码的问题,只是在某个边缘情况下无法正常工作并得到了答案,你没想到工作会为你完成,你显示代码以正确说明问题。删除代码会使这个问题无效,并使其对其他人没有用处。无论如何,任何人都可以单击您的问题或我的答案的编辑并查看所做的所有编辑。你会没事的。
【解决方案2】:

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

如果有人转得太快,比如说向左行驶时,“上”和“右”太快,蛇仍然在同一 y 轴上,因为它还没有随着向上运动而改变,并且随着'right' 按键,snake 试图在 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

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

【讨论】:

  • 是的,这看起来更像是它,但我应该如何做到这一点,以便您能够快速向下和向右按下,并优先考虑向下,然后触发右侧?我想是这样的:做一个if语句,如果你在向左的时候向上和向右按等。先向上然后向右转。并在所有可能发生这种情况的地方执行此操作。但我不知道如何做到这一点,所以我可以检测我是否按下两个键,它们之间的延迟很小。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-18
  • 1970-01-01
相关资源
最近更新 更多