Issue
I have this JavaFX Circle
, which moves according to keyboard's arrows.
All the AnimationTimer
does is refreshing the circle position every frame.
I found a movement of 0.1
every time a KeyEvent
is triggered to be smooth enough for the animation, however it moves really slow. On the other hand if I change the movement to let's say 1.0
or 10.0
, it's undoubtedly faster, but also much more choppy (you can clearly see it starts moving by discrete values).
I want to be able to keep the smoothness of translating at most 0.1
per frame, but also be able to change how much space it should move every time a key is triggered.
Below is an mre describing the problem:
public class MainFX extends Application {
private double playerX;
private double playerY;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
AnchorPane pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
playerX = pane.getWidth()/2;
playerY = pane.getHeight()/2;
Circle player = new Circle(playerX,playerY,10);
pane.getChildren().add(player);
scene.addEventHandler(KeyEvent.KEY_PRESSED, this::animate);
AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long now) {
player.setCenterX(playerX);
player.setCenterY(playerY);
}
};
timer.start();
}
private void animate(KeyEvent key){
if (key.getCode() == KeyCode.UP) {
playerY-=0.1;
}
if (key.getCode() == KeyCode.DOWN) {
playerY+=0.1;
}
if (key.getCode() == KeyCode.RIGHT) {
playerX+=0.1;
}
if (key.getCode() == KeyCode.LEFT) {
playerX-=0.1;
}
}
}
Solution
The AnimationTimer
's handle()
method is invoked on every frame rendering. Assuming the FX Application thread is not overwhelmed with other work, this will occur at approximately 60 frames per second. Updating the view from this method will give a relatively smooth animation.
By contrast, the key event handlers are invoked on every key press (or release, etc.) event. Typically, when a key is held down, the native system will issue repeated key press events at some rate that is system dependent (and usually user-configurable), and typically is much slower that animation frames (usually every half second or so). Changing the position of UI elements from here will result in jerky motion.
Your current code updates the position of the UI element from the playerX
and playerY
variables in the AnimationTimer
: however you only change those variable is the key event handlers. So if the AnimationTimer
is running at 60fps, and the key events are occurring every 0.5s (for example), you will "update" the UI elements 30 times with each new value, changing the actual position only two times per second.
A better approach is to use key event handlers merely to maintain the state of variables indicating if each key is pressed or not. In the AnimationTimer
, update the UI depending on the state of the keys, and the amount of time elapsed since the last update.
Here is a version of your code using this approach:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class MainFX extends Application {
private boolean leftPressed ;
private boolean rightPressed ;
private boolean upPressed ;
private boolean downPressed ;
private static final double SPEED = 100 ; // pixels/second
private static final double PLAYER_RADIUS = 10 ;
private AnchorPane pane;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
double playerX = pane.getWidth() / 2;
double playerY = pane.getHeight() / 2;
Circle player = new Circle(playerX, playerY, PLAYER_RADIUS);
pane.getChildren().add(player);
scene.addEventHandler(KeyEvent.KEY_PRESSED, this::press);
scene.addEventHandler(KeyEvent.KEY_RELEASED, this::release);
AnimationTimer timer = new AnimationTimer() {
private long lastUpdate = System.nanoTime() ;
@Override
public void handle(long now) {
double elapsedSeconds = (now - lastUpdate) / 1_000_000_000.0 ;
int deltaX = 0 ;
int deltaY = 0 ;
if (leftPressed) deltaX -= 1 ;
if (rightPressed) deltaX += 1 ;
if (upPressed) deltaY -= 1 ;
if (downPressed) deltaY += 1 ;
Point2D translationVector = new Point2D(deltaX, deltaY)
.normalize()
.multiply(SPEED * elapsedSeconds);
player.setCenterX(clampX(player.getCenterX() + translationVector.getX()));
player.setCenterY(clampY(player.getCenterY() + translationVector.getY()));
lastUpdate = now ;
}
};
timer.start();
}
private double clampX(double value) {
return clamp(value, PLAYER_RADIUS, pane.getWidth() - PLAYER_RADIUS);
}
private double clampY(double value) {
return clamp(value, PLAYER_RADIUS, pane.getHeight() - PLAYER_RADIUS);
}
private double clamp(double value, double min, double max) {
return Math.max(min, Math.min(max, value));
}
private void press(KeyEvent event) {
handle(event.getCode(), true);
}
private void release(KeyEvent event) {
handle(event.getCode(), false);
}
private void handle(KeyCode key, boolean press) {
switch(key) {
case UP: upPressed = press ; break ;
case DOWN: downPressed = press ; break ;
case LEFT: leftPressed = press ; break ;
case RIGHT: rightPressed = press ; break ;
default: ;
}
}
}
Answered By - James_D