Search code examples
javamultithreadingjavafxtimer

timer in javaFX using thread


I'm trying to code battle sea and on one of my menus I should show the client their map and give them 30s to decide whether they want a new map or the map is fine and they hit start game. if they click on the start game button the timer should stop and the scene will change. if their time is up it is just like they have clicked on start game button and after time out scene should change automatically. if they click on new map button I should give them remaining time + 10 to decide again. I did some coding but I can't do the remaining+10 part and I don't know how to stop the thread. this is my FXML controller of the scene where timer should be in. the drawMap function isn't important here.

public class StandbyMapGuiController implements Initializable {
    @FXML
    private volatile Label timerLabel;
    @FXML
    private GridPane sea;

    private static Stage stage;
    private static int time;
    private GameTimer gameTimer;
    private CountDown countDown;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        drawMap();
        gameTimer = new GameTimer(time,timerLabel , this);
        gameTimer.countDown();
       
    }

    public void updateTimer(int newTime){
        timerLabel.setText(String.valueOf(newTime));
    }
    public void drawMap(){
        for (int i = 0 ; i < 10 ; i++){
            for (int j = 0 ; j < 10 ; j++){
                MapButton btn = new MapButton(i,j);
                btn.setManner(MapButton.COLOR.VIOLET);
                sea.add(btn,j,i);
            }
        }

    }

    public void changeMap(ActionEvent actionEvent) {
        int remaining = Integer.parseInt(timerLabel.getText())+10;
        System.out.println(remaining);
        setTime(remaining);
//restart the page
        Toolbar.getInstance().changeScene(ConfigLoader.readProperty("standbyMapMenuAdd"), actionEvent);
     
    }

    public void startGame() {
        //todo: tell server the gamer is ready
        timerLabel.setText("Time's up");
        try {
            Parent root;
            root = FXMLLoader.load(getClass().getClassLoader().getResource("FXMLs/GameBoard.fxml"));
            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.show();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println(e.getMessage());
        }
    }
}

and this is my GameTimer class:



public class GameTimer {
    private Timer timer;
    private TimerTask task;
    private int time;
    private volatile Label label;

    public GameTimer(int time, Label label, StandbyMapGuiController controller) {
        this.time = time;
        this.label = label;
        timer = new Timer();
        task = new TimerTask() {
            int counter = time;
            boolean timeOut = false;

public int getCounter() {
                return counter;
            }

            public void setCounter(int counter) {
                this.counter = counter;
            }

            public boolean isTimeOut() {
                return timeOut;
            }

            public void setTimeOut(boolean timeOut) {
                this.timeOut = timeOut;
            }

            @Override
            public void run() {
                Platform.runLater(() -> {
                    if (counter > 0) {
                        label.setText(String.valueOf(counter));
                        counter--;
                    } else {
                        timeOut = true;
                        controller.startGame();
                        timer.cancel();
                    }
                });
            }
        };
    }

    public void countDown() {
        timer.scheduleAtFixedRate(task, 0, 1000);
    }
}

I can't access the timeOut and counter for setting their values or getting them.(the getters and setters in TimerTask thread doesn't work)


Solution

  • Don't use a thread for this. The better option is to use an animation. Animations are asynchronous but execute on the JavaFX Application Thread. For example, you could use a PauseTransition:

    import javafx.animation.PauseTransition;
    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.beans.binding.Bindings;
    import javafx.geometry.Insets;
    import javafx.geometry.Pos;
    import javafx.scene.Scene;
    import javafx.scene.control.Alert;
    import javafx.scene.control.Alert.AlertType;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.control.ProgressBar;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    public class App extends Application {
    
      @Override
      public void start(Stage primaryStage) {
        PauseTransition timer = new PauseTransition(Duration.seconds(30));
        timer.setOnFinished(
            e -> {
              Alert alert = new Alert(AlertType.INFORMATION);
              alert.initOwner(primaryStage);
              alert.setHeaderText(null);
              alert.setContentText("You took too long! Will now exit application.");
              alert.setOnHidden(we -> Platform.exit());
              alert.show();
            });
    
        Button button = new Button("Add ten seconds");
        button.setOnAction(
            e -> {
              e.consume();
              timer.jumpTo(timer.getCurrentTime().subtract(Duration.seconds(10)));
            });
    
        Label timerLabel = new Label();
        timerLabel
            .textProperty()
            .bind(
                Bindings.createStringBinding(
                    () -> {
                      Duration currentTime = timer.getCurrentTime();
                      Duration duration = timer.getDuration();
                      double timeRemaining = duration.subtract(currentTime).toSeconds();
                      return String.format("%04.1f seconds remaining", timeRemaining);
                    },
                    timer.currentTimeProperty(),
                    timer.durationProperty()));
    
        ProgressBar timerProgress = new ProgressBar();
        timerProgress.setMaxWidth(Double.MAX_VALUE);
        timerProgress
            .progressProperty()
            .bind(
                Bindings.createDoubleBinding(
                    () -> {
                      double currentTime = timer.getCurrentTime().toMillis();
                      double duration = timer.getDuration().toMillis();
                      return 1.0 - (currentTime / duration);
                    },
                    timer.currentTimeProperty(),
                    timer.durationProperty()));
    
        StackPane root = new StackPane(button, timerLabel, timerProgress);
        root.setPadding(new Insets(10));
        StackPane.setAlignment(timerLabel, Pos.TOP_LEFT);
        StackPane.setAlignment(timerProgress, Pos.BOTTOM_CENTER);
        primaryStage.setScene(new Scene(root, 600, 400));
        primaryStage.show();
    
        timer.playFromStart();
      }
    }
    

    The PauseTransition effectively counts from zero to its duration (30 seconds, in this case). That's why the label and progress bar "reverse" the time value to give time remaining instead of time elapsed.

    Note the jumpTo method will never go below zero or above the duration. In other words, if the user presses the button with 25 seconds remaining the time will increase to 30 seconds remaining. If you would rather the time increase to 35 seconds remaining in that scenario then change:

    timer.jumpTo(timer.getCurrentTime().subtract(Duration.seconds(10)));
    

    To:

    timer.pause();
    Duration currentTime = timer.getCurrentTime();
    Duration duration = timer.getDuration();
    timer.setDuration(duration.subtract(currentTime).add(Duration.seconds(10)));
    timer.playFromStart();