【问题标题】:How do I change the speed of an object moved by keyboard input inside an AnimationTimer?如何更改 AnimationTimer 内由键盘输入移动的对象的速度?
【发布时间】:2021-12-04 20:24:34
【问题描述】:

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

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

我希望能够保持每帧最多 0.1 的翻译流畅度,而且还能够更改每次触发按键时应该移动多少空间。

下面是对问题的描述:

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

【问题讨论】:

标签: java animation javafx event-handling


【解决方案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: ;
        }
    }
}

【讨论】:

  • 此方法的一个通用示例是此key listener question 的答案中输入处理程序中的活动键映射。
【解决方案2】:

您没有正确使用动画计时器。 您的关键代码处理应该用于设置对象的速度。释放键时将速度设置为 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。) 基本上,您希望根据为动画计时器的刻度之间的间隔调整的速度矢量来更新位置。

【讨论】:

    猜你喜欢
    • 2015-07-20
    • 2018-10-24
    • 1970-01-01
    • 2015-11-23
    • 1970-01-01
    • 2016-12-17
    • 2023-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多