英文:
Input lag when changing layoutX & layoutY on an ImageView using keyboard input
问题
我的用例是这样的:
给定一个ImageView元素,通过使用键盘的上/下/左/右按钮将其在Pane中移动。
ImageView应该在不同的图像之间循环,并且当检测到移动时,源图像将发生变化。
我使用了两个不同的AnimateTimers来实现。第一个用于“循环”一个图像列表,并在检测到移动时更新给定的列表。
public class MultipleImageListAnimation extends AbstractAnimationTimer {
@Getter
private final SimpleObjectProperty<Image> imageProp;
@Getter
private final SimpleIntegerProperty scaleProp;
private volatile List<List<Image>> spriteImagesLists;
private volatile List<Image> displayedImageList;
private int imageIndex = 0;
private int listIndex = 0;
public MultipleImageListAnimation(List<List<Image>> spriteImagesLists) {
super(1);
this.imageProp = new SimpleObjectProperty<>();
this.scaleProp = new SimpleIntegerProperty(1);
this.spriteImagesLists = spriteImagesLists;
this.displayedImageList = spriteImagesLists.get(0);
if (displayedImageList.size() > 0) {
super.setFramesPerSecond(displayedImageList.size() * 2);
}
}
public void changeSpriteImages(List<List<Image>> images) {
this.stop();
this.spriteImagesLists = images;
this.listIndex = ThreadLocalRandom.current().nextInt(images.size());
this.imageIndex = 0;
this.displayedImageList = images.get(listIndex);
super.setFramesPerSecond(displayedImageList.size() * 2);
this.start();
}
public void setScaleProp(int angle) {
scaleProp.set(angle);
}
@Override
public void handle(long now) {
// shouldRun() is a typical FPS method
if (shouldRun(now)) {
Image image = displayedImageList.get(imageIndex);
imageProp.set(image);
if (++imageIndex >= displayedImageList.size()) {
listIndex = ThreadLocalRandom.current().nextInt(spriteImagesLists.size());
displayedImageList = spriteImagesLists.get(listIndex);
imageIndex = 0;
super.setFramesPerSecond(spriteImagesLists.get(listIndex).size() * 2);
}
}
}
}
第二个AnimateTimer用于更新图像的layoutX和layoutY属性。
new AnimationTimer() {
@Override
public void handle(long now) {
int xDelta = playerMovementListener.getXDeltaAndReset();
if (xDelta != 0) {
playerImageView.setLayoutX(playerImageView.getLayoutX() + xDelta);
}
int yDelta = playerMovementListener.getYDeltaAndReset();
if (yDelta != 0) {
playerImageView.setLayoutY(playerImageView.getLayoutY() + yDelta);
}
}
}.start();
这些是通过下面的EventHandler进行更新的。
public class PlayerMovementListener implements EventHandler<KeyEvent> {
private final GameViewManager gameViewManager;
// Using a Set of keys because I want to detect diagonal movement as well such as UP + LEFT etc.
private final Set<KeyCode> keysPressed;
private final AtomicInteger xDeltaCounter;
private final AtomicInteger yDeltaCounter;
public PlayerMovementListener(GameViewManager gameViewManager) {
this.gameViewManager = gameViewManager;
this.keysPressed = new HashSet<>();
this.xDeltaCounter = new AtomicInteger(0);
this.yDeltaCounter = new AtomicInteger(0);
}
public int getXDeltaAndReset() {
return xDeltaCounter.getAndSet(0);
}
public int getYDeltaAndReset() {
return yDeltaCounter.getAndSet(0);
}
@Override
public void handle(KeyEvent keyEvent) {
if (KeyEvent.KEY_RELEASED.equals(keyEvent.getEventType())) {
keysPressed.remove(keyEvent.getCode());
// Stopping movement on any key released
// This is fine for now.
gameViewManager.stopPlayerMoving();
} else if (KeyEvent.KEY_PRESSED.equals(keyEvent.getEventType())) {
keysPressed.add(keyEvent.getCode());
if (KeyCode.RIGHT.equals(keyEvent.getCode())) {
gameViewManager.rotatePlayerAnimation(1);
}
if (KeyCode.LEFT.equals(keyEvent.getCode())) {
gameViewManager.rotatePlayerAnimation(-1);
}
if (Arrays.asList(KeyCode.UP, KeyCode.DOWN, KeyCode.RIGHT, KeyCode.LEFT).contains(keyEvent.getCode())) {
yDeltaCounter.addAndGet(MovementInput.getLayoutYDelta(keysPressed));
xDeltaCounter.addAndGet(MovementInput.getLayoutXDelta(keysPressed));
gameViewManager.startPlayerMoving();
}
}
}
}
以及GameViewManager
的相关方法
public class GameViewManager {
private AnchorPane gamePane = ...;
private PlayerEntity playerEntity = ...;
private ImageView playerImageView;
private MultipleImageListAnimation playerAnimationTimer;
private boolean isPlayerMoving = false;
public void startPlayerMoving() {
Platform.runLater(() -> {
if (!isPlayerMoving) {
isPlayerMoving = true;
playerAnimationTimer.changeSpriteImages(List.of(playerEntity.getRunImages()));
}
});
}
public void stopPlayerMoving() {
Platform.runLater(() -> {
if (isPlayerMoving) {
isPlayerMoving = false;
playerAnimationTimer.changeSpriteImages(List.of(playerEntity.getIdleImages()));
}
});
}
public void rotatePlayerAnimation(int angle) {
playerAnimationTimer.setScaleProp(angle);
}
private void initializeKeyboardListeners() {
PlayerMovementListener playerMovementListener = new PlayerMovementListener(this);
gameScene.setOnKeyPressed(playerMovementListener);
gameScene.setOnKeyReleased(playerMovementListener);
new AnimationTimer() {
@Override
public void handle(long now) {
// This is executing constantly, I know of the implications but it's good for now
int xDelta = playerMovementListener.getXDeltaAndReset();
if (xDelta != 0) {
playerImageView.setLayoutX(playerImageView.getLayoutX() + xDelta);
}
int yDelta = playerMovementListener.getYDeltaAndReset();
if (yDelta != 0) {
playerImageView.setLayoutY(playerImageView.getLayoutY() + yDelta);
}
}
}.start();
}
private void initializePlayerImageView() {
this.playerImageView = new ImageView();
playerImageView.setLayoutX(STARTING_WIDTH);
playerImageView.setLayoutY(STARTING_HEIGHT);
playerAnimationTimer = new MultipleImageListAnimation(Arrays.asList(playerEntity.getIdleImages()));
playerAnimationTimer.start();
playerImageView.imageProperty().bind(playerAnimationTimer.getImageProp());
playerImageView.scaleXProperty().bind(playerAnimationTimer.getScaleProp());
gamePane.getChildren().add(this.playerImageView);
}
}
现在这种方法对我的用例来说工作得很好。然而,我注意到当按下输入键时,直到视图实际移动时有一个非常轻微但明显的“延迟”。我可以看到图像立即循环到“运行”状态,但精灵直到稍后才移动。一旦移动,处理输入时就不再有延迟。释放键并按下另一个键会在移动开始时显示相同的延迟。您可以在此处查看此行为的录制。我不知道是什么导致了这个问题,以及如何修复它,所以希望能得到任何意见。
编辑:AbstractAnimationTimer类
public abstract class AbstractAnimationTimer extends AnimationTimer {
public long now;
protected long FPS_BREAKPOINT;
public AbstractAnimationTimer(int framesPerSecond) {
this.now = 0;
this.FPS_BREAKPOINT = 1_000_000_000 / framesPerSecond;
}
protected void setFramesPerSecond(int framesPerSecond) {
this.FPS_BREAKPOINT = 1_000_000_000 / framesPerSecond;
}
public boolean shouldRun(long now) {
if (now - this.now > this.FPS_BREAKPOINT) {
this.now = now;
return true;
}
return false;
}
}
编辑2:可重现的示例
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class Main extends Application {
private final Set<KeyCode> keysPressed = new HashSet<>();
private final AtomicInteger yDeltaCounter = new AtomicInteger(0);
private final AtomicInteger xDeltaCounter = new AtomicInteger(0);
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
AnchorPane pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 800);
scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent keyEvent) {
keysPressed.clear();
}
});
scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent keyEvent) {
if (KeyEvent.KEY_PRESSED.equals(keyEvent.getEventType())) {
keysPressed.add(keyEvent.getCode());
if (Arrays.asList(KeyCode.UP, KeyCode.DOWN, KeyCode.RIGHT, KeyCode.LEFT).contains(keyEvent.getCode())) {
yDeltaCounter.addAndGet(getLayoutYDelta(keysPressed));
xDeltaCounter.addAndGet(getLayoutXDelta(keysPressed));
}
}
}
});
primaryStage = new Stage();
primaryStage.setScene(scene);
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.RED);
new AnimationTimer() {
@Override
public void handle(long now) {
// This is executing constantly, I know of the implications but it's good for now
int xDelta = xDeltaCounter.getAndSet(0);
if (xDelta != 0) {
rect.setLayoutX(rect.getLayoutX() + xDelta);
}
int yDelta = yDeltaCounter.getAndSet(0);
if (yDelta != 0) {
rect.setLayoutY(rect.getLayoutY() + yDelta);
}
}
}.start();
pane.getChildren().add(rect);
primaryStage.show();
}
public static int getLayoutXDelta(Set<KeyCode> keysPressed) {
int xDelta = 0;
if (keysPressed.isEmpty()) {
return xDelta;
}
if (keysPressed.contains(KeyCode.RIGHT)) {
xDelta += 10;
}
if (keysPressed.contains(KeyCode.LEFT)) {
xDelta -= 10;
}
return xDelta;
}
public static int getLayoutYDelta(Set<KeyCode> keysPressed) {
int yDelta = 0;
if (keysPressed.isEmpty()) {
return yDelta;
}
if (keysPressed.contains(KeyCode.UP)) {
yDelta -= 10;
}
if (keysPressed.contains(KeyCode.DOWN)) {
yDelta += 10;
}
return yDelta;
}
}
英文:
My use case is the following:
Given an ImageView element, move that around the Pane by using keyboard UP/DOWN/LEFT/RIGHT buttons.
The ImageView should by cycling between different images and the source images
will change when movement is detected.
I am using two different AnimateTimers for that. The first one is used to "cycle" through a list of Images and update given list when movement is detected.
public class MultipleImageListAnimation extends AbstractAnimationTimer {
@Getter
private final SimpleObjectProperty<Image> imageProp;
@Getter
private final SimpleIntegerProperty scaleProp;
private volatile List<List<Image>> spriteImagesLists;
private volatile List<Image> displayedImageList;
private int imageIndex = 0;
private int listIndex = 0;
public MultipleImageListAnimation(List<List<Image>> spriteImagesLists) {
super(1);
this.imageProp = new SimpleObjectProperty<>();
this.scaleProp = new SimpleIntegerProperty(1);
this.spriteImagesLists = spriteImagesLists;
this.displayedImageList = spriteImagesLists.get(0);
if (displayedImageList.size() > 0) {
super.setFramesPerSecond(displayedImageList.size() * 2);
}
}
public void changeSpriteImages(List<List<Image>> images) {
this.stop();
this.spriteImagesLists = images;
this.listIndex = ThreadLocalRandom.current().nextInt(images.size());
this.imageIndex = 0;
this.displayedImageList = images.get(listIndex);
super.setFramesPerSecond(displayedImageList.size() * 2);
this.start();
}
public void setScaleProp(int angle) {
scaleProp.set(angle);
}
@Override
public void handle(long now) {
// shouldRun() is a typical FPS method
if (shouldRun(now)) {
Image image = displayedImageList.get(imageIndex);
imageProp.set(image);
if (++imageIndex >= displayedImageList.size()) {
listIndex = ThreadLocalRandom.current().nextInt(spriteImagesLists.size());
displayedImageList = spriteImagesLists.get(listIndex);
imageIndex = 0;
super.setFramesPerSecond(spriteImagesLists.get(listIndex).size() * 2);
}
}
}
}
The second one is updating the layoutX & layoutY properties of the image.
new AnimationTimer() {
@Override
public void handle(long now) {
int xDelta = playerMovementListener.getXDeltaAndReset();
if (xDelta != 0) {
playerImageView.setLayoutX(playerImageView.getLayoutX() + xDelta);
}
int yDelta = playerMovementListener.getYDeltaAndReset();
if (yDelta != 0) {
playerImageView.setLayoutY(playerImageView.getLayoutY() + yDelta);
}
}
}.start();
Those are updated through an EventHandler as shown below
public class PlayerMovementListener implements EventHandler<KeyEvent> {
private final GameViewManager gameViewManager;
// Using a Set of keys because I want to detect diagonal movement as well such as UP + LEFT etc.
private final Set<KeyCode> keysPressed;
private final AtomicInteger xDeltaCounter;
private final AtomicInteger yDeltaCounter;
public PlayerMovementListener(GameViewManager gameViewManager) {
this.gameViewManager = gameViewManager;
this.keysPressed = new HashSet<>();
this.xDeltaCounter = new AtomicInteger(0);
this.yDeltaCounter = new AtomicInteger(0);
}
public int getXDeltaAndReset() {
return xDeltaCounter.getAndSet(0);
}
public int getYDeltaAndReset() {
return yDeltaCounter.getAndSet(0);
}
@Override
public void handle(KeyEvent keyEvent) {
if (KeyEvent.KEY_RELEASED.equals(keyEvent.getEventType())) {
keysPressed.remove(keyEvent.getCode());
// Stopping movement on any key released
// This is fine for now.
gameViewManager.stopPlayerMoving();
} else if (KeyEvent.KEY_PRESSED.equals(keyEvent.getEventType())) {
keysPressed.add(keyEvent.getCode());
if (KeyCode.RIGHT.equals(keyEvent.getCode())) {
gameViewManager.rotatePlayerAnimation(1);
}
if (KeyCode.LEFT.equals(keyEvent.getCode())) {
gameViewManager.rotatePlayerAnimation(-1);
}
if (Arrays.asList(KeyCode.UP, KeyCode.DOWN, KeyCode.RIGHT, KeyCode.LEFT).contains(keyEvent.getCode())) {
yDeltaCounter.addAndGet(MovementInput.getLayoutYDelta(keysPressed));
xDeltaCounter.addAndGet(MovementInput.getLayoutXDelta(keysPressed));
gameViewManager.startPlayerMoving();
}
}
}
}
And the relevant methods of the GameViewManager
public class GameViewManager {
private AnchorPane gamePane = ...;
private PlayerEntity playerEntity = ...;
private ImageView playerImageView;
private MultipleImageListAnimation playerAnimationTimer;
private boolean isPlayerMoving = false;
public void startPlayerMoving() {
Platform.runLater(() -> {
if (!isPlayerMoving) {
isPlayerMoving = true;
playerAnimationTimer.changeSpriteImages(List.of(playerEntity.getRunImages()));
}
});
}
public void stopPlayerMoving() {
Platform.runLater(() -> {
if (isPlayerMoving) {
isPlayerMoving = false;
playerAnimationTimer.changeSpriteImages(List.of(playerEntity.getIdleImages()));
}
});
}
public void rotatePlayerAnimation(int angle) {
playerAnimationTimer.setScaleProp(angle);
}
private void initializeKeyboardListeners() {
PlayerMovementListener playerMovementListener = new PlayerMovementListener(this);
gameScene.setOnKeyPressed(playerMovementListener);
gameScene.setOnKeyReleased(playerMovementListener);
new AnimationTimer() {
@Override
public void handle(long now) {
// This is executing constantly, I know of the implications but it's good for now
int xDelta = playerMovementListener.getXDeltaAndReset();
if (xDelta != 0) {
playerImageView.setLayoutX(playerImageView.getLayoutX() + xDelta);
}
int yDelta = playerMovementListener.getYDeltaAndReset();
if (yDelta != 0) {
playerImageView.setLayoutY(playerImageView.getLayoutY() + yDelta);
}
}
}.start();
}
private void initializePlayerImageView() {
this.playerImageView = new ImageView();
playerImageView.setLayoutX(STARTING_WIDTH);
playerImageView.setLayoutY(STARTING_HEIGHT);
playerAnimationTimer = new MultipleImageListAnimation(Arrays.asList(playerEntity.getIdleImages()));
playerAnimationTimer.start();
playerImageView.imageProperty().bind(playerAnimationTimer.getImageProp());
playerImageView.scaleXProperty().bind(playerAnimationTimer.getScaleProp());
gamePane.getChildren().add(this.playerImageView);
}
}
Now this approach is working fine for my use case. However I can't help but notice that there is a very slight but noticeable "lag" when an input key is pressed and until the View actually moves. I can see that the images are cycled to the "running" state immediately but the sprite is not moving until later. Once it does there is no more lagging on processing the input. Releasing the key and pressing another one displays the same lag at the start of movement. You can see a recording of this behavior here. I do not know what's causing this and how I can fix this so any input is appreciated.
Edit: The AbstractAnimationTimer class
public abstract class AbstractAnimationTimer extends AnimationTimer {
public long now;
protected long FPS_BREAKPOINT;
public AbstractAnimationTimer(int framesPerSecond) {
this.now = 0;
this.FPS_BREAKPOINT = 1_000_000_000 / framesPerSecond;
}
protected void setFramesPerSecond(int framesPerSecond) {
this.FPS_BREAKPOINT = 1_000_000_000 / framesPerSecond;
}
public boolean shouldRun(long now) {
if (now - this.now > this.FPS_BREAKPOINT) {
this.now = now;
return true;
}
return false;
}
Edit 2: Reproducible example
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
public class Main extends Application {
private final Set<KeyCode> keysPressed = new HashSet<>();
private final AtomicInteger yDeltaCounter = new AtomicInteger(0);
private final AtomicInteger xDeltaCounter = new AtomicInteger(0);
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
AnchorPane pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 800);
scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent keyEvent) {
keysPressed.clear();
}
});
scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent keyEvent) {
if (KeyEvent.KEY_PRESSED.equals(keyEvent.getEventType())) {
keysPressed.add(keyEvent.getCode());
if (Arrays.asList(KeyCode.UP, KeyCode.DOWN, KeyCode.RIGHT, KeyCode.LEFT).contains(keyEvent.getCode())) {
yDeltaCounter.addAndGet(getLayoutYDelta(keysPressed));
xDeltaCounter.addAndGet(getLayoutXDelta(keysPressed));
}
}
}
});
primaryStage = new Stage();
primaryStage.setScene(scene);
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.RED);
new AnimationTimer() {
@Override
public void handle(long now) {
// This is executing constantly, I know of the implications but it's good for now
int xDelta = xDeltaCounter.getAndSet(0);
if (xDelta != 0) {
rect.setLayoutX(rect.getLayoutX() + xDelta);
}
int yDelta = yDeltaCounter.getAndSet(0);
if (yDelta != 0) {
rect.setLayoutY(rect.getLayoutY() + yDelta);
}
}
}.start();
pane.getChildren().add(rect);
primaryStage.show();
}
public static int getLayoutXDelta(Set<KeyCode> keysPressed) {
int xDelta = 0;
if (keysPressed.isEmpty()) {
return xDelta;
}
if (keysPressed.contains(KeyCode.RIGHT)) {
xDelta += 10;
}
if (keysPressed.contains(KeyCode.LEFT)) {
xDelta -= 10;
}
return xDelta;
}
public static int getLayoutYDelta(Set<KeyCode> keysPressed) {
int yDelta = 0;
if (keysPressed.isEmpty()) {
return yDelta;
}
if (keysPressed.contains(KeyCode.UP)) {
yDelta -= 10;
}
if (keysPressed.contains(KeyCode.DOWN)) {
yDelta += 10;
}
return yDelta;
}
}
答案1
得分: 3
你的动画计时器在使用deltaX
和deltaY
时将它们重置为零:
new AnimationTimer() {
@Override
public void handle(long now) {
// 这个方法会不断执行,我知道这样做的影响,但现在先这样
int xDelta = xDeltaCounter.getAndSet(0);
if (xDelta != 0) {
rect.setLayoutX(rect.getLayoutX() + xDelta);
}
int yDelta = yDeltaCounter.getAndSet(0);
if (yDelta != 0) {
rect.setLayoutY(rect.getLayoutY() + yDelta);
}
}
}.start();
只有当键盘监听器被调用时,也就是再次按下相应的键时,它们才会被重新设置为非零值。因此,矩形每次按键时只会在适当的方向上移动10个像素。
你真正想要的是在按住键时连续移动。之所以看起来会重复移动,是因为操作系统的键盘重复功能会发送多个按键事件,如果按键被按住,就会发送多个事件。这个过程中有一个延迟,所以你会看到矩形在开始重复移动之前有一个延迟。(如果你仔细观察,你会看到按下键时会有一个孤立的移动,然后是延迟,然后是重复的移动。)你还会注意到移动不是平滑的,因为它是以10像素为单位在离散的时间点上移动的(这些时间点由操作系统确定,所以在不同的系统上移动速度会有所不同)。
正确的策略是简单地跟踪哪些键被按下,然后根据这些键计算动画计时器中的水平和垂直移动量。你可能希望根据上一帧动画之间的时间来计算移动量,以便能够准确控制速度。
注意,由于所有的操作都在同一个线程上进行,所以根本不需要使用AtomicInteger
等。
以下是一个实现了这些想法的可行解决方案:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.HashSet;
import java.util.Set;
public class Main extends Application {
private final Set<KeyCode> keysPressed = new HashSet<>();
private static final double SPEED = 200.0; // 每秒的像素数
private Point2D delta = Point2D.ZERO;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
AnchorPane pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 800);
scene.setOnKeyReleased(keyEvent -> keysPressed.remove(keyEvent.getCode()));
scene.setOnKeyPressed(keyEvent -> keysPressed.add(keyEvent.getCode()));
primaryStage = new Stage();
primaryStage.setScene(scene);
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.RED);
new AnimationTimer() {
private long lastUpdate;
@Override
public void handle(long now) {
long elapsedNanos = now - lastUpdate;
double elapsedSeconds = elapsedNanos / 1_000_000_000.0;
delta = getDelta(elapsedSeconds);
rect.setLayoutX(rect.getLayoutX() + delta.getX());
rect.setLayoutY(rect.getLayoutY() + delta.getY());
lastUpdate = now;
}
}.start();
pane.getChildren().add(rect);
primaryStage.show();
}
public Point2D getDelta(double elapsedSeconds) {
double distance = SPEED * elapsedSeconds;
int xDelta = 0;
int yDelta = 0;
if (keysPressed.contains(KeyCode.LEFT)) xDelta--;
if (keysPressed.contains(KeyCode.RIGHT)) xDelta++;
if (keysPressed.contains(KeyCode.UP)) yDelta--;
if (keysPressed.contains(KeyCode.DOWN)) yDelta++;
return new Point2D(xDelta, yDelta).normalize().multiply(distance);
}
}
只是为了演示在你原来的代码中的精灵动画(在另一个AnimationTimer
中定义)与移动(在一个AnimationTimer
中定义)是完全独立的,以下代码片段将模拟精灵动画(在矩形移动时交替改变颜色,而不是交替改变图像)。只需将以下代码添加到start()
方法中即可。
// 在移动时交替改变颜色:
new AnimationTimer() {
private long lastUpdate;
private final Color[] colors = new Color[] {Color.BLUE, Color.RED};
private int currentColor = 0;
@Override
public void handle(long now) {
if (delta.equals(Point2D.ZERO)) {
currentColor = 0;
} else {
long elapsedNanos = now - lastUpdate;
double elapsedSeconds = elapsedNanos / 1_000_000_000.0;
if (elapsedSeconds > 0.25) {
currentColor = (currentColor + 1) % colors.length;
lastUpdate = now;
}
}
rect.setFill(colors[currentColor]);
}
}.start();
英文:
Your animation timer sets deltaX
and deltaY
back to zero when it uses them:
new AnimationTimer() {
@Override
public void handle(long now) {
// This is executing constantly, I know of the implications but it's good for now
int xDelta = xDeltaCounter.getAndSet(0);
if (xDelta != 0) {
rect.setLayoutX(rect.getLayoutX() + xDelta);
}
int yDelta = yDeltaCounter.getAndSet(0);
if (yDelta != 0) {
rect.setLayoutY(rect.getLayoutY() + yDelta);
}
}
}.start();
They will only be made non-zero again when the key listener is invoked, i.e. when the appropriate keys are pressed again. Consequently, the rectangle moves 10 pixels in the appropriate direction for each key press.
What you really want is for it to move continuously while the key is held down.
The reason it appears to move repeatedly is because of the operating system's keyboard repeat, which sends multiple key press events if a key is held down. There is a delay on this, which is why you see a delay before the rectangle starts repeatedly moving. (If you observe closely, you will see one isolated movement when the key is first pressed, then the delay, then the repeated movement.) You'll also notice the movement is not smooth, because it is moving in 10 pixel increments at discrete times (which are determined by the operating system, so it will move at different speeds on different systems).
The correct strategy for this is simply to keep track of which keys are down, and then to calculate the amount of horizontal and vertical movement in the animation timer depending on those keys. You probably want to make the amount of movement dependent on the amount of time since the last animation frame, so that you can accurately control the speed.
Note that since everything is on the same thread, there is no need at all for AtomicInteger
etc.
Here is a working solution that implements these ideas:
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.HashSet;
import java.util.Set;
public class Main extends Application {
private final Set<KeyCode> keysPressed = new HashSet<>();
private static final double SPEED = 200.0 ; // pixels per second
private Point2D delta = Point2D.ZERO;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
AnchorPane pane = new AnchorPane();
Scene scene = new Scene(pane, 800, 800);
scene.setOnKeyReleased(keyEvent -> keysPressed.remove(keyEvent.getCode()));
scene.setOnKeyPressed(keyEvent -> keysPressed.add(keyEvent.getCode()));
primaryStage = new Stage();
primaryStage.setScene(scene);
Rectangle rect = new Rectangle(100, 100);
rect.setFill(Color.RED);
new AnimationTimer() {
private long lastUpdate ;
@Override
public void handle(long now) {
long elapsedNanos = now - lastUpdate;
double elapsedSeconds = elapsedNanos / 1_000_000_000.0;
delta = getDelta(elapsedSeconds);
rect.setLayoutX(rect.getLayoutX() + delta.getX());
rect.setLayoutY(rect.getLayoutY() + delta.getY());
lastUpdate = now;
}
}.start();
pane.getChildren().add(rect);
primaryStage.show();
}
public Point2D getDelta(double elapsedSeconds) {
double distance = SPEED * elapsedSeconds;
int xDelta = 0 ;
int yDelta = 0 ;
if (keysPressed.contains(KeyCode.LEFT)) xDelta--;
if (keysPressed.contains(KeyCode.RIGHT)) xDelta++;
if (keysPressed.contains(KeyCode.UP)) yDelta--;
if (keysPressed.contains(KeyCode.DOWN)) yDelta++;
return new Point2D(xDelta, yDelta).normalize().multiply(distance);
}
}
Just to demonstrate that the movement (from one AnimationTimer
) is completely independent of the sprite animation in your original (defined in another AnimationTimer
), the following snippet will mimic the sprite animation (alternating colors while the rectangle moves, instead of alternating images). Just add this in the start()
method.
// alternate colors while moving:
new AnimationTimer() {
private long lastUpdate ;
private final Color[] colors = new Color[] {Color.BLUE, Color.RED};
private int currentColor = 0 ;
@Override
public void handle(long now) {
if (delta.equals(Point2D.ZERO)) {
currentColor = 0;
} else {
long elapsedNanos = now - lastUpdate;
double elapsedSeconds = elapsedNanos / 1_000_000_000.0;
if (elapsedSeconds > 0.25) {
currentColor = (currentColor + 1) % colors.length;
lastUpdate = now;
}
}
rect.setFill(colors[currentColor]);
}
}.start();
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论