Home > Blockchain >  How do I change the speed of an object moved by keyboard input inside an AnimationTimer?
How do I change the speed of an object moved by keyboard input inside an AnimationTimer?

Time:10-18

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;
        }
    }
}

CodePudding user response:

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.

CodePudding user response:

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: ;
        }
    }
}
  • Related