【问题标题】:How to safely update the GUI of a JavaFX application from a background thread?如何从后台线程安全地更新 JavaFX 应用程序的 GUI?
【发布时间】:2018-05-03 23:34:30
【问题描述】:

我正在尝试使用 JavaFX 2.2 编写 Asteroids 游戏,但是当我尝试移动游戏对象(即岩石、太空船和横梁)或检测它们之间的碰撞时遇到了问题他们。

最初我尝试使用 ScheduledThreadPoolExecutor 类的 scheduleAtFixedRate(Runnable, long, long, TimeUnit) 方法从后台线程进行所有移动和碰撞检测,但是这导致了可怕的运行时异常,甚至在我的代码中都没有,因为我试图从后台线程修改 GUI。

我的下一个方法是使用 AnimationTimer 类从 UI 线程本身更新游戏对象。虽然这种方法解决了异常的问题,但在 UI 线程上运行,它会导致严重的延迟。

那么,我想知道是否有可行的方法来更新游戏对象而不会导致异常或滞后?

这是我的应用程序的 Main 类:

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.media.AudioClip;
import javafx.scene.shape.Shape;
import javafx.stage.Stage;

import java.util.ArrayList;

public class Main extends Application {
    private ArrayList<Rock> rocks = new ArrayList<>();
    private ArrayList<Beam> beams = new ArrayList<>();
    private SpaceShip spaceShip = null;
    private Group group;
    private final int SCENE_WIDTH = 900, SCENE_HEIGHT = 600;
    private final int ROCK_COUNT = 20;
    private boolean upKeyPressed, upKeyReleased, zKeyPressed, leftKeyPressed, rightKeyPressed;
    private int bulletsFired = 0, skipCount = 10;
    private AudioClip explosion = new AudioClip(Main.class.getResource("explosion.wav").toString());
    private AudioClip destroy = new AudioClip(Main.class.getResource("destroy.mp3").toString());

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        ImageView spaceBackground = new ImageView("space.jpg");
        spaceBackground.setFitHeight(SCENE_HEIGHT);
        spaceBackground.setFitWidth(SCENE_WIDTH);

        group = new Group(spaceBackground);
        Scene scene = new Scene(group, SCENE_WIDTH, SCENE_HEIGHT);

        initializeGameObjects();

        // add event listeners for the spaceShip controls
        scene.setOnKeyPressed((keyEvent) -> {
            switch(keyEvent.getCode()) {
                case UP:
                    upKeyPressed = true;
                    break;
                case Z:
                    zKeyPressed = true;
                    break;
                case LEFT:
                    leftKeyPressed = true;
                    break;
                case RIGHT:
                    rightKeyPressed = true;
            }
        });

        scene.setOnKeyReleased((keyEvent) -> {
            switch(keyEvent.getCode()) {
                case UP:
                    upKeyPressed = false;
                    upKeyReleased = true;
                    break;
                case Z:
                    zKeyPressed = false;
                    break;
                case LEFT:
                    leftKeyPressed = false;
                    break;
                case RIGHT:
                    rightKeyPressed = false;
            }
        });

        AnimationTimer updater = new AnimationTimer() {
            @Override
            public void handle(long now) {
                updateGameObjects();
            }
        };

        primaryStage.setScene(scene);
        primaryStage.setTitle("Asteroids");
        primaryStage.setResizable(false);
        primaryStage.getIcons().add(new Image(Main.class.getResource("icon.png").toString()));
        primaryStage.show();

        updater.start();
    }

    private void initializeGameObjects() {
        // initialize the Rock ArrayList
        for(int i=0; i<ROCK_COUNT; i++) {
            Rock rock = new Rock();
            rocks.add(rock);
            group.getChildren().add(rock);
        }

        // add the space ship to the center
        spaceShip = new SpaceShip();
        group.getChildren().add(spaceShip);
    }

    private void updateGameObjects() {
        // move the rocks
        for(Rock rock: rocks) {
            rock.move(rocks);
        }

        // check for collision among rocks
        for(int i=0; i<rocks.size(); i++) {
            for(int j=i+1; j<rocks.size(); j++) {
                Rock rock1 = rocks.get(i), rock2 = rocks.get(j);

                // if two rocks collide, interchange their speeds
                if(rock1.getBoundsInParent().intersects(rock2.getBoundsInParent())) {
                    int tmpSpeedX = rock1.getSpeedX();
                    int tmpSpeedY = rock1.getSpeedY();

                    rock1.setSpeedX(rock2.getSpeedX());
                    rock1.setSpeedY(rock2.getSpeedY());

                    rock2.setSpeedX(tmpSpeedX);
                    rock2.setSpeedY(tmpSpeedY);
                }
            }
        }

        // control the spaceShip
        if(upKeyPressed) {
            spaceShip.accelerate();
            //System.out.println(spaceShip.getSpeed());
        }
        else if(upKeyReleased) {
            if(spaceShip.getSpeed() > 0)
                spaceShip.decelerate();
            else {
                spaceShip.nullifySpeed();
                upKeyReleased = false;
            }
            //System.out.println(spaceShip.getSpeed());
        }

        if(leftKeyPressed)
            spaceShip.rotateLeft();
        if(rightKeyPressed)
            spaceShip.rotateRight();
        if(zKeyPressed) {
            if(bulletsFired < 4) {
                beams = spaceShip.fire(group);
                bulletsFired++;
                skipCount = 15;
            } else {
                skipCount--;

                if(skipCount == 0)
                    bulletsFired = 0;
            }
        }

        // move the beams
        for(int i=0; i<beams.size(); i++) {
            Beam beam = beams.get(i);

            if(!beam.isAlive()) {
                beams.remove(beam);
                continue;
            }

            beam.move();
        }

        // check if the ship hits a rock
        for(int i=0; i<rocks.size(); i++) {
            Rock rock = rocks.get(i);

            if(Shape.intersect(spaceShip, rock).getLayoutBounds().getWidth() > 0) {
                rock.setVisible(false);
                rocks.remove(rock);
                explosion.play(0.04, 0, 1.5, 0, 1);
            }
        }

        // check if a beam hits a rock
        for(int i=0; i<beams.size(); i++) {
            for(int j=0; j<rocks.size(); j++) {
                Beam beam = beams.get(i);
                Rock rock = rocks.get(j);

                if(Shape.intersect(beam, rock).getLayoutBounds().getWidth() > 1) {
                    rock.setVisible(false);
                    rocks.remove(rock);
                    beam.setVisible(false);
                    beams.remove(beam);

                    destroy.play(0.04, 0, 1.5, 0, 1);
                }
            }
        }
    }
}

为了简洁起见,我省略了 SpaceShip、Beam 和 Rock 类。

【问题讨论】:

  • 正如我在问题中提到的,我目前正在使用 AnimationTimer。
  • 本教程会讲解如何正确使用。我没有注意到你已经在使用它了。
  • 这应该不会导致任何性能问题,除非您在任何时候都有大量的游戏对象(尤其是岩石)。
  • AnimationTimer 绝对是这里的正确方法;基本上,您的任何代码都不能/不应该在 FX 应用程序线程之外执行,因此在后台线程中无需执行任何操作。我有一个类似的例子here,它对于合理数量的对象运行得非常顺利。

标签: java multithreading javafx concurrency


【解决方案1】:

我发现了问题。我按照 James_D 的建议分析了我的代码,发现所有延迟都是由 Shape.intersect(Shape, Shape) 方法引起的,我使用该方法进行更精确的碰撞检查。我用常规的边界检查方法替换了它,它现在运行顺利。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-11-25
    • 2021-06-26
    • 1970-01-01
    • 2015-05-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-30
    相关资源
    最近更新 更多