JavaFX has an AnimationTimer, but that deals with nanoseconds so it seems more appropriate for pure UI constructs. In this post, I've opted to use the standard Java SE timer -- java.util.Timer - because it allows me to separate my business logic (a POJO called GameEngine) from my presentation (a JavaFX Controller called MainViewController).
This app keeps an elapsed time that begins counting when the Start button is pressed and stops when the Finish button is pressed or the quiz completes. There is a JavaFX Label in the lower part of the screen showing 00:29 for 29 elapsed seconds.
A JavaFX App with a Label Showing Elapsed Time |
App Design
This UML diagram shows two collaborating classes: a JavaFX Controller called MainViewController and a POJO called GameEngine. The MainViewController manages all of the JavaFX Controls and Events in the app while GameEngine is a UI-neutral class that manages the data and logic in the app.UML Diagram Showing Presentation and Business Logic |
I made the design decision to make the Timer a part of the GameEngine rather than the MainViewController. That's so the UI-neutral functionality of GameEngine can be accessed in other non-JavaFX ways such as a web service, a web app, or a JUnit test. This would be preferred to passing JavaFX handles into the business logic which would force a dependency on JavaFX.
Business Delegate Digression
Sometimes, I will do such a thing. As a quick refactor to shrink a large JavaFX Controller or to reuse code in another JavaFX area, I pull out chunks of the JavaFX code which includes access to it's @FXML members. This isn't optimal however because of the coupling. The projects I work on will append the name "BusinessDelegate" to the classes that are conceived in this way. Team members know that the BusinessDelegate, while usable in other JavaFX areas, is inherently tied to the presentation.GameEngine, though, can stand on its own.
Timer Code
GameEngine manages the Timer because the timing operation is important to the gameplay. Your score is determined by the number of correct responses computed with the time it took to form those responses. Referring to the UML diagram in the previous section, GameEngine has a Timer member, an elapsedTime variable, and a Consumer member which is a callback provided to GameEngine for updating the UI. Here is GameEngine's constructor.
public GameEngine(GameModule gameModule, Consumer<Integer> timer_cb) {
this.gameModule = gameModule;
this.timer_cb = timer_cb;
timer = new Timer();
}
}
This creates a new Timer. Note that the Timer is not started and there are no scheduled tasks.
The Timer is started when the Start button is pressed. A scheduled task is created. Some non-Timer code has been removed for brevity.
The TimerTask object is called immediately and once every 1,000 milliseconds thereafter. The overridden run method increments the elapsedTime variable which will be used for a final scoring and presentation when the game is finished. timer_cb is also invoked. Because it may update
public void start() {
elapsedTime = 0;
timer.schedule(new TimerTask() {
@Override
public void run() {
elapsedTime++;
if( timer_cb != null ) {
timer_cb.accept(elapsedTime);
}}}, 0, 1000);
Calling Code
Although GameEngine is free from JavaFX code, I still want JavaFX interaction from my Timer. This comes from passing a callback to GameEngine. This block of code is executed in the @FXML initialize() method of the JavaFX Controller, MainViewController.An pair of Lambda Expressions are used in this block. The timer_cb member is set based on the "nsecs -> ..." outer Lambda Expression. It will updated timerLabel with setText() based on the passed-in nsecs formatted by formatSeconds. A second Lambda Expression is used in the Platform.runLater() method which is crucial.
Because the Timer involves another Thread, you'll get an error trying the setText() directly from GameEngine. Wrapping the call with runLater fixes this.
engine = new GameEngine(new GameModule(),
nsecs ->
Platform.runLater( () -> {timerLabel.setText( formatSeconds(nsecs) );
}));
And formatSeconds is a number formatting method.
private NumberFormat twoDigitFormat = new DecimalFormat("00");
public
String formatSeconds(int seconds)
long minutes = seconds / 60;
long secondsRemainder = seconds - (minutes * 60);return twoDigitFormat.format(minutes) + ":" + twoDigitFormat.format(secondsRemainder);
}
While you can have your timer code directly manipulate your JavaFX Controls, separating the timer code from a Controller allows the code to be reused in non-JavaFX settings. Unit testing is also simpler because you don't have to mock UI components; pass in a no-op timer callback to verify that a callback is called when the Timer fires. I also preferred this technique to working with AnimationTimer which deals in nanoseconds and may not let me capture a specific nanosecond value (say exactly one second).
No comments:
Post a Comment