JavaFX 如何处理帧率非常高的输入?
JavaFX How to Handle Input With Very High Framerate?
我正在 Ubuntu 20.04 使用 OpenJavaFX。我想让用户按下转义键来切换菜单的显示。由于帧率非常高,我正在努力实现这一目标。
简单程序:
class
AutoScalingCanvas extends Region {
private final Canvas canvas;
public AutoScalingCanvas(double canvasWidth, double canvasHeight) {
this.canvas = new Canvas(canvasWidth, canvasHeight);
getChildren().add(canvas);
}
public GraphicsContext getGraphicsContext2D() {
return canvas.getGraphicsContext2D();
}
@Override
protected void layoutChildren() {
double x = getInsets().getLeft();
double y = getInsets().getTop();
double w = getWidth() - getInsets().getRight() - x;
double h = getHeight() - getInsets().getBottom() - y;
// preserve aspect ratio while also staying within the available space
double sf = Math.min(w / canvas.getWidth(), h / canvas.getHeight());
canvas.setScaleX(sf);
canvas.setScaleY(sf);
positionInArea(canvas, x, y, w, h, -1, HPos.CENTER, VPos.CENTER);
}
}
public class
Gui extends Application
{
long target_ns_per_frame = 1_000_000_00 / 60;
boolean in_menu;
boolean esc_down;
@Override
public void
start(Stage primary_stage) throws Exception
{
primary_stage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
Scene scene = new Scene(canvas);
scene.setFill(Color.BLACK);
primary_stage.setScene(scene);
GraphicsContext gc = canvas.getGraphicsContext2D();
scene.setOnKeyPressed(new EventHandler<KeyEvent>(){
@Override
public void handle(KeyEvent event)
{
esc_down = (event.getCode() == KeyCode.ESCAPE);
}
});
scene.setOnKeyReleased(new EventHandler<KeyEvent>(){
@Override
public void handle(KeyEvent event)
{
if (event.getCode() == KeyCode.ESCAPE)
{
esc_down = false;
}
}
});
new AnimationTimer()
{
@Override
public void
handle(long total_elapsed_time_ns)
{
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (esc_down)
{
in_menu = !in_menu;
}
if (in_menu)
{
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
}
else
{
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
long elapsed_time_ns = System.nanoTime() -
total_elapsed_time_ns;
if (elapsed_time_ns < target_ns_per_frame)
{
long time_remaining_ms = (target_ns_per_frame - elapsed_time_ns)
/ 1000;
try {
Thread.sleep(time_remaining_ms);
}
catch (InterruptedException e)
{
}
}
}
}.start();
primary_stage.show();
}
}
如果 运行 没有 Thread.sleep()
,则帧速率约为 600fps。因此,按下 Esc 键一次将被视为按下数帧(由于我的手指的速度限制),从而触发多次切换。这显然不是故意的。所以,我试图将帧速率限制在 60fps。但是,随着休眠,程序 运行 非常慢(也许我在错误的线程上休眠?)
如何最好地跟踪输入以实现这种切换行为?
我没有尝试过这些,所以我只能在一个大概的方向上推动你。
您可以在 handle
方法的末尾添加另一个设置为 true
的布尔变量 esc_handled
。然后,如果事件已被处理,则可以向该方法再添加一项检查,如果已处理,则跳过处理步骤。
以下代码实现了这一点:
- 添加变量
boolean in_menu;
boolean esc_down;
boolean esc_handled;
- 检查
esc_handled
(在 handle
内)并将事件设置为已处理
if (esc_down && !esc_handled)
{
in_menu = !in_menu;
esc_handled = true;
}
- 在释放 esc 时将 esc_handled 设置为 false
scene.setOnKeyReleased(new EventHandler<KeyEvent>(){
@Override
public void handle(KeyEvent event)
{
if (event.getCode() == KeyCode.ESCAPE)
{
esc_down = false;
esc_handled = false;
}
}
});
首先,您不应该通过调用 Thread.sleep()
来阻塞 FX 应用程序线程。这将阻止 UI 更新或处理事件,直到 sleep()
完成。
如果只是想在用户每次按下 ESCAPE 键时切换菜单,那么您的代码就太复杂了。只需切换一个标志,指示是否应在 onReleased
处理程序中绘制菜单,并检查 AnimationTimer.handle()
:
中的标志
public class Gui extends Application {
boolean inMenu;
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
Scene scene = new Scene(canvas);
scene.setFill(Color.BLACK);
primaryStage.setScene(scene);
GraphicsContext gc = canvas.getGraphicsContext2D();
scene.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
inMenu = ! inMenu;
}
});
new AnimationTimer() {
@Override
public void handle(long now) {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (inMenu) {
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
} else {
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
}
}.start();
primaryStage.show();
}
}
如果您只想在需要重绘时优化重绘,只需引入另一个标志来指示重绘是必要的:
public class Gui extends Application {
private boolean inMenu;
private boolean repaintRequested = true ;
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
Scene scene = new Scene(canvas);
scene.setFill(Color.BLACK);
primaryStage.setScene(scene);
GraphicsContext gc = canvas.getGraphicsContext2D();
scene.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
inMenu = ! inMenu;
repaintRequested = true ;
}
});
new AnimationTimer() {
@Override
public void handle(long now) {
if (repaintRequested) {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (inMenu) {
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
} else {
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
repaintRequested = false ;
}
}
}.start();
primaryStage.show();
}
}
您似乎在使用动画计时器对输入或输出的状态以及转义键的 press/release 进行某种采样。您根本不需要这样做。
您正试图将关键 press/release 事件转变为一种状态,这是有道理的,但您只需要在事件处理程序中切换该状态即可。由于 show/hide 动作是对事件的响应,因此您可以直接从事件中调用绘图例程。那么事件将切换状态并调用屏幕重绘:
public class Gui extends Application {
boolean in_menu;
GraphicsContext gc;
@Override
public void start(Stage primary_stage) throws Exception {
primary_stage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
primary_stage.setScene(new Scene(canvas));
gc = canvas.getGraphicsContext2D();
showOrHideMenu();
new Scene(canvas).setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
in_menu = !in_menu;
showOrHideMenu();
}
});
primary_stage.show();
}
private void showOrHideMenu() {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (in_menu) {
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
} else {
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
}
}
或者(这可能更像“JavaFX”),您可以使 In/Out 菜单状态可观察,然后在该状态上放置一个更改侦听器以进行重绘:
public class Gui extends Application {
BooleanProperty in_menu = new SimpleBooleanProperty(false);
GraphicsContext gc;
@Override
public void start(Stage primary_stage) throws Exception {
primary_stage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
Scene scene = new Scene(canvas);
primary_stage.setScene(scene);
gc = canvas.getGraphicsContext2D();
showOrHideMenu(false);
scene.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
in_menu.set(!in_menu.get());
}
});
in_menu.addListener(((observable, oldValue, newValue) -> showOrHideMenu(newValue)));
primary_stage.show();
}
private void showOrHideMenu(boolean inMenu) {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (inMenu) {
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
} else {
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
}
}
我正在 Ubuntu 20.04 使用 OpenJavaFX。我想让用户按下转义键来切换菜单的显示。由于帧率非常高,我正在努力实现这一目标。
简单程序:
class
AutoScalingCanvas extends Region {
private final Canvas canvas;
public AutoScalingCanvas(double canvasWidth, double canvasHeight) {
this.canvas = new Canvas(canvasWidth, canvasHeight);
getChildren().add(canvas);
}
public GraphicsContext getGraphicsContext2D() {
return canvas.getGraphicsContext2D();
}
@Override
protected void layoutChildren() {
double x = getInsets().getLeft();
double y = getInsets().getTop();
double w = getWidth() - getInsets().getRight() - x;
double h = getHeight() - getInsets().getBottom() - y;
// preserve aspect ratio while also staying within the available space
double sf = Math.min(w / canvas.getWidth(), h / canvas.getHeight());
canvas.setScaleX(sf);
canvas.setScaleY(sf);
positionInArea(canvas, x, y, w, h, -1, HPos.CENTER, VPos.CENTER);
}
}
public class
Gui extends Application
{
long target_ns_per_frame = 1_000_000_00 / 60;
boolean in_menu;
boolean esc_down;
@Override
public void
start(Stage primary_stage) throws Exception
{
primary_stage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
Scene scene = new Scene(canvas);
scene.setFill(Color.BLACK);
primary_stage.setScene(scene);
GraphicsContext gc = canvas.getGraphicsContext2D();
scene.setOnKeyPressed(new EventHandler<KeyEvent>(){
@Override
public void handle(KeyEvent event)
{
esc_down = (event.getCode() == KeyCode.ESCAPE);
}
});
scene.setOnKeyReleased(new EventHandler<KeyEvent>(){
@Override
public void handle(KeyEvent event)
{
if (event.getCode() == KeyCode.ESCAPE)
{
esc_down = false;
}
}
});
new AnimationTimer()
{
@Override
public void
handle(long total_elapsed_time_ns)
{
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (esc_down)
{
in_menu = !in_menu;
}
if (in_menu)
{
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
}
else
{
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
long elapsed_time_ns = System.nanoTime() -
total_elapsed_time_ns;
if (elapsed_time_ns < target_ns_per_frame)
{
long time_remaining_ms = (target_ns_per_frame - elapsed_time_ns)
/ 1000;
try {
Thread.sleep(time_remaining_ms);
}
catch (InterruptedException e)
{
}
}
}
}.start();
primary_stage.show();
}
}
如果 运行 没有 Thread.sleep()
,则帧速率约为 600fps。因此,按下 Esc 键一次将被视为按下数帧(由于我的手指的速度限制),从而触发多次切换。这显然不是故意的。所以,我试图将帧速率限制在 60fps。但是,随着休眠,程序 运行 非常慢(也许我在错误的线程上休眠?)
如何最好地跟踪输入以实现这种切换行为?
我没有尝试过这些,所以我只能在一个大概的方向上推动你。
您可以在 handle
方法的末尾添加另一个设置为 true
的布尔变量 esc_handled
。然后,如果事件已被处理,则可以向该方法再添加一项检查,如果已处理,则跳过处理步骤。
以下代码实现了这一点:
- 添加变量
boolean in_menu; boolean esc_down; boolean esc_handled;
- 检查
esc_handled
(在handle
内)并将事件设置为已处理if (esc_down && !esc_handled) { in_menu = !in_menu; esc_handled = true; }
- 在释放 esc 时将 esc_handled 设置为 false
scene.setOnKeyReleased(new EventHandler<KeyEvent>(){ @Override public void handle(KeyEvent event) { if (event.getCode() == KeyCode.ESCAPE) { esc_down = false; esc_handled = false; } } });
首先,您不应该通过调用 Thread.sleep()
来阻塞 FX 应用程序线程。这将阻止 UI 更新或处理事件,直到 sleep()
完成。
如果只是想在用户每次按下 ESCAPE 键时切换菜单,那么您的代码就太复杂了。只需切换一个标志,指示是否应在 onReleased
处理程序中绘制菜单,并检查 AnimationTimer.handle()
:
public class Gui extends Application {
boolean inMenu;
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
Scene scene = new Scene(canvas);
scene.setFill(Color.BLACK);
primaryStage.setScene(scene);
GraphicsContext gc = canvas.getGraphicsContext2D();
scene.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
inMenu = ! inMenu;
}
});
new AnimationTimer() {
@Override
public void handle(long now) {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (inMenu) {
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
} else {
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
}
}.start();
primaryStage.show();
}
}
如果您只想在需要重绘时优化重绘,只需引入另一个标志来指示重绘是必要的:
public class Gui extends Application {
private boolean inMenu;
private boolean repaintRequested = true ;
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
Scene scene = new Scene(canvas);
scene.setFill(Color.BLACK);
primaryStage.setScene(scene);
GraphicsContext gc = canvas.getGraphicsContext2D();
scene.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
inMenu = ! inMenu;
repaintRequested = true ;
}
});
new AnimationTimer() {
@Override
public void handle(long now) {
if (repaintRequested) {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (inMenu) {
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
} else {
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
repaintRequested = false ;
}
}
}.start();
primaryStage.show();
}
}
您似乎在使用动画计时器对输入或输出的状态以及转义键的 press/release 进行某种采样。您根本不需要这样做。
您正试图将关键 press/release 事件转变为一种状态,这是有道理的,但您只需要在事件处理程序中切换该状态即可。由于 show/hide 动作是对事件的响应,因此您可以直接从事件中调用绘图例程。那么事件将切换状态并调用屏幕重绘:
public class Gui extends Application {
boolean in_menu;
GraphicsContext gc;
@Override
public void start(Stage primary_stage) throws Exception {
primary_stage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
primary_stage.setScene(new Scene(canvas));
gc = canvas.getGraphicsContext2D();
showOrHideMenu();
new Scene(canvas).setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
in_menu = !in_menu;
showOrHideMenu();
}
});
primary_stage.show();
}
private void showOrHideMenu() {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (in_menu) {
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
} else {
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
}
}
或者(这可能更像“JavaFX”),您可以使 In/Out 菜单状态可观察,然后在该状态上放置一个更改侦听器以进行重绘:
public class Gui extends Application {
BooleanProperty in_menu = new SimpleBooleanProperty(false);
GraphicsContext gc;
@Override
public void start(Stage primary_stage) throws Exception {
primary_stage.setTitle("GUI");
AutoScalingCanvas canvas = new AutoScalingCanvas(1280, 720);
Scene scene = new Scene(canvas);
primary_stage.setScene(scene);
gc = canvas.getGraphicsContext2D();
showOrHideMenu(false);
scene.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
in_menu.set(!in_menu.get());
}
});
in_menu.addListener(((observable, oldValue, newValue) -> showOrHideMenu(newValue)));
primary_stage.show();
}
private void showOrHideMenu(boolean inMenu) {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, 1280, 720);
if (inMenu) {
gc.setFill(Color.BLUE);
gc.fillRect(300, 300, 200, 200);
} else {
gc.setFill(Color.RED);
gc.fillRect(100, 100, 100, 100);
}
}
}