Swing Pong游戏独立GameLoop类调用repaint()无效问题排查
问题根源分析
你的代码里repaint()无效的核心原因是实例不共享:
- 在
GUI类的构造方法中,你创建了一个new Display()并添加到窗口,这是实际显示在屏幕上的Display实例。 - 而在
Gameloop类的构造方法中,你又重新new Display()创建了一个全新的Display对象,这个对象从未被添加到任何窗口,是完全“离线”的组件。
当你在Gameloop中调用这个离线Display的repaint()时,只会更新这个看不见的组件,对窗口上真正的Display没有任何影响。此外你还存在Handler实例不共享的问题:Display和Gameloop各自创建了独立的Handler,两者的数据完全不互通,就算实例共享了,数据也不会同步。
补充Swing线程规则:Swing组件的所有更新操作(包括repaint())都应该在**事件调度线程(EDT)**中执行。你用普通Thread调用repaint()虽不会直接导致失效,但可能引发线程安全问题;而Swing的Timer默认就在EDT中执行任务,这也是之前把Timer放在Display里有效的原因之一,但你之前用Timer时同样犯了实例不共享的错误,所以还是无效。
实现方案(保留Gameloop类的Timer)
1. 共享正确的Display和Handler实例
修改代码,让Gameloop接收已经创建好的、添加到窗口的Display实例,同时共享同一个Handler:
- 修改
Main类,传递共享实例:
package pong; public class Main { public static void main(String args[]) { new Main().start(); } public void start() { Handler handler = new Handler(); Display display = new Display(handler); // 让Display接收共享的Handler new GUI(display); // 把Display传给GUI new Gameloop(display, handler); // 把Display和Handler传给Gameloop } }
- 修改
Display类,通过构造方法接收Handler:
package pong; import java.awt.*; import javax.swing.*; public class Display extends JPanel { private static final long serialVersionUID = 1L; int PANEL_WIDTH = 584; int PANEL_HEIGHT = 362; private int BALL_X; private int BALL_Y; private int BALL_SIZE; private int BALL_X_VELOCITY; private int BALL_Y_VELOCITY; private int PLAYER_WIDTH; private int PLAYER_HEIGHT; private int SELF_X; private int SELF_Y; private int OP_X; private int OP_Y; boolean gameRunning = true; Handler handler; // 修改构造方法,接收外部的Handler Display(Handler handler) { this.handler = handler; getConcreteValues(); } // 其他方法不变 public void getConcreteValues() { BALL_SIZE = handler.getBallSize(); PLAYER_WIDTH = handler.getPlayerWidth(); PLAYER_HEIGHT = handler.getPlayerHeight(); } public void getVariableValues() { int[] recievedData = handler.getData(); BALL_X = recievedData[0]; BALL_Y = recievedData[1]; BALL_X_VELOCITY = recievedData[2]; BALL_Y_VELOCITY = recievedData[3]; SELF_X = recievedData[4]; SELF_Y = recievedData[5]; OP_X = recievedData[6]; OP_Y = recievedData[7]; } @Override public void paintComponent(Graphics g) { super.paintComponent(g); g.setColor(Color.DARK_GRAY); g.fillRect(0, 0, PANEL_WIDTH, PANEL_HEIGHT); g.setColor(Color.WHITE); g.fillOval(BALL_X, BALL_Y, BALL_SIZE, BALL_SIZE); g.fillRect(SELF_X, SELF_Y, PLAYER_WIDTH, PLAYER_HEIGHT); g.fillRect(OP_X, OP_Y, PLAYER_WIDTH, PLAYER_HEIGHT); } }
- 修改
GUI类,接收已创建的Display:
package pong; import javax.swing.*; public class GUI extends JFrame { // 接收外部的Display GUI(Display display) { setTitle("Pong"); setSize(600, 400); setResizable(false); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 建议用常量而非数字3 add(display); setLocationRelativeTo(null); setVisible(true); } }
2. 用Swing Timer实现Gameloop
修改Gameloop类,使用Swing Timer替代普通Thread,确保在EDT中执行更新逻辑:
package pong; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class Gameloop { private Timer gameTimer; private Display display; private Handler handler; // 接收共享的Display和Handler Gameloop(Display display, Handler handler) { this.display = display; this.handler = handler; // 初始化Swing Timer,每16ms触发一次(约60帧) gameTimer = new Timer(16, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // 更新数据 display.getVariableValues(); // 触发重绘,此时操作的是窗口上的Display实例 display.repaint(); } }); gameTimer.start(); } }
额外注意事项
- 永远不要在Swing中创建多个独立的UI组件实例来操作同一个界面,必须确保操作的是已经添加到容器中并显示的那个实例。
- 所有Swing组件的更新、重绘操作都要在EDT线程执行,Swing Timer是实现游戏循环的首选方式,因为它天然适配EDT。
- 共享数据模型(比如你的
Handler)时,要注意线程安全:如果后续加入网络操作,需要确保多线程下数据的一致性,可以用同步块或者线程安全的容器。
内容的提问来源于stack exchange,提问作者SomeoneMissing
相关产品推荐
相关产品推荐

