Search code examples
javamultithreadingjavafxpolling

Java/JavaFX: Poll a database for single value update, while keeping responsiveness in GUI


Together with some friends, I've tried to create a turnbased game. We have some issues regarding checking for when a user has their turn, while also keeping the GUI responsive, and also closing the thread we're using now when the game is closed. I wish to get some information on how to do this, but I'm not sure whether the problem is JavaFX-related, thread-related or both.

I've tried to search as much as I can, but simply couldn't find exactly what I'm looking for, even though I believe it is quite simple. Right now, we have a thread running a loop when you click a button to check whether or not it is your turn. When it isn't your turn, we wish to disable some user input, so I'm not sure if we actually need a thread, other than to keep the responsiveness.

I've also tried implementing a class extending Thread, but this only seemed to make the problems worse by either starting a new thread each time it wasn't the players turn, or freezing the GUI if I put the loop outside of the thread.

public void refreshButtonPressed(){
    try{
        refreshButton.setDisable(true);
        Thread pollThread = new Thread(() -> {
            System.out.println("Thread started");  //Stop being able to start more threads
            int user_id = 0;
            String gamePin = "xxxxxx";
        while (!GameConnection.yourTurn(user_id, Context.getContext().getGamePin())){ //This method checks the database if it is your turn
            try{
                Thread.sleep(5000);  //So we don't flood the database
            }
            catch (InterruptedException e){
                System.out.println("Interrupted");
                break;
            }
             //If we close the game, stop the thread/while loop. 
            if (TurnPolling.closedGame){
                break;
            }
        }

        playerButton.setDisable(false);
        refreshButton.setDisable(false);
        refreshButton.setText("Refresh");
        System.out.println("Thread ended");
        });
        pollThread.start();
    }catch (Exception e){
        e.printStackTrace();
    }
}

And in the controller for the gameScreen.fxml file (Not the main screen, but one loaded via login screens and the Main extending Application).

public void initialize(URL location, ResourceBundle resources) {
    playerButton.setDisable(!GameConnection.yourTurn(user_id, gameTurn));
    myStage.setOnCloseRequest(event -> TurnPolling.closedGame = true);
}

Right now, the TurnPolling class only has the public static boolean closedGame, so as not to keep this in the controller. The last line setting the closedGame = true actually gives me a NullPointerException, which may be because the Stage isn't initialized yet, when I do this in the initialize() method?

I would wish to enable the players buttons only when it is their turn, as well as closing the thread (if needed) when the gameScreen closes. Right now, you have to click a button to check if it is your turn, which again checks every five seconds, and it won't stop when you close the game.`

Please tell me if you need more code or clarification, this is my first big project, so I don't really know how much to put here. I know this isn't working code, but it's as much as I can do without it feeling like cluttering. Thank you for any answers!


Solution

  • First, it is important to remember that it is not permitted to alter JavaFX nodes in any thread other than the JavaFX application thread. So, your thread would need to move these lines:

    playerButton.setDisable(false);
    refreshButton.setDisable(false);
    refreshButton.setText("Refresh");
    

    into a Runnable which is passed to Platform.runLater:

    Platform.runLater(() -> {
        playerButton.setDisable(false);
        refreshButton.setDisable(false);
        refreshButton.setText("Refresh");
    });
    

    Note that changes to your TurnPolling.closedGame field in one thread may not be visible in another thread, unless it’s declared volatile. From the Java Language Specification:

    For example, in the following (broken) code fragment, assume that this.done is a non-volatile boolean field:

    while (!this.done)
        Thread.sleep(1000);
    

    The compiler is free to read the field this.done just once, and reuse the cached value in each execution of the loop. This would mean that the loop would never terminate, even if another thread changed the value of this.done.

    Using Task and Service

    JavaFX provides a cleaner solution to all this: Task and Service.

    A Service creates Tasks. A Service has a bindable value property, which is always equal to the value of the most recently created Task. You can bind your button properties to the Service’s value property:

    int user_id = 0;
    
    Service<Boolean> turnPollService = new Service<Boolean>() {
        @Override
        protected Task<Boolean> createTask() {
            return new Task<Boolean>() {
                @Override
                protected Boolean call()
                throws InterruptedException {
    
                    updateValue(true);
    
                    String gamePin = Context.getContext().getGamePin();
    
                    while (!GameConnection.yourTurn(user_id, gamePin)) {
                        Thread.sleep(5000);
    
                        if (TurnPolling.closedGame){
                            break;
                        }
                    }
    
                    return false;
                }
            };
        }
    };
    
    playerButton.disableProperty().bind(turnPollService.valueProperty());
    refreshButton.disableProperty().bind(turnPollService.valueProperty());
    
    refreshButton.textProperty().bind(
        Bindings.when(
            turnPollService.valueProperty().isEqualTo(true))
            .then("Waiting for your turn\u2026")
            .otherwise("Refresh"));
    

    When the player’s turn is finished, you would call turnPollService.restart();.

    Whether you use a Service, or just use Platform.runLater, you still need to make TurnPolling.closedGame thread-safe, either by making it volatile, or by enclosing all accesses to it in synchronized blocks (or Lock guards).