我正在尝试编写贪吃蛇游戏,但 KeyListener 不起作用

I'm trying to write a snake game, but the KeyListener does not work

我曾尝试将侦听器添加到我创建的 Snake class 的对象、Window class 的对象和 JFrame class 的对象在 Snake class 的构造函数中,但它仍然不起作用。如果我按任意键,必须写入的字符和蛇必须转动,但它不会发生。难道是因为 snakeThread?

public class Main {

    public static void main(String[] args) {
        Window window = new Window();
        System.out.print("k");
    }
}
import javax.swing.*;
import java.awt.*;

public class Window  extends JPanel {
    private Snake snake; 


    public Window() {
        super(true);
        snake = Snake.getSnake(50, 50);
        Thread snakeThread = new Thread(snake);
        snakeThread.start();
     

    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        snake.paint(g);
    }
}
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;


public class Snake extends JPanel implements Runnable {

    public static int length;
    public static ArrayList<Point> parts;
    public static Field field;
    public static Food food;
    public static int speedX=0;
    public static int speedY=1;

private static Snake snake = null;

    public static final int TIME_DELTA = 1000;
public static  Snake getSnake(int w, int h)
{
    if(snake == null) {
        snake = new Snake(w, h);
        snake.addKeyListener(new Turn(snake));
    }
    return snake;
}

    private Snake(int Width, int Heigth) {

        JFrame jf = new JFrame();
        jf.setSize(500,500);
        jf.add(this); //this - это Jpanel которым расширяется Snake
        jf.setVisible(true);


        food = new Food();field = Field.getField();
        Point start = new Point((int)Width/2, (int)Heigth/2); //размеры поля, а не окна
        parts = new ArrayList<>();parts.add(start);
        Point p1 = new Point((int)start.getX(), ((int)start.getY())-1);parts.add(p1);
        Point p2 = new Point((int)start.getX(), ((int)p1.getY())-1);
        parts.add(p2);length = 3;
    }
  

    private boolean checkHead()
    {
        for (int i=1; i<parts.size(); ++i)
        {
            if(parts.get(parts.size()-1).getLocation() == parts.get(i).getLocation())
                return false;
        }

        if(parts.get(parts.size()-1).getX() <=0 || parts.get(parts.size()-1).getX() >= field.sizeX ||
                parts.get(parts.size()-1).getY() <=0 || parts.get(parts.size()-1).getY() >= field.sizeY )
            return false;

        return true;
    }

    public static void move()
    {
        for (Point i: parts)
        {
            i.y=i.y-1*speedY;
            i.x-=1*speedX;
        }
    }
    public static void eat()
    {
        Point np = new Point ((int)parts.get(length).getX(),(int)parts.get(length).getY()-1 );
        parts.add(np);
        ++length;
        food.respawn();
    }

    public static boolean checkFood()
    {
        if(parts.get(parts.size()-1).getX() == food.x &&  parts.get(parts.size()-1).getY()==food.y)
            return true;
        else
            return false;
    }

  
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        for (Point i: parts) 
            g.fillRect((int) i.getX() * 10, (int) i.getY() * 10, 8, 8);

       g.setColor(Color.RED);
       g.fillRect(food.x * 10, food.y * 10, 8, 8);
       g.setColor(Color.BLACK);
    }

    @Override

            public void run() {
        while (checkHead()) {
            move();
            repaint();
            if(checkFood())
                eat();
            try {
                Thread.sleep(TIME_DELTA);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
            }

            public  void turn(char Key)
            {
                int delta = 0;
                for(Point i: parts)
                {
                    switch (Key) {
                        case 'a':
                            i.y=parts.get(parts.size()-1).y;
                            i.x=parts.get(parts.size()-1).x+delta;
                            break;
                        case'd':
                            i.y=parts.get(parts.size()-1).y;
                            i.x=parts.get(parts.size()-1).x-delta;
                            break;

                        case 'w':
                            i.x=parts.get(parts.size()-1).x;
                            i.y=parts.get(parts.size()-1).y-delta;
                            break;
                        case's':
                            i.x=parts.get(parts.size()-1).x;
                            i.y=parts.get(parts.size()-1).y+delta;
                            break;
                    }
                    ++delta;
                }
                repaint();
            }


}
import javax.swing.*;
import java.awt.*;
import java.util.Random;

public class Food extends JPanel {
    public static  int x;
    public static int y;
    private static Random random;

    public Food()
    {
        super(true);
        random = new Random();
        
       x =  random.nextInt(50);
       y = random.nextInt(50);
    }

    @Override
   public void paint(Graphics g) {
        super.paint(g);
        g.setColor(Color.RED);
        g.fillRect(x * 10, y * 10, 8, 8);
        g.setColor(Color.BLACK);
    }

    public  void respawn()
    {
        x = random.nextInt(40);
        y = random.nextInt(40);
        repaint();
    }
}

听众在此:

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;


public class Turn implements KeyListener {
   private char key = 'O';
   private Snake snake;

   public Turn(Snake s)
   {
       this.snake = s;
   }


    @Override
    public void keyTyped(KeyEvent e) {
        System.out.println("0");//when I press a button, "0" must be written, but it doesn't
        if(e.getKeyChar() == 'A')
        {
            System.out.println("a");//this character too
            if(snake.speedX==0)
            {
                snake.speedX=-1;//speedX was not changed
                snake.speedY=0;
                key='a';
            }
        }
        else if (e.getKeyChar() == 'W')
        {
            System.out.println("w");
            if(snake.speedY==0)
            {
                snake.speedY=-1;
                snake.speedX=0;
                key='w';
            }

        }
        else if (e.getKeyChar() == 'S')
        {
            System.out.println("s");
            if(snake.speedY==0)
            {
                snake.speedY=1;
                snake.speedX=0;
                key='s';
            }

        }
        else if (e.getKeyChar() == 'D')
        {
            System.out.println("d");
            if(snake.speedX==0)
            {
                snake.speedX=1;
                snake.speedY=0;
                key='d';
            }
        }
        if(key!='O')
snake.turn(key);
    }

    @Override
    public void keyPressed(KeyEvent e) {

    }

    @Override
    public void keyReleased(KeyEvent e) {

    }
}
  1. 切勿直接在组件上调用 paint(...)。如果你需要绘制一个组件,你可以在该组件上调用 repaint() 。然后 Swing 将 paint(...) 该组件并在添加到面板的所有子组件上调用 paint(...)。

  2. 所以这意味着您需要将 Snake 组件添加到父游戏面板。这也意味着没有理由覆盖 Window 面板的 paint() 方法。

  3. 不要使用静态变量和方法。这表明设计不当。从变量和方法中删除 static 关键字。

  4. 对于动画,您应该使用 Swing Timer,而不是线程。对 Swing 组件的更新应该在事件调度线程 (EDT) 上完成。定时器将在 EDT 上执行代码。

  5. 不要使用 KeyListener。 Swing 旨在与键绑定一起使用。请参阅:Motion Using the Keyboard 了解更多信息和示例。

  6. 自定义绘画是通过覆盖 paintComponent(...) 而不是 paint(...) 完成的。

  7. main() 方法的要点是创建框架并将游戏面板添加到框架中。游戏面板不应创建框架。

我同意 camickr 回答中的所有内容。您的代码中有很多错误,从头开始记住这些错误将有助于避免您面临的许多问题。

但是,您的实际问题是这一行 private static Snake snake = null; 和这种方法:

public static  Snake getSnake(int w, int h)
{
    if(snake == null) {
        snake = new Snake(w, h);
        snake.addKeyListener(new Turn(snake));
    }
    return snake;
}

您永远不需要像那样在 class 中创建自引用。 Snake class 已经是一个 Snake 对象,所以在 class 中使用 private static Snake snake = null; 只是创建一个新的引用到一个全新的和不同的 Snake 在你的 Snake 内。相反,您需要使用 this 关键字来引用 Snake 对象,例如 this.addKeyListener(new Turn(this)); 而不是 snake.addKeyListener(new Turn(snake));

Delete/remove getSnake 方法,你试图混合静态和非静态对象,但不需要,我们可以将方法的 keylistener 部分移到我们的构造函数中(见下文) .请注意,如果您想更新蛇的宽度和高度,那么您应该使用不同的方法分别更新 partsp1 以及 p2

修复蛇class:

固定的 Snake class 可能看起来像这样:

public class Snake extends JPanel implements Runnable {

    //None of the variables or methods should be static
    //remove the static keyword from everywhere in the Snake class
    public int length;
    public ArrayList<Point> parts;
    public Field field;
    public Food food;
    public int speedX=0;
    public int speedY=1;

    public final int TIME_DELTA = 1000;

    //The constructor needs to be a public method (Not private)
    public Snake(int Width, int Heigth) {
        //Delete all the JFrame code,
        //it should be done in the main method

        //Add the key ilstener here (moved from the getSnake method)
        this.addKeyListener(new Turn(this));
        
        //Create the snake
        food = new Food();
        field = Field.getField();
        Point start = new Point((int)Width/2, (int)Heigth/2); //размеры поля, а не окна
        parts = new ArrayList<>();
        parts.add(start);
        Point p1 = new Point((int)start.getX(), ((int)start.getY())-1);parts.add(p1);
        Point p2 = new Point((int)start.getX(), ((int)p1.getY())-1);
        parts.add(p2);
        length = 3;
    }

    //Rest of the code removed for clarity
    ...
}

修复主要方法并删除 Window class:

要使用此更新的 Snake class,您可以完全删除 Window class 并在主 class:

中使用类似的内容
public class Main {

    public static void main(String[] args) {
        //Create JFrame
        JFrame jf = new JFrame();
        jf.setSize(500,500);

        //Create Snake
        Snake snake = new Snake(50, 50);

        //Add Snake to JFrame and set the frame visible
        jf.add(snake);
        jf.setVisible(true);
        
        //Finally start the Snake thread
        Thread snakeThread = new Thread(snake);
        snakeThread.start();
    }
}

还有很多问题需要修复,但这应该可以解决您的关键侦听器无法正常工作的问题。


其他修复:

更好的解决方案是使用键绑定,而不是键监听器。我们可以在您的主方法中创建这样的键绑定:

//Key binding to trigger when KeyEvent.VK_W ('w') is pressed
snake.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), "NORTH");
snake.getActionMap().put("NORTH", new AbstractAction()
     {
     @Override
     public void actionPerformed(ActionEvent e)
     {
         //Your code here to change the snake direction
         //we can call the turn method from inside this snake object/class
         snake.turn('w');
     }
     });

你应该在你的 Snake class 中覆盖 protected void paintComponent(Graphics g) 而不是 public void paint(Graphics g),像这样:

@Override
protected void paintComponent(Graphics g){
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g;
    for (Point i: parts) 
        g2d.fillRect((int) i.getX() * 10, (int) i.getY() * 10, 8, 8);

   g2d.setColor(Color.RED);
   g2d.fillRect(food.x * 10, food.y * 10, 8, 8);
   g2d.setColor(Color.BLACK);
}

最后,与其让你的 Snake class 实现 Runniable,不如在你的 Snake class 中创建一个简单的独立摆动计时器,如下所示:

void startTimer(){
    //Start timer to update snake location every 100ms
    Timer timer = new Timer(100, new ActionListener(){
        @Override
        public void actionPerformed(ActionEvent ae){
            //Your code here to update and repaint the snake location
            snake.updateLocation();
        }
    });
    timer.start();
}

并且在您的主要方法中,您可以简单地使用 snake.startTimer(); 而不是 Thread snakeThread = new Thread(snake);snakeThread.start();