Java Swing отображает несоответствия и мерцает

Я пытаюсь ознакомиться с созданием графических интерфейсов на Java и написал программу, которая должна перемещать квадрат на экране в зависимости от ввода клавиши WASD (следуя онлайн-руководству).

Однако при запуске кажется, что квадрат мерцает и «запаздывает», иногда рендеринг плавный, но в большинстве случаев нет.

Вот мой класс JPanel, в котором происходит основная работа:

package me.analyzers.scs.game;

import me.analyzers.scs.utilities.KeyPressHandler;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class MainPanel extends JPanel implements Runnable{
    final int FPS = 240;
    final int tileSize = 16;
    final int scaling = 3;
    final int realTileSize = tileSize * scaling;
    final int widthX = 16;
    final int widthY = 12;
    final int screenWidth = realTileSize * widthX;
    final int screenHeight = realTileSize * widthY;

    Thread mainLoop;
    KeyPressHandler keyPressHandler = new KeyPressHandler();

    public MainPanel() {
        super();
        this.setPreferredSize(new Dimension(screenWidth, screenHeight));
        this.setBackground(Color.WHITE);
        this.addKeyListener(keyPressHandler);
        this.setFocusable(true);
    }

    public void startGameLoop() {
        mainLoop = new Thread(this);
        mainLoop.start();
    }

    public boolean isRunning() {
        return mainLoop != null;
    }

    int posX = 0;
    int posY = 0;

    @Override
    public void run() {
        long lastTime = 0;
        while(isRunning()) {
            long currentTime = System.currentTimeMillis();
            if (currentTime-lastTime > 1000/FPS) {
                lastTime = System.currentTimeMillis();
                tick();
                repaint();
            }
        }
    }

    public void tick() {
        switch (keyPressHandler.getLastKey()) {
            case 'w' -> posY-=1;
            case 's' -> posY+=1;
            case 'a' -> posX-=1;
            case 'd' -> posX+=1;
        }
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setColor(Color.BLACK);
        g2d.fillRect(posX, posY, realTileSize, realTileSize);
        g2d.dispose();
    }
}


Сначала я подумал, что это может быть вызвано проблемами параллелизма, и поэтому я часто менял код «основного цикла»; Я попытался выполнить его условно, проверив currentTime() с помощью LastTime, я попробовал выполнить Thread.sleep, а также попробовал использовать класс Timer Swing. Все они дают один и тот же результат.

Интересно отметить, что если я вызову repaint() внутри метода PaintComponent() (думаю, вызывая цикл), он будет работать нормально; но это вызывает PaintComponent() сотни раз за игровой цикл.

После поиска в сети подобных проблем и неспособности их исправить я попытался запустить тот же jar-архив на виртуальной машине Windows. Удивительно, но он все визуализирует плавно! Также кажется, что на компьютере с Windows квадрат движется намного медленнее, чем на моем. Это меня сильно смущает, потому что я думал, что JVM должна быть единообразной на всех платформах.

Обновлено: Я понял, что постоянный ввод (включая спам-нажатия на окно) временно останавливает странное заикание. Это заставляет меня задаться вопросом, не вызвано ли это каким-то процессом, пытающимся «экономить время процессора» в моем графическом интерфейсе? Существует ли это?

Обновлено еще раз: Toolkit.sync() успешно решил эту проблему. Я считаю, что моя оконная система работает из-за «буферизации графических событий» (что объясняет постоянный ввод, останавливающий заикание).

Вам нужно это изучить, поскольку ваши «дела» идут не в ту ветку. Изменения в вашем графическом интерфейсе следует вносить в потоке отправки событий.

g00se 19.06.2024 22:03

Не разрешается выполнять вызовы Swing в каком-либо потоке, кроме выделенного потока событий AWT. Не пытайтесь создать новую тему; вместо этого используйте javax.swing.Timer. Кроме того, никогда не удаляйте объект Graphics, если вы его не создали. Помимо этого, ваши проблемы с рендерингом, скорее всего, будут исправлены вызовом Toolkit.sync после каждой перерисовки.

VGR 20.06.2024 01:52
g2d.dispose(); -- это не является причиной вашей нынешней проблемы, но может вызвать проблемы в будущем. Никогда не удаляйте объект Graphics, предоставленный вам JVM, поскольку это может нарушить цепочку рисования. Утилизируйте только те объекты Graphics, которые вы сами создали. Кастинг не считается творчеством.
Hovercraft Full Of Eels 20.06.2024 03:11

Чего бы это ни стоило, я скопировал код из вашего вопроса и добавил свою собственную реализацию класса KeyPressHandler, а также метод main для запуска приложения, и, похоже, у меня это работает нормально.

Abra 20.06.2024 09:40

@VGR Спасибо, Toolkit.sync() решил мои проблемы с заиканием! Я считаю, что это произошло потому, что моя оконная система выполняет буферизацию графических событий, что также объясняет, почему спам-входы «заставили» ее обновиться прямо сейчас, устранив заикание. Пожалуйста, ответьте, чтобы я мог принять его!

Analyzers 20.06.2024 23:22
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
6
79
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ваша программа должна начинаться так:

class Program {

   public static void main( String[] args ) {
      EventQueue.invokeLater( () -> { 
         gui = new Gui();
      });
   }
}

class Gui extends JFrame {

   public Gui() {
      .....
      .....
      setVisible( true );
   }

....
....

}

Таким образом, вся ваша программа будет работать в потоке событий.
С другой стороны, setVisible( true ); должен находиться в последней строке конструктора (за ним может следовать назначение слушателя).
Внеся эти исправления, ваша проблема, вероятно, исчезнет.

«Таким образом, вся ваша программа будет работать в потоке событий». Нет, не будет. Вот для чего нужен EventQueue.invokeLater. Например, EventQueue.invokeLater(() -> new Gui());.

VGR 20.06.2024 14:15

@VGR, спасибо за комментарий, я исправил.

Marce Puente 20.06.2024 16:36
Ответ принят как подходящий

Основная причина этого в том, что вам нужно вызывать getToolkit().sync() после каждой перерисовки.

Однако это не единственная ваша проблема. Вы не должны удалять объект Graphics, если вы сами его не создали. Графика, переданная в PaintComponent, вам не принадлежит. Swing создал его, и только Swing может избавиться от него. Вмешательство в это может вызвать проблемы с покраской.

Другая проблема заключается в использовании вами Thread. Все методы Swing должны выполняться в одном выделенном потоке для операций AWT и Swing. Если вы вызываете эти методы из другого потока, они могут иногда работать, а иногда давать сбой. Или они могут работать только на определенных компьютерах. Если вы хотите быть уверенным, что ваша программа работает всегда и на каждом компьютере, избегайте вызова методов Swing из другого потока. (Все это задокументировано по адресу https://docs.oracle.com/javase/tutorial/uiswing/concurrency/ .)

Правильный способ запуска кода Swing через регулярные промежутки времени — это javax.swing.Timer. (Это не тот же класс, что и java.util.Timer!) Например:

Timer timer = new Timer(1000 / FPS,
    e -> {
        tick();
        repaint();
        getToolkit().sync();
    });
timer.start();

Другие вопросы по теме