You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java Swing Pong游戏:无法同时移动两个Raqueta问题求助

Java Swing Pong双球拍同时移动问题

我用Java Swing开发Pong游戏,需求是两名玩家通过同一键盘的W-S键和上下方向键分别控制左右两个球拍移动,但存在无法同时移动两个球拍的问题:按下W键移动左侧球拍时,必须松开W键才能操作右侧球拍,反之亦然。已将两个球拍放入不同线程,但问题依旧,推测是KeyListener导致的。

核心代码

KeyListener相关代码(Ventana类)

public Ventana() {
    setSize(600, 400);
    setResizable(false);
    setLocationRelativeTo(null);
    setTitle("Pong");
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    
    jugador1 = new Raqueta(this, Raqueta.JUGADOR1);
    jugador2 = new Raqueta(this, Raqueta.JUGADOR2);
    
    jugador1.start();
    jugador2.start();
            
    addKeyListener(this);
}

@Override
public void paint(Graphics g) { 
    g.setColor(Color.BLACK);
    g.fillRect(0, 0, getWidth(), getHeight());
    
    g.setColor(Color.WHITE);
    jugador1.dibujar(g);
    jugador2.dibujar(g);
}

@Override
public void keyTyped(KeyEvent e) {}

@Override
public void keyPressed(KeyEvent e) {
    switch (e.getKeyCode()) {
        case KeyEvent.VK_W:
            jugador1.moverY(Raqueta.SUBIR);
            break;
        case KeyEvent.VK_S:
            jugador1.moverY(Raqueta.BAJAR);
            break;
        case KeyEvent.VK_UP:
            jugador2.moverY(Raqueta.SUBIR);
            break;
        case KeyEvent.VK_DOWN:
            jugador2.moverY(Raqueta.BAJAR);
            break;
    }
    
    repaint();
}

@Override
public void keyReleased(KeyEvent e) {
    switch (e.getKeyCode()) {
        case KeyEvent.VK_W:
        case KeyEvent.VK_S:
            jugador1.moverY(Raqueta.PARAR);
        case KeyEvent.VK_UP:
        case KeyEvent.VK_DOWN:
            jugador2.moverY(Raqueta.PARAR);
    }
}

Raqueta类代码

public class Raqueta extends Thread{
    private int x, y, anchura, altura;
    private byte direccion;
    public final static byte BAJAR = 0;
    public final static byte SUBIR = 1;
    public final static byte PARAR = -1;
    public final static byte JUGADOR1 = 1;
    public final static byte JUGADOR2 = 2;
    
    public void setDireccion(byte direccion) {
        this.direccion = direccion;
    }
    
    @Override
    public void run() {
        try {
            while (true) {
                moverY(direccion);
                Thread.sleep(5);
            }
        } catch (InterruptedException ex) {
            System.exit(0);
        }
    }
    
    public void moverY(byte x) {
        if (x == BAJAR){
            this.y = y + 10;
        } else if (x == SUBIR){
            this.y = y - 10;
        }
    }
    
    public void dibujar(Graphics g){
        g.fillRect(getX(), getY(), getAnchura(), getAltura());
    }
}

问题根源

  1. KeyListener多键处理缺陷:Swing的KeyListener无法可靠处理多键同时按下的场景,操作系统可能不会持续发送所有按键的重复事件,导致仅第一个按住的键能触发移动。
  2. keyReleased逻辑错误:switch分支未添加break,松开W/S键时会同时触发右侧球拍停止,直接打断另一玩家操作。
  3. 线程与事件同步混乱:keyPressed直接单次调用移动方法,而线程循环又依赖direccion状态,但direccion未被正确关联到按键状态,导致移动逻辑冲突。

修复方案

1. 修复keyReleased的break问题

给switch分支添加break,避免松开一个玩家的按键影响另一个:

@Override
public void keyReleased(KeyEvent e) {
    switch (e.getKeyCode()) {
        case KeyEvent.VK_W:
        case KeyEvent.VK_S:
            jugador1.setDireccion(Raqueta.PARAR);
            break;
        case KeyEvent.VK_UP:
        case KeyEvent.VK_DOWN:
            jugador2.setDireccion(Raqueta.PARAR);
            break;
    }
}

2. 改用按键状态跟踪

放弃KeyListener的单次事件触发,改用集合跟踪所有按下的按键,在游戏循环中根据状态更新球拍方向:

import java.util.HashSet;
import java.util.Set;

public class Ventana extends JFrame implements KeyListener {
    private Set<Integer> pressedKeys = new HashSet<>();
    private Raqueta jugador1, jugador2;

    @Override
    public void keyPressed(KeyEvent e) {
        pressedKeys.add(e.getKeyCode());
    }

    @Override
    public void keyReleased(KeyEvent e) {
        pressedKeys.remove(e.getKeyCode());
    }

    @Override
    public void keyTyped(KeyEvent e) {}

    // 定时更新球拍方向
    public void updateGameState() {
        // 更新左侧球拍方向
        if (pressedKeys.contains(KeyEvent.VK_W)) {
            jugador1.setDireccion(Raqueta.SUBIR);
        } else if (pressedKeys.contains(KeyEvent.VK_S)) {
            jugador1.setDireccion(Raqueta.BAJAR);
        } else {
            jugador1.setDireccion(Raqueta.PARAR);
        }

        // 更新右侧球拍方向
        if (pressedKeys.contains(KeyEvent.VK_UP)) {
            jugador2.setDireccion(Raqueta.SUBIR);
        } else if (pressedKeys.contains(KeyEvent.VK_DOWN)) {
            jugador2.setDireccion(Raqueta.BAJAR);
        } else {
            jugador2.setDireccion(Raqueta.PARAR);
        }
    }
}

3. 优化Raqueta线程逻辑

给direccion添加volatile保证线程可见性,修复移动逻辑并添加边界检测:

public class Raqueta extends Thread{
    private int x, y, anchura, altura;
    private volatile byte direccion; // 确保线程间可见性
    public final static byte BAJAR = 0;
    public final static byte SUBIR = 1;
    public final static byte PARAR = -1;
    public final static byte JUGADOR1 = 1;
    public final static byte JUGADOR2 = 2;

    // 补充构造方法初始化位置
    public Raqueta(Ventana ventana, byte jugador) {
        anchura = 20;
        altura = 80;
        if (jugador == JUGADOR1) {
            x = 30;
            y = ventana.getHeight() / 2 - altura / 2;
        } else {
            x = ventana.getWidth() - 30 - anchura;
            y = ventana.getHeight() / 2 - altura / 2;
        }
        direccion = PARAR;
    }

    public void setDireccion(byte direccion) {
        this.direccion = direccion;
    }
    
    @Override
    public void run() {
        try {
            while (true) {
                // 根据方向移动
                switch (direccion) {
                    case BAJAR:
                        y += 2;
                        break;
                    case SUBIR:
                        y -= 2;
                        break;
                    case PARAR:
                        break;
                }
                // 边界检测,防止球拍移出窗口
                if (y < 0) y = 0;
                if (y + altura > 400) y = 400 - altura;
                Thread.sleep(16); // 约60帧,更流畅
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
    
    // 实现必要的getter方法
    public int getX() { return x; }
    public int getY() { return y; }
    public int getAnchura() { return anchura; }
    public int getAltura() { return altura; }
    
    public void dibujar(Graphics g){
        g.fillRect(getX(), getY(), getAnchura(), getAltura());
    }
}

4. 添加游戏循环

在Ventana构造方法中启动游戏循环线程,定时更新状态并重绘:

public Ventana() {
    // 原构造代码...
    jugador1 = new Raqueta(this, Raqueta.JUGADOR1);
    jugador2 = new Raqueta(this, Raqueta.JUGADOR2);
    
    jugador1.start();
    jugador2.start();
    
    // 启动游戏循环
    new Thread(() -> {
        try {
            while (true) {
                updateGameState();
                repaint();
                Thread.sleep(16);
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }).start();
    
    addKeyListener(this);
    setFocusable(true); // 确保窗口获取焦点
}

内容的提问来源于stack exchange,提问作者MrFull12

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 12:50:22