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

Java Swing 2D游戏在60Hz显示器上动画规律性微卡顿问题求助

Swing 2D游戏跨刷新率显示器卡顿问题排查

问题现象

  • 窗口运行在240Hz显示器时,所有动画效果流畅顺滑
  • 窗口移动至60Hz显示器后,横向滚动障碍物动画存在全程规律性微卡顿
  • 替换Swing Timer为独立线程、设置16ms循环延迟后,微卡顿大幅缓解,但仍存在约1秒一次的周期性卡顿

附问题相关代码

Main类

package dinoRun;

public class Main {

    public static void main(String[] args) {
        new Window();
    }
}

Window类

package dinoRun;

import javax.swing.*;

public class Window extends JFrame {

    static GamePanel panel;

    Window() {
        panel = new GamePanel();

        this.setResizable(false);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.add(panel);
        this.pack();
        this.setLocationRelativeTo(null);
        this.setTitle("Dino Run");
        this.setVisible(true);
    }
}

GamePanel类

package dinoRun;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Random;

public class GamePanel extends Renderer implements ActionListener {

    final int WIDTH = 1280, HEIGHT = 720;
    Rectangle dino;
    ArrayList<Rectangle> cactii;

    GamePanel() {
        this.setPreferredSize(new Dimension(WIDTH, HEIGHT));
        dino = new Rectangle(WIDTH / 20, HEIGHT / 2, 100, 100);
        cactii = new ArrayList<>();
        addCactus();
        Timer timer = new Timer(1, this);
        timer.start();
    }

    void paintDino(Graphics g) {
        g.setColor(Color.BLACK);
        g.fillRect(dino.x, dino.y, dino.width, dino.height);
    }

    void addCactus() {
        Random rand = new Random();
        int space = rand.nextInt(64) * 10;
        cactii.add(new Rectangle(space + WIDTH * (cactii.size() + 1), HEIGHT / 2, 30, 100));
    }

    void paintCactus(Graphics g, Rectangle cactus) {
        g.setColor(Color.BLACK);
        g.fillRect(cactus.x, cactus.y, cactus.width, cactus.height);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        repaint();
    }

    void repaint(Graphics g) {
        g.setColor(Color.BLACK);
        paintDino(g);
        g.drawLine(0, HEIGHT / 2 + 100, WIDTH, HEIGHT / 2 + 100);
        for (int i = 0; i < cactii.size(); i++){
            if (cactii.get(i).getX() + cactii.get(i).getWidth() != 0) {
                paintCactus(g, cactii.get(i));
                cactii.get(i).translate(-10, 0);
            } else {
                cactii.remove(cactii.get(i));
                i--;
            }
        }
        addCactus();
    }
}

Renderer类

package dinoRun;

import javax.swing.*;
import java.awt.*;

public class Renderer extends JPanel {

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Window.panel.repaint(g);
    }
}

根因分析与修复方案

1. 逻辑更新与帧率绑定,未做时间步长归一化

当前代码每触发一次循环/定时器回调,就将障碍物固定移动10像素,移动速度完全和回调触发频率绑定:

  • 240Hz环境下每秒触发约240次回调,障碍物每秒移动2400像素,高帧率下视觉无卡顿
  • 60Hz环境下Swing定时器精度不足,回调触发频率波动大,固定步长移动会导致单位时间内障碍物移动距离忽快忽慢,直接产生微卡顿
  • 16ms固定延迟的循环理论帧率为62.5FPS,和60Hz显示器的垂直同步节奏存在固定差值,累计约1秒就会产生一帧的延迟差,表现为周期性卡顿

修复方式:将逻辑更新和渲染流程解耦,引入 deltaTime(帧间隔时间) 计算移动距离,不要写死每帧移动像素。核心逻辑示例:

// 初始化时记录上一帧时间
long lastFrameNano = System.nanoTime();
// 配置障碍物目标移动速度:每秒600像素
final double CACTUS_MOVE_SPEED = 600.0;

// 游戏循环内执行
long currentNano = System.nanoTime();
// 计算距上一帧的时间间隔,单位转换为秒
double deltaTime = (currentNano - lastFrameNano) / 1_000_000_000.0;
lastFrameNano = currentNano;
// 实际移动距离 = 速度 * 时间间隔
for (Rectangle cactus : cactii) {
    cactus.translate(-CACTUS_MOVE_SPEED * deltaTime, 0);
}

2. 游戏逻辑写在绘制流程中,更新时机不可控

当前代码把障碍物移动、新增仙人掌的逻辑写在自定义的repaint(Graphics g)方法中,该方法属于Swing绘制链路的一部分:Swing会根据系统负载合并、跳过重绘请求,直接导致逻辑更新频率随绘制调度波动,完全不可控。同时Swing Timer本身精度仅为10~15ms,无法满足稳定游戏循环的需求。

修复方式:

  • 所有游戏状态更新逻辑(移动、碰撞检测、生成/删除元素)必须独立于绘制流程,放在单独的游戏循环线程中执行
  • 绘制方法仅负责读取当前游戏状态,绘制对应画面,不做任何状态修改
  • 废弃Swing Timer作为游戏循环驱动,改用BufferStrategy实现双/三缓冲,配合垂直同步自动适配显示器刷新率,从根源避免不同步问题
  • 对JPanel开启双缓冲:调用setDoubleBuffered(true)

3. 渲染链路设计不合理

当前通过Renderer父类中转调用GamePanel的自定义绘制方法,多了一层不必要的静态引用,既容易引发内存泄漏,也会导致绘制流程无法被Swing正常优化,额外增加绘制开销。

修复方式:去掉独立的Renderer类,直接在GamePanel中重写paintComponent方法完成绘制,所有绘制操作在该方法内完成,不要跨类静态调用。

4. 跨显示器刷新率适配缺失

桌面系统下窗口在不同刷新率显示器间移动时,系统会自动将窗口的显示节奏切换为当前显示器的刷新率,Swing默认的绘制策略不会主动感知这个变化,会出现同步节奏错配。使用BufferStrategy时开启垂直同步,系统会自动匹配当前显示器的刷新率,无需手动计算延迟。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 18:14:51