2

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;
        }
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • 2
    You only actually change the position in the key handler, so the player only moves on each keyboard event. These are basically keyboard repeats that are relatively far apart (and controlled by the OS). A better approach is to define variables representing the speed of the player, which you can modify in `KEY_PRESSED` and `KEY_RELEASED` events. In the animation timer, you can use the `now` variable to compute how much time has elapsed since the last update, and update the position accordingly. – James_D Oct 17 '21 at 14:53
  • 2
    Some similar Q/A: https://stackoverflow.com/questions/29962395/how-to-write-a-keylistener-for-javafx https://stackoverflow.com/questions/21331519/how-to-get-smooth-animation-with-keypress-event-in-javafx https://stackoverflow.com/questions/28749737/javafx-key-interruptions – James_D Oct 17 '21 at 14:58
  • 2
    https://stackoverflow.com/a/5199636/8556269 i think this could help, the speed of keypressed events is os and/or frame rate dependent so you can't really depend on it if you need consistent resulys6 – SDIDSA Oct 17 '21 at 15:00

2 Answers2

3

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: ;
        }
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • 1
    A generic example is of this approach is the active key map in the input handler in the answer to this [key listener question](https://stackoverflow.com/questions/29962395/how-to-write-a-keylistener-for-javafx). – jewelsea Jan 27 '22 at 04:10
2

You aren't using the animation timer properly. Your key code processing should be used to set a velocity for the object. Set the velocity to 0 when the key is released. In the animation timer code, change the position based on the elapsed time and the velocity.

The timer will fire events at the frame rate - likely around 60fps. If you want smooth motion you want to adjust the position on every frame. Instead you are using it to set the position to a pre-computed value. It isn't doing anything useful that way. You could just as easily set the position in the key processing code and get the same effect you are getting now.

If you don't want to have the user hold down the keys to move. That is, you want to tap once and have the object move by 10.0. You can set the target position in the key processing code. Jumping the target position by 10 at a time. Then have the animation timer move the current position towards the target position at an appropriate velocity, stopping when the target position is reached.

Maybe something like this:

    AnimationTimer timer = new AnimationTimer() {
        @Override
        public void handle(long now) {
            double curX = player.getCenterX();
            double curY = player.getCenterY();
            double diffX = playerX-curX;
            double diffY = playerY-curY;
            if (diffX > 1.0) {
                curX += 1.0;
            else if (diffX < -1.0) {
                curX -= 1.0;
            } else {
                curX = playerX;
            }
            if (diffY > 1.0) {
                curY += 1.0;
            else if (diffY < -1.0) {
                curY -= 1.0;
            } else {
                curY = playerY;
            }
            player.setCenterX(curX);
            player.setCenterY(curY);
        }
    };

That's a primitive example... and note that it will make diagonal movements that go faster than axis-aligned movements. (The velocity vector magnitude for diagonal movements is sqrt(2) in that example instead of 1.) Basically you want to update the position based on a velocity vector adjusted for the interval between ticks of the animation timer.

swpalmer
  • 3,890
  • 2
  • 23
  • 31