I'm on Ubuntu 20.04 using OpenJavaFX. I want to have the user press the escape key to toggle the display of a menu. Due to the very high frame rate, I'm struggling to achieve this.
The simple program:
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();
}
}
If run without Thread.sleep() the framerate is around 600fps. As a result, pressing the escape key once will be seen as down for a number of frames (due to the speed limit of my human finger) thereby triggering the toggle multiple times. This is obviously not intended. So, I tried to cap the framerate at 60fps. However, with the sleeping, the program runs very slow (perhaps I'm sleeping on the wrong thread?)
How best to keep track of the input to achieve this toggling behavior?
First, you should never block the FX Application Thread by calling Thread.sleep() on it. That will prevent the UI from being updated, or events being handled, until the sleep() is complete.
If the intention is simply that each time the user presses the ESCAPE key that the menu is toggled, then your code is way too complex. Simply toggle a flag indicating if the menu should be painted in the onReleased handler, and check the flag in 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();
}
}
If you want to optimize repaints only to when they are needed, simply introduce another flag indicating a repaint is necessary:
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();
}
}
I didn't try any of this, so I can only nudge you in a general direction.
You could add another boolean variable esc_handled you set to true at the end of your handle method. Then you can add one more check to the method if the event has already been handled and if it has, you skip the handling step.
The following code achieves this:
add variable
boolean in_menu;
boolean esc_down;
boolean esc_handled;
check for esc_handled (inside handle) and set event to handled
if (esc_down && !esc_handled)
{
in_menu = !in_menu;
esc_handled = true;
}
on release esc set esc_handled to false
scene.setOnKeyReleased(new EventHandler<KeyEvent>(){
#Override
public void handle(KeyEvent event)
{
if (event.getCode() == KeyCode.ESCAPE)
{
esc_down = false;
esc_handled = false;
}
}
});
It looks like you're using the animation timer to do some kind of sampling of the state of the in or out and the press/release of the escape key. You don't need to do that at all.
You're trying to turn the key press/release events into a state, which makes sense, but you simply need to toggle that state in the event handler. Since the show/hide action is in response to an event, you can just call the draw routine directly from the event. So then the event will toggle the state and call the screen redraw:
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);
}
}
}
Alternatively (and this is probably more "JavaFX"), you can make the In/Out Menu state observable, and then put a change listener on the state to do the repaint:
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);
}
}
}
I have figured out how to detect the collision of the rectangle and the image but where I am having problems is when I call the method to relocate the image it does it like 10 times in a row then won't work again.
I am making my first game in JavaFX. Im trying to make a basic snake game but haven't been able to figure out what is wrong with relocating the food after the snake collides with it.
public class Main extends Application {
Stage window;
Scene mainScene;
private final int WIDTH = 500;
private final int HEIGHT = 500;
Timeline timeline;
private Direction action = Direction.RIGHT;
private Rectangle snakeHead;
private final int speed = 3;
private int xSpeed = 3;
private int ySpeed = 3;
private int snakeW = 20;
private int snakeH = 20;
private BoundingBox snakeBox;
private BoundingBox foodBox;
private ImageView food;
private Random rand = new Random();
private double foodX;
private double foodY;
enum Direction {
LEFT,RIGHT,UP,DOWN;
}
private Parent createContent(){
Pane root = new Pane();
//food = new Food();
food = new ImageView(new Image("resources/apple.png"));
food.setFitHeight(25);
food.setFitWidth(25);
food.setPreserveRatio(true);
newFood();
foodBox = new BoundingBox(foodX,foodY,20,20);
snakeHead = new Rectangle(snakeW,snakeH);
snakeHead.setTranslateX(200);
snakeHead.setTranslateY(200);
snakeBox = new BoundingBox(snakeHead.getTranslateX(),snakeHead.getTranslateY(),snakeW,snakeH);
timeline = new Timeline(new KeyFrame(Duration.millis(16), e-> {
//Snake movement
if(action == Direction.LEFT) {snakeHead.setTranslateX(snakeHead.getTranslateX() - xSpeed);}
if(action == Direction.RIGHT) {snakeHead.setTranslateX(snakeHead.getTranslateX() + xSpeed);}
if(action == Direction.UP) {snakeHead.setTranslateY(snakeHead.getTranslateY() - ySpeed);}
if(action == Direction.DOWN) {snakeHead.setTranslateY(snakeHead.getTranslateY() + ySpeed);}
//Stops snake at edges of screen
if(snakeHead.getTranslateX() <= 0){
xSpeed = 0;
if(action == Direction.RIGHT){xSpeed = speed;}
}
if(snakeHead.getTranslateX() >= WIDTH - snakeW){
xSpeed = 0;
if(action == Direction.LEFT){xSpeed = speed;}
}
if(snakeHead.getTranslateY() <= 0){
ySpeed = 0;
if(action == Direction.DOWN){ySpeed = speed;}
}
if(snakeHead.getTranslateY() >= HEIGHT - snakeH){
ySpeed = 0;
if(action == Direction.UP){ySpeed = speed;}
}
//TODO: Detect Collisions
if(foodBox.intersects(snakeHead.getTranslateX(),snakeHead.getTranslateY (),snakeW,snakeH)){
newFood();
System.out.println("Collision");
}
}));
timeline.setCycleCount(Timeline.INDEFINITE);
root.getChildren().addAll(snakeHead,food);
return root;
}
private void newFood() {
foodX = rand.nextInt(500);
foodY = rand.nextInt(500);
food.setTranslateX(foodX);
food.setTranslateY(foodY);
System.out.println("X " + foodX);
System.out.println("Y " + foodY);
}
private void startGame() {
timeline.play();
}
private void stopGame() {
timeline.stop();
}
#Override
public void start(Stage primaryStage) throws Exception{
window = primaryStage;
mainScene = new Scene(createContent(),WIDTH,HEIGHT);
mainScene.setOnKeyPressed(e-> {
switch(e.getCode()) {
case UP: action = Direction.UP; break;
case DOWN: action = Direction.DOWN; break;
case LEFT: action = Direction.LEFT; break;
case RIGHT: action = Direction.RIGHT; break;
}
});
window.setTitle("Snake");
window.setResizable(false);
window.setScene(mainScene);
window.show();
startGame();
}
public static void main(String[] args) {
launch(args);
}
}
What I'm looking for is when the rectangle hits the apple it relocates. I have been struggling with this for awhile and don't know what to do. Im a fairly new programmer still.
You can check for intersection of snake's and food's boundsInParent.
if(food.getBoundsInParent().intersects(snakeHead.getBoundsInParent())){
newFood();
System.out.println("Collision");
}
i want to show result 80 percent when i got result.(start 0 to myresult=80)
but my progressbar is show result 0 until 100 (max of procress bar)
#FXML
private ProgressBar progress;
#Override
public void initialize(URL url, ResourceBundle rb) {
double percent = 0.8;
progress.setProgress(percent);
progress.progressProperty().bind(task.progressProperty());
Thread th = new Thread(task);
th.start();
// TODOs
}
public Task<Void> task = new Task<Void>() {
#Override
public Void call() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.interrupted();
break;
}
System.out.println(i + 1);
updateProgress(i + 1, 10);
}
return null;
}
};
thank you for comment
Instead of binding to the progress property of the task, bind to the progress property multiplied by 0.8. This way you convert the [0, 1] range of the task progress to range [0, 0.8]:
progress.progressProperty().bind(task.progressProperty().multiply(0.8));
I use this JavaFX code to drag BorderPane into FlowPane:
private Node dragPanel(Node bp)
{
bp.setOnDragDetected(new EventHandler<MouseEvent>()
{
#Override
public void handle(MouseEvent event)
{
Dragboard db = bp.startDragAndDrop(TransferMode.MOVE);
ClipboardContent clipboard = new ClipboardContent();
final int nodeIndex = bp.getParent().getChildrenUnmodifiable()
.indexOf(bp);
clipboard.putString(Integer.toString(nodeIndex));
db.setContent(clipboard);
Image img = bp.snapshot(null, null);
db.setDragView(img, 7, 7);
event.consume();
}
});
bp.setOnDragOver(new EventHandler<DragEvent>()
{
#Override
public void handle(DragEvent event)
{
boolean accept = true;
final Dragboard dragboard = event.getDragboard();
if (dragboard.hasString())
{
try
{
int incomingIndex = Integer.parseInt(dragboard.getString());
int myIndex = bp.getParent().getChildrenUnmodifiable()
.indexOf(bp);
if (incomingIndex == myIndex)
{
accept = false;
}
}
catch (java.lang.NumberFormatException e)
{
// handle null or not number string in clipboard
accept = false;
}
}
else
{
accept = false;
}
if (accept)
{
event.acceptTransferModes(TransferMode.MOVE);
}
}
});
bp.setOnDragDropped(new EventHandler<DragEvent>()
{
#Override
public void handle(DragEvent event)
{
boolean success = false;
final Dragboard dragboard = event.getDragboard();
if (dragboard.hasString())
{
try
{
int incomingIndex = Integer.parseInt(dragboard.getString());
final Pane parent = (Pane) bp.getParent();
final ObservableList<Node> children = parent.getChildren();
int myIndex = children.indexOf(bp);
final int laterIndex = Math.max(incomingIndex, myIndex);
Node removedLater = children.remove(laterIndex);
final int earlierIndex = Math.min(incomingIndex, myIndex);
Node removedEarlier = children.remove(earlierIndex);
children.add(earlierIndex, removedLater);
children.add(laterIndex, removedEarlier);
success = true;
}
catch (java.lang.NumberFormatException e)
{
//TO DO... handle null or not number string in clipboard
}
}
event.setDropCompleted(success);
}
});
// bp.setMinSize(50, 50);
return bp;
}
I enable this drag event using this code:
BorderPane panel = new BorderPane();
dragPanel(panel),
I also have resize code which is also activated. I need some way to apply the drag code only of I click and drag the panel. I want to disable the drag listener when I drag the panel borders. Is there a way to limit this?
I'm guessing by "borders" you just mean the edges of the border panes. You can just check the coordinates of the mouse event and only initiate dragging if you're away from the borders. To do this, you need to know the width and height of the border pane. The methods to get those are defined in Region, so you need to narrow the type of the parameter from Node to Region. This will still work if you call dragPanel(panel) but you won't be able to pass in a Node that is not a Region instance.
final int borderSize = 5 ;
// ...
private Node dragPane(Region bp) {
bp.setOnDragDetected(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
double x = event.getX();
double y = event.getY();
double width = bp.getWidth();
double height = bp.getHeight();
if (x > borderSize && x < width - borderSize
&& y > borderSize && y < height - borderSize) {
Dragboard db = bp.startDragAndDrop(TransferMode.MOVE);
ClipboardContent clipboard = new ClipboardContent();
final int nodeIndex = bp.getParent().getChildrenUnmodifiable()
.indexOf(bp);
clipboard.putString(Integer.toString(nodeIndex));
db.setContent(clipboard);
Image img = bp.snapshot(null, null);
db.setDragView(img, 7, 7);
event.consume();
}
}
});
// ...
}
I'm using javafx in javase8 and netbeans 8.0.2, I made randomly generated shape images and show them sequentially with timeline. But last image isn't shown. timeline.setcyclecount(12) i use java generate 12 images but doesn't show 12. image in timeline.
public class JavaFXApplication3 extends Application {
int k;
Timeline timeline;
class ResizableCanvas extends Canvas {
private void draw() {
int[] uyaran = {3, 7, 12};
boolean[] type = new boolean[12];
for (int i = 0; i < 12; i++) {
type[i] = false;
}
for (int v : uyaran) {
type[v - 1] = true;
}
double w = getWidth();
double h = getHeight();
GraphicsContext gc = getGraphicsContext2D();
gc.clearRect(0, 0, w, h);
gc.setFill(Color.RED);
System.out.println(k);
if (type[k]) {
gc.fillOval(0, 0, w, h);
}
k++;
}
}
#Override
public void start(Stage stage) throws Exception {
k = 0;
ResizableCanvas canvas = new ResizableCanvas();
timeline = new Timeline(new KeyFrame(Duration.millis(1000), ae -> canvas.draw()));
timeline.setCycleCount(12);
timeline.setOnFinished(ActionEvent -> {
try {
Thread.sleep(10000);
} catch (InterruptedException ex) {}
stage.close();
});
timeline.play();
Pane pane = new Pane();
pane.getChildren().add(canvas);
canvas.widthProperty().bind(pane.widthProperty());
canvas.heightProperty().bind(pane.heightProperty());
stage.setScene(new Scene(pane));
stage.show();
}
}
You are calling Thread.sleep(...) on the FX Application Thread, which blocks the thread and prevents it from updating. The final ellipse is only actually rendered when the pause is over, but of course you then close the window so you never see it.
Use a PauseTransition to pause, and use its onFinished handler to do something when the pause is over:
timeline.setOnFinished(ae -> {
PauseTransition pause = new PauseTransition(Duration.seconds(10));
pause.setOnFinished(event -> stage.close());
pause.play();
});