我对您的代码进行了一些更改,现在它可以按您的预期正常工作。
问题本质上是,如果在计时器计时之前足够快地按下 2 个键并调用 snakeMove,您将覆盖 direction 变量,因此会“丢失”一个键。
想象一下这些步骤发生了:
-
direction是V,蛇要往左边走
- 计时器滴答声和
snakeMove 被调用,该方法评估direction 这是V 所以蛇继续向左移动
- 在计时器再次滴答作响之前,我在“同一时间”按up + right。因此,在计时器再次滴答之前发生了 2 个事件:
- 第一个键被处理,所以
direction 设置为 up direction == "U"
- 第二个密钥被处理,所以
direction被设置为right direction == "H"
- 现在只有计时器再次计时并调用
snakeMove。该方法在 switch 语句中评估 direction 和 direction == "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) {
}
}
其他一些注意事项如下:
- 所有的swing组件都应该通过
SwingUtilities.invokeLater在EDT上创建
- 不要使用
setBounds 或setSize,使用适当的layout manager 并在添加所有组件之后调用JFrame#pack(),然后再使其可见
- 覆盖
getPreferredSize 而不是调用setPreferredSize
- 您不需要使用图形来执行简单的操作,例如绘制字符串,只需使用
JLabel 并将其添加到具有适当布局的 JPanel 中
- 您可以将
Graphics 对象转换为Graphics2D 对象,并将一些anti-aliasing and other rendering hints 转换为您的绘图,使其看起来更清晰
- 您应该使用KeyBindings 而不是
KeyListener
- 分离您的代码关注点,不要将一个
actionPerformed 用于所有目的,例如计时器和按钮回调,使用任何类以使代码更干净简洁