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

Java开发Pong游戏:使用KeyListener实现双Paddle同时移动的问题

不用多线程!这其实是Java Swing里处理键盘输入的常见误区,KeyListener的机制本身就不适合处理多按键同时输入的场景,引入多线程反而会带来UI线程同步的麻烦,完全没必要。

问题根源

KeyListener的keyPressed事件是每次按键按下(或重复)时触发一次,当你同时按住多个键时,系统会发送多个事件,但你的代码每次只能处理一个键的移动逻辑,而且没法在同一帧里同时更新两个 paddle 的位置——因为repaint()是在单个按键事件结束后才调用的,所以看起来两个 paddle 无法同时动。

正确的做法是跟踪按键的状态,然后用一个游戏循环来统一更新所有物体的位置,再刷新画面。下面是具体的修改方案:


步骤1:添加按键状态跟踪数组

在PaintPanel类中定义一个布尔数组,用来记录每个按键是否被按下:

private boolean[] keys = new boolean[256]; // 覆盖大部分常用按键的VK码

步骤2:简化KeyListener实现

不再在keyPressed里直接移动 paddle,只负责更新按键状态:

@Override
public void keyPressed(KeyEvent e) {
    int key = e.getKeyCode();
    if (key >= 0 && key < keys.length) {
        keys[key] = true;
    }
}

@Override
public void keyReleased(KeyEvent e) {
    int key = e.getKeyCode();
    if (key >= 0 && key < keys.length) {
        keys[key] = false;
    }
}

步骤3:添加游戏循环(用Swing Timer)

Swing Timer是专门为EDT(事件调度线程)设计的定时任务,适合做简单的游戏循环。在PaintPanel的构造函数里初始化Timer:

PaintPanel() {
    setBackground(Color.BLACK);
    addKeyListener(this);
    
    // 游戏循环:每30毫秒更新一次(约33FPS,可根据需求调整)
    Timer gameTimer = new Timer(30, e -> {
        updateGameState();
        repaint();
    });
    gameTimer.start();
}

步骤4:实现统一的状态更新方法

把原来的移动逻辑移到updateGameState()里,根据按键状态同时处理两个 paddle 的移动:

private void updateGameState() {
    // 处理第一个Paddle(W/S控制)
    if (keys[KeyEvent.VK_W] && RACKET_ONE_Y > LOWEST_RACKET_POS) {
        RACKET_ONE_Y -= SPEED;
    }
    if (keys[KeyEvent.VK_S] && RACKET_ONE_Y < HIGHEST_RACKET_POS) {
        RACKET_ONE_Y += SPEED;
    }
    
    // 处理第二个Paddle(O/L控制)
    if (keys[KeyEvent.VK_O] && RACKET_TWO_Y > LOWEST_RACKET_POS) {
        RACKET_TWO_Y -= SPEED;
    }
    if (keys[KeyEvent.VK_L] && RACKET_TWO_Y < HIGHEST_RACKET_POS) {
        RACKET_TWO_Y += SPEED;
    }
}

完整修改后的PaintPanel代码

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

class PaintPanel extends JPanel implements KeyListener {
    // holds the panel's insets
    Insets ins;
    private int RACKET_ONE_Y = 360;
    private final int RACKET_ONE_X = 160;
    private int RACKET_TWO_Y = 360;
    private final int RACKET_TWO_X = 1025;
    private final int SPEED = 5; // 调整为更合理的速度,原50过快
    private final int LOWEST_RACKET_POS = 60;
    private final int HIGHEST_RACKET_POS = 600;
    
    // 按键状态跟踪数组
    private boolean[] keys = new boolean[256];

    // Construct a panel.
    PaintPanel() {
        setBackground(Color.BLACK);
        addKeyListener(this);
        
        // 初始化游戏循环
        Timer gameTimer = new Timer(30, e -> {
            updateGameState();
            repaint();
        });
        gameTimer.start();
    }

    // Override the paintComponent() method.
    protected void paintComponent(Graphics g) {
        // Always call the superclass method first.
        super.paintComponent(g);
        drawBorders(g);
        drawMiddleLine(g);
        drawRacketOne(g);
        drawRacketTwo(g);
    }

    // drawing rackets
    private void drawRacketOne(Graphics g){
        g.setColor(Color.RED);
        g.fillRect(RACKET_ONE_X, RACKET_ONE_Y, 15, 90);
    }
    private void drawRacketTwo(Graphics g){
        g.setColor(Color.RED);
        g.fillRect(RACKET_TWO_X, RACKET_TWO_Y, 15, 90);
    }

    // game frame borders
    private void drawBorders(Graphics g){
        g.setColor(Color.WHITE);
        g.fillRect(100, 50, 1000, 10);
        g.fillRect(100, 50, 10, 660);
        g.fillRect(100, 700, 1000, 10);
        g.fillRect(1090, 50, 10, 660);
    }

    // middle line
    private void drawMiddleLine(Graphics g){
        g.setColor(Color.WHITE);
        for (int i = 70; i < 672; i += 40) {
            g.fillRect(590, i, 10, 25);
        }
    }

    // 统一更新游戏状态
    private void updateGameState() {
        // 第一个Paddle移动逻辑
        if (keys[KeyEvent.VK_W] && RACKET_ONE_Y > LOWEST_RACKET_POS) {
            RACKET_ONE_Y -= SPEED;
        }
        if (keys[KeyEvent.VK_S] && RACKET_ONE_Y < HIGHEST_RACKET_POS) {
            RACKET_ONE_Y += SPEED;
        }

        // 第二个Paddle移动逻辑
        if (keys[KeyEvent.VK_O] && RACKET_TWO_Y > LOWEST_RACKET_POS) {
            RACKET_TWO_Y -= SPEED;
        }
        if (keys[KeyEvent.VK_L] && RACKET_TWO_Y < HIGHEST_RACKET_POS) {
            RACKET_TWO_Y += SPEED;
        }
    }

    @Override
    public void keyTyped(KeyEvent e) { }

    @Override
    public void keyPressed(KeyEvent e) {
        int key = e.getKeyCode();
        if (key >= 0 && key < keys.length) {
            keys[key] = true;
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
        int key = e.getKeyCode();
        if (key >= 0 && key < keys.length) {
            keys[key] = false;
        }
    }
}

补充说明

  • 我把SPEED从50调整为5,原速度过快几乎无法控制,你可以根据手感自行修改。
  • Swing Timer是线程安全的,它的任务在EDT中执行,符合Swing的单线程规则,不会出现UI同步问题。
  • 这种按键状态跟踪的方式是2D游戏处理多按键输入的标准方案,比直接用KeyListener处理事件更灵活,还能避免按键重复触发的延迟问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 20:19:08