0

So I'm a bit stuck on this one. I have a pretty basic game where you move a ship around a grid with arrow keys.

I added another thread with some monster that are supposed to automatically roam the grid. I can see from print statements that the thread is running an the Monster is moving, however, the image location is not being updated.

I found some similar questions and there's many recommendations to use Platfrom.runLater. But I'm unsure if it fits my specific case, and if it does, how to implement it.

Here is what the Monster class is doing, moving the monster right one space every second. Like I mentioned, I'm logging the current location every time setX() is called, so I can see that the location is being updated.

import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import java.awt.Point;

public class Monster implements Runnable {

    private Point currentPoint;
    private OceanMap map;

    public Monster(int x, int y) {
        this.currentPoint = new Point(x, y);
        this.map = OceanMap.getInstance();
    }

    public Point getLocation() {
        System.out.println(this.currentPoint.toString());
        return this.currentPoint;
    }

    private void setNewLocation(Point newLocation) {
        this.currentPoint = newLocation;
    }

    private void setY(int newY) {
        this.currentPoint.y = newY;
        this.setNewLocation(new Point(this.currentPoint.x, this.currentPoint.y));
    }

    private void setX(int newX) {
        this.currentPoint.x = newX;
        this.setNewLocation(new Point(this.currentPoint.x, this.currentPoint.y));
        System.out.println(this.currentPoint.toString());
    }

//    public void addToPane() {
//        System.out.println("this is called");
//        iv.setX(this.currentPoint.x + 1 * 50);
//        iv.setY(this.currentPoint.y * 50);
//        obsrvList.add(iv);
//    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setX(this.currentPoint.x + 1);
        }

    }

}

And here is the JavaFX thread.

/* Monster resources */
private Image monsterImage = new Image(getClass().getResource("monster.png").toExternalForm(), 50, 50, true, true);
private ImageView monsterImageView1 = new ImageView(monsterImage);
private Monster monster1;
private Thread monsterThread;

@Override
public void start(Stage oceanStage) throws Exception {

    root = new AnchorPane();
    scene = new Scene(root, scale * xDimensions, scale * yDimensions);

    oceanStage.setScene(scene);
    oceanStage.setTitle("Ocean Explorer");

    /* Draw Grid */
    for (int x = 0; x < xDimensions; x++) {
        for (int y = 0; y < yDimensions; y++) {
            Rectangle rect = new Rectangle(x * scale, y * scale, scale, scale);
            rect.setStroke(Color.BLACK);
            rect.setFill(Color.PALETURQUOISE);
            root.getChildren().add(rect);
        }
    }

    oceanStage.show();

    monsterThread = new Thread(monster1);
    monsterThread.start();
    Platform.runLater(() -> {
        monsterImageView1.setX(monster1.getLocation().x * scale);
        monsterImageView1.setY(monster1.getLocation().y * scale);
        root.getChildren().add(monsterImageView1);
    });

    startSailing();
}

I can provide more code if needed, this is all I thought was relevant at the moment.

So again, my question, how can I update the UI of a JavaFX thread from another thread?

23k
  • 1,596
  • 3
  • 23
  • 52
  • Can you post the source of `Monster` ? – Chris Smith Jun 03 '18 at 21:21
  • 1
    Don’t use threads for this kind of functionality. Use a [`TimeLine`](https://stackoverflow.com/questions/9966136/javafx-periodic-background-task) – James_D Jun 03 '18 at 21:21
  • You do not need another thread for this. Use an AnimationTimer instead. The handle method in the NAimationTimer gives you the timestamp in nanoseconds. After x amount of seconds pass by, your method with `this.setX(this.currentPoint.x + 1); stop();` – Jose Martinez Jun 03 '18 at 21:23
  • @ChrisSmith updated – 23k Jun 03 '18 at 21:23
  • TimeLine even better. @James_D – Jose Martinez Jun 03 '18 at 21:23
  • @James_D so inside of TimeLine I would do something similar to what I have currently in Platform.runLater? – 23k Jun 03 '18 at 21:26
  • @23k Yes. See linked question. – James_D Jun 03 '18 at 21:27
  • 2
    While the duplicated answer would be the correct way to do this, I thought I'd address your original question. The issue is you're updating `currentPoint` inside the `Monster` class, but this value is never propagating to `monsterImageView1`. The way to fix that specific issue would be to convert `currentPoint` into an `ObjectProperty` and then using bindings on it. But I agree with @James_D, a `TimeLine` would be the better approach. – Chris Smith Jun 03 '18 at 21:28
  • @ChrisSmith Good point. I reopened the question, if you want to assemble all that into an answer. – James_D Jun 03 '18 at 21:40

1 Answers1

2

While you are updating currentPoint inside Monster, this value is never propagated to monsterImageView1. You should convert currentPoint into a property and then bind to it:

class Point {
    final int x;
    final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

class Monster implements Runnable {
    private ReadOnlyObjectWrapper<Point> location = new ReadOnlyObjectWrapper<>();

    public Monster(int x, int y) {
        setLocation(new Point(x, y));
    }

    public Point getLocation() {
        return this.location.get();
    }

    private void setLocation(Point location) {
        this.location.set(location);
    }

    public ReadOnlyProperty<Point> locationProperty() {
        return this.location.getReadOnlyProperty();
    }

    private void setY(int newY) {
        this.setLocation(new Point(this.getLocation().x, newY));
    }

    private void setX(int newX) {
        this.setLocation(new Point(newX, this.getLocation().y));
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // run the update on the FX Application Thread for thread safety as well as to prevent errors in certain cases
            Platform.runLater(() -> this.setX(this.getLocation().x + 1));
        }
    }
}

monsterThread = new Thread(monster1);
monsterThread.start();
monsterImageView1.xProperty().bind(Bindings.createIntegerBinding(() -> monster1.getLocation().x * scale, monster1.locationProperty()));
monsterImageView1.yProperty().bind(Bindings.createIntegerBinding(() -> monster1.getLocation().y * scale, monster1.locationProperty()));
root.getChildren().add(monsterImageView1);

However, as @James_D mentions, a Timeline would be a far better approach to solving this the correct way:

class Monster {
    private ReadOnlyObjectWrapper<Point> location = new ReadOnlyObjectWrapper<>();
    private Timeline timeline;

    public Monster(int x, int y) {
        setLocation(new Point(x, y));

        timeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> {
            setX(getLocation().x + 1);
        }));
        timeline.setCycleCount(Timeline.INDEFINITE);
    }

    public void start() {
        timeline.play();
    }

    // NOTE: remember to call stop() or this will result in a memory leak
    public void stop() {
        timeline.stop();
    }

    public Point getLocation() {
        return this.location.get();
    }

    private void setLocation(Point location) {
        this.location.set(location);
    }

    public ReadOnlyProperty<Point> locationProperty() {
        return this.location.getReadOnlyProperty();
    }

    private void setY(int newY) {
        this.setLocation(new Point(this.getLocation().x, newY));
    }

    private void setX(int newX) {
        this.setLocation(new Point(newX, this.getLocation().y));
    }
}
Chris Smith
  • 2,928
  • 4
  • 27
  • 59