
当在javafx中使用单个`timeline`并添加具有不同持续时间的`keyframe`时,`timeline`的实际循环周期将由所有`keyframe`中最长的持续时间决定,导致所有`keyframe`以该最长周期同步触发。这使得原本预期高频率触发的`keyframe`被限制在低频率。本文将深入探讨此问题的原因,并提供两种有效的解决方案:使用多个独立的`timeline`或利用`animationtimer`,以实现不同任务的精确控制和独立执行。
JavaFX的Timeline类用于创建基于时间的动画。它通过一系列KeyFrame来定义动画在特定时间点应执行的操作。每个KeyFrame包含一个Duration(持续时间)和一个EventHandler(事件处理器)。当Timeline播放时,它会从开始时间点(通常是0)开始,并在每个KeyFrame指定的Duration到达时触发相应的事件处理器。
然而,一个常见的误解是,如果在一个Timeline中添加了多个KeyFrame,它们会按照各自的Duration独立地、并行地触发。实际上,Timeline的默认行为是将其所有KeyFrame视为一个完整动画周期的一部分。这意味着,Timeline的一个完整循环的持续时间将由其所有KeyFrame中最长的那个Duration决定。在每个周期内,所有KeyFrame都会在它们各自设定的时间点被触发一次。
例如,如果您有一个KeyFrame设置为1/120秒,另一个设置为1/60秒,还有一个设置为1秒,那么这个Timeline的完整周期将是1秒。在这1秒内:
这意味着,即使您希望某个任务每秒执行120次,但由于Timeline的周期被最长的KeyFrame(1秒)所限制,该任务实际上也只会在每秒的特定时间点被触发一次。这就是导致Timeline看起来“锁定在1fps”的原因。
立即学习“Java免费学习笔记(深入)”;
解决上述问题的最直接和推荐的方法是为每个需要独立频率执行的任务创建单独的Timeline实例。每个Timeline将只包含一个KeyFrame,其Duration设置为该任务所需的频率。这样,每个Timeline都可以独立地循环,互不影响。
以下是修改后的TickSystem类,演示了如何使用多个Timeline:
package TickSystem;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.ActionEvent;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;
import javafx.animation.Animation; // 导入 Animation 类
public class TickSystem {
private Rectangle r;
public int curFrame = 0; // 用于绘制的帧计数
public int tick = 0; // 用于更新的逻辑计数
// 定义三个独立的Timeline,分别用于更新、绘制和FPS计算
private final Timeline updateLoop;
private final Timeline drawLoop;
private final Timeline fpsLoop;
public int fps; // 实际测量的FPS
private int lastFrames = 0; // 上一秒的帧计数
public TickSystem(Rectangle r){
this.r = r;
// 定义更新任务的KeyFrame和Timeline (60次/秒)
Duration updateTime = Duration.millis(1000.0 / 60);
KeyFrame kfU = new KeyFrame(updateTime, "tickKeyUpdate", this::handleUpdate);
this.updateLoop = new Timeline(kfU); // 直接在构造Timeline时添加KeyFrame
this.updateLoop.setCycleCount(Animation.INDEFINITE); // 无限循环
// 定义绘制任务的KeyFrame和Timeline (120次/秒)
Duration drawTime = Duration.millis(1000.0 / 120);
KeyFrame kfD = new KeyFrame(drawTime, "tickKeyDraw", this::handleDraw);
this.drawLoop = new Timeline(kfD);
this.drawLoop.setCycleCount(Animation.INDEFINITE);
// 定义FPS计算任务的KeyFrame和Timeline (1次/秒)
KeyFrame kfFPS = new KeyFrame(Duration.seconds(1), "tickKeyFPS", this::handleFPS);
this.fpsLoop = new Timeline(kfFPS);
this.fpsLoop.setCycleCount(Animation.INDEFINITE);
}
/**
* 更简洁的构造函数,利用辅助方法创建Timeline
*/
public TickSystem(Rectangle r, boolean succinct) {
this.r = r;
// 使用列表管理Timelines,方便统一操作
List<Timeline> timelines = new ArrayList<>();
timelines.add(createTimeline(60, this::handleUpdate)); // 更新逻辑,60Hz
timelines.add(createTimeline(120, this::handleDraw)); // 绘制逻辑,120Hz
timelines.add(createTimeline(1, this::handleFPS)); // FPS计算,1Hz
// 将这些Timeline赋值给成员变量,以便后续控制
this.updateLoop = timelines.get(0);
this.drawLoop = timelines.get(1);
this.fpsLoop = timelines.get(2);
}
/**
* 辅助方法:创建并配置一个Timeline
* @param frequency 每秒触发次数
* @param handler 事件处理器
* @return 配置好的Timeline实例
*/
private Timeline createTimeline(int frequency, javafx.event.EventHandler<ActionEvent> handler) {
Timeline timeline = new Timeline(); // 注意:这里Timeline构造函数不再接受频率参数
timeline.getKeyFrames().add(new KeyFrame(Duration.millis(1000.0 / frequency), handler));
timeline.setCycleCount(Animation.INDEFINITE);
return timeline;
}
public void start(){
this.updateLoop.play();
this.drawLoop.play();
this.fpsLoop.play();
}
public void pause(){
this.updateLoop.pause();
this.drawLoop.pause();
this.fpsLoop.pause();
}
public void stop(){
this.updateLoop.stop();
this.drawLoop.stop();
this.fpsLoop.stop();
}
public void handleUpdate(ActionEvent ae) { // 用于更新逻辑
this.tick++;
// System.out.println("Update tick: " + this.tick); // 调试用
}
public void handleDraw(ActionEvent ae){ // 用于绘制逻辑
this.curFrame++;
this.r.setWidth(curFrame); // 每次调用增加矩形宽度
// System.out.println("Draw frame: " + this.curFrame); // 调试用
}
public void handleFPS(ActionEvent ae) { // 用于FPS计数
this.fps = this.curFrame - this.lastFrames;
this.lastFrames = this.curFrame;
System.out.println("Current FPS: " + this.fps); // 打印每秒绘制的帧数
}
}代码解析与注意事项:
对于需要持续、高频率更新的场景,特别是游戏循环或自定义动画,JavaFX的AnimationTimer是一个更强大和灵活的选择。AnimationTimer是一个抽象类,它提供了一个handle(long now)方法,该方法会在JavaFX的渲染线程上,在每帧(通常是每秒约60次)被调用。
使用AnimationTimer的优点是:
以下是一个AnimationTimer的简单示例:
import javafx.animation.AnimationTimer;
import javafx.scene.shape.Rectangle;
public class GameLoopTimer extends AnimationTimer {
private Rectangle r;
private long lastUpdateTime = 0;
private long lastFpsTime = 0;
private int frameCount = 0;
private int logicTick = 0;
private static final double UPDATE_INTERVAL_NS = 1_000_000_000.0 / 60.0; // 60 updates per second
private static final double DRAW_INTERVAL_NS = 1_000_000_000.0 / 120.0; // 120 draws per second (conceptual, as handle is ~60Hz)
private double updateAccumulator = 0;
private double drawAccumulator = 0;
public GameLoopTimer(Rectangle r) {
this.r = r;
}
@Override
public void handle(long now) {
if (lastUpdateTime == 0) {
lastUpdateTime = now;
lastFpsTime = now;
return;
}
double deltaTimeNs = now - lastUpdateTime;
lastUpdateTime = now;
// 逻辑更新 (例如,每秒60次)
updateAccumulator += deltaTimeNs;
while (updateAccumulator >= UPDATE_INTERVAL_NS) {
logicTick++;
// System.out.println("Logic Update: " + logicTick);
// 这里放置需要60Hz更新的逻辑
updateAccumulator -= UPDATE_INTERVAL_NS;
}
// 绘制逻辑 (AnimationTimer本身大约60Hz,所以直接在这里更新绘制状态)
// 如果需要120Hz的视觉更新,AnimationTimer无法直接达到,
// 但可以在每次handle调用时更新属性,JavaFX会尽可能快地渲染。
// 对于游戏,通常是每帧(handle调用一次)进行一次渲染。
frameCount++;
r.setWidth(r.getWidth() + 1); // 每次handle调用时增加宽度,模拟绘制
// System.out.println("Draw Frame: " + frameCount);
// FPS计算 (每秒一次)
if (now - lastFpsTime >= 1_000_000_000L) { // 1秒
System.out.println("Actual FPS (handle calls): " + frameCount);
frameCount = 0;
lastFpsTime = now;
}
}
public void startLoop() {
super.start();
}
public void stopLoop() {
super.stop();
}
}AnimationTimer注意事项:
在JavaFX中处理多速率动画和事件时,理解Timeline的工作原理至关重要。当需要不同频率的任务独立运行时,避免将所有KeyFrame堆叠在一个Timeline中。正确的做法是:
此外,在编写JavaFX代码时,建议遵循最佳实践,例如移除不必要的接口实现,并利用lambda表达式简化事件处理器的编写。同时,对FPS的测量应明确其所代表的含义,是逻辑更新频率还是实际的渲染帧率。
以上就是深入理解JavaFX Timeline:解决多速率KeyFrame同步问题的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号