如何更改 AnimationTimer 中通过键盘输入移动的对象的速度?

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

我有这个 JavaFX Circle,它根据键盘的箭头移动。 AnimationTimer 所做的只是每帧刷新圆圈位置。

我发现每次触发 KeyEvent 时都会有一个 0.1 的移动,对于动画来说足够平滑,但它移动得非常慢。另一方面,如果我将移动更改为 1.010.0,它无疑会更快,但也会更加不稳定(您可以清楚地看到它开始以离散值移动)。

我希望能够保持每帧最多 0.1 平移的流畅性,但也能够更改每次触发键时移动 space 的幅度。

下面是对问题的描述:

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

您没有正确使用动画计时器。 您的关键代码处理应该用于设置对象的速度。释放键时将速度设置为 0。 在动画计时器代码中,根据经过的时间和速度更改位置。

计时器将以帧速率触发事件 - 可能在 60fps 左右。如果你想要平滑的运动,你想要调整每一帧的位置。相反,您使用它来将位置设置为预先计算的值。它没有做任何有用的事情。您可以轻松地在关键处理代码中设置位置并获得与现在相同的效果。

如果您不想让用户按住按键移动。也就是说,您想要点击一次并让对象移动 10.0。您可以在按键处理代码中设置目标位置。一次跳跃目标位置 10 个。然后让动画计时器以适当的速度将当前位置移向目标位置,到达目标位置时停止。

也许是这样的:

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

这是一个原始示例...请注意,它会使对角线运动比轴对齐运动更快。 (在该示例中,对角线运动的速度矢量大小为 sqrt(2) 而不是 1。) 基本上你想根据为动画计时器的滴答之间的间隔调整的速度矢量更新位置。

AnimationTimerhandle() 方法在每一帧渲染时被调用。假设 FX 应用程序线程没有被其他工作淹没,这将以大约每秒 60 帧的速度发生。从这个方法更新视图会得到一个相对流畅的动画。

相比之下,按键事件处理程序在每次按键(或释放等)事件时被调用。通常,当按下某个键时,本机系统将以系统相关的某种速率(通常是用户可配置的)发出重复的按键事件,并且通常比动画帧慢得多(通常每半秒左右)。从此处更改 UI 个元素的位置将导致抖动。

您当前的代码从 AnimationTimer 中的 playerXplayerY 变量更新 UI 元素的位置:但是您只更改这些变量是关键事件处理程序。因此,如果 AnimationTimer 在 60fps 时是 运行,并且按键事件每 0.5 秒发生一次(例如),您将使用每个新值“更新”UI 元素 30 次,每秒仅更改实际位置两次。

更好的方法是仅使用按键事件处理程序来维护变量的状态,以指示每个按键是否被按下。在 AnimationTimer 中,根据键的状态和自上次更新以来经过的时间更新 UI。

这是使用此方法的代码版本:

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