Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Checking Collision of Shapes with JavaFX

Tags:

I am trying to do some collision detection. For this test I am using simple rectangular Shape, and checking their Bound, to figure if they are colliding. Although the detection does not work as expected. I have tried using different ways to move the object(relocate, setLayoutX,Y) and also different bound checks (boundsInLocal,boundsInParrent etc) but I still cannot get this to work. As you can see the detection works only for one object, even when you have three objects only one detects collision. This is some working code demonstrating the problem:

import javafx.application.Application; import javafx.event.EventHandler; import javafx.scene.Cursor; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage;  import java.util.ArrayList;   public class CollisionTester extends Application {       private ArrayList<Rectangle> rectangleArrayList;      public static void main(String[] args) {         launch(args);     }      public void start(Stage primaryStage) {         primaryStage.setTitle("The test");         Group root = new Group();         Scene scene = new Scene(root, 400, 400);          rectangleArrayList = new ArrayList<Rectangle>();         rectangleArrayList.add(new Rectangle(30.0, 30.0, Color.GREEN));         rectangleArrayList.add(new Rectangle(30.0, 30.0, Color.RED));         rectangleArrayList.add(new Rectangle(30.0, 30.0, Color.CYAN));         for(Rectangle block : rectangleArrayList){             setDragListeners(block);         }         root.getChildren().addAll(rectangleArrayList);         primaryStage.setScene(scene);         primaryStage.show();     }      public void setDragListeners(final Rectangle block) {         final Delta dragDelta = new Delta();          block.setOnMousePressed(new EventHandler<MouseEvent>() {             @Override             public void handle(MouseEvent mouseEvent) {                 // record a delta distance for the drag and drop operation.                 dragDelta.x = block.getTranslateX() - mouseEvent.getSceneX();                 dragDelta.y = block.getTranslateY() - mouseEvent.getSceneY();                 block.setCursor(Cursor.NONE);             }         });         block.setOnMouseReleased(new EventHandler<MouseEvent>() {             @Override             public void handle(MouseEvent mouseEvent) {                 block.setCursor(Cursor.HAND);             }         });         block.setOnMouseDragged(new EventHandler<MouseEvent>() {             @Override             public void handle(MouseEvent mouseEvent) {                  block.setTranslateX(mouseEvent.getSceneX() + dragDelta.x);                 block.setTranslateY(mouseEvent.getSceneY() + dragDelta.y);                 checkBounds(block);              }         });     }      private void checkBounds(Rectangle block) {         for (Rectangle static_bloc : rectangleArrayList)             if (static_bloc != block) {                 if (block.getBoundsInParent().intersects(static_bloc.getBoundsInParent())) {                     block.setFill(Color.BLUE);        //collision                 } else {                     block.setFill(Color.GREEN);    //no collision                 }             } else {                 block.setFill(Color.GREEN);    //no collision -same block             }     }      class Delta {         double x, y;     } } 
like image 499
Giannis Avatar asked Feb 21 '13 23:02

Giannis


People also ask

How do you detect collisions?

If both the horizontal and vertical edges overlap we have a collision. We check if the right side of the first object is greater than the left side of the second object and if the second object's right side is greater than the first object's left side; similarly for the vertical axis.

What is stroke in JavaFX?

The stroke property specifies/defines the color of the boundary of a shape. You can set the color of the boundary using the setStroke() method of the javafx.

What are the two parameters of stage in JavaFX divided as?

A stage has two parameters determining its position namely Width and Height. It is divided as Content Area and Decorations (Title Bar and Borders).

What is scene graph JavaFX?

A scene graph is a tree data structure, most commonly found in graphical applications and libraries such as vector editing tools, 3D libraries, and video games. The JavaFX scene graph is a retained mode API, meaning that it maintains an internal model of all graphical objects in your application.


1 Answers

Looks like you have a slight logic error in your checkBounds routine - you are correctly detecting collisions (based on bounds) but are overwriting the fill of your block when you perform subsequent collision checks in the same routine.

Try something like this - it adds a flag so that the routine does not "forget" that a collision was detected:

private void checkBounds(Shape block) {   boolean collisionDetected = false;   for (Shape static_bloc : nodes) {     if (static_bloc != block) {       static_bloc.setFill(Color.GREEN);        if (block.getBoundsInParent().intersects(static_bloc.getBoundsInParent())) {         collisionDetected = true;       }     }   }    if (collisionDetected) {     block.setFill(Color.BLUE);   } else {     block.setFill(Color.GREEN);   } } 

Note that the check you are doing (based on bounds in parent) will report intersections of the rectangle enclosing the visible bounds of nodes within the same parent group.

Alternate Implementation

In case you need it, I updated your original sample so that it is able to check based on the visual shape of the Node rather than the bounding box of the visual shape. This lets you to accurately detect collisions for non-rectangular shapes such as Circles. The key for this is the Shape.intersects(shape1, shape2) method.

import javafx.application.Application; import javafx.event.EventHandler; import javafx.scene.*; import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import javafx.stage.Stage;  import java.util.ArrayList; import javafx.scene.shape.*;  public class CircleCollisionTester extends Application {    private ArrayList<Shape> nodes;    public static void main(String[] args) { launch(args); }    @Override public void start(Stage primaryStage) {     primaryStage.setTitle("Drag circles around to see collisions");     Group root = new Group();     Scene scene = new Scene(root, 400, 400);      nodes = new ArrayList<>();     nodes.add(new Circle(15, 15, 30));     nodes.add(new Circle(90, 60, 30));     nodes.add(new Circle(40, 200, 30));     for (Shape block : nodes) {       setDragListeners(block);     }     root.getChildren().addAll(nodes);     checkShapeIntersection(nodes.get(nodes.size() - 1));      primaryStage.setScene(scene);     primaryStage.show();   }    public void setDragListeners(final Shape block) {     final Delta dragDelta = new Delta();      block.setOnMousePressed(new EventHandler<MouseEvent>() {       @Override public void handle(MouseEvent mouseEvent) {         // record a delta distance for the drag and drop operation.         dragDelta.x = block.getLayoutX() - mouseEvent.getSceneX();         dragDelta.y = block.getLayoutY() - mouseEvent.getSceneY();         block.setCursor(Cursor.NONE);       }     });     block.setOnMouseReleased(new EventHandler<MouseEvent>() {       @Override public void handle(MouseEvent mouseEvent) {         block.setCursor(Cursor.HAND);       }     });     block.setOnMouseDragged(new EventHandler<MouseEvent>() {       @Override public void handle(MouseEvent mouseEvent) {         block.setLayoutX(mouseEvent.getSceneX() + dragDelta.x);         block.setLayoutY(mouseEvent.getSceneY() + dragDelta.y);         checkShapeIntersection(block);       }     });   }    private void checkShapeIntersection(Shape block) {     boolean collisionDetected = false;     for (Shape static_bloc : nodes) {       if (static_bloc != block) {         static_bloc.setFill(Color.GREEN);          Shape intersect = Shape.intersect(block, static_bloc);         if (intersect.getBoundsInLocal().getWidth() != -1) {           collisionDetected = true;         }       }     }      if (collisionDetected) {       block.setFill(Color.BLUE);     } else {       block.setFill(Color.GREEN);     }   }    class Delta { double x, y; } } 

Sample program output. In the sample the circles have been dragged around and the user is currently dragging a circle which has been marked as colliding with another circle (by painting it blue) - for demonstration purposes only the circle currently being dragged has it's collision color marked.

collisions

Comments based on additional questions

The link I posted to an intersection demo application in a prior comment was to illustrate the use of various bounds types rather than as a specific type of collision detection sample. For your use case, you don't need the additional complexity of the change listener and checking on various different kinds of bounds types - just settling on one type will be enough. Most collision detection is only going to be interested in intersection of visual bounds rather than other JavaFX bounds types such as the layout bounds or local bounds of a node. So you can either:

  1. Check for intersection of getBoundsInParent (as you did in your original question) which works on the smallest rectangular box which will encompass the visual extremities of the node OR
  2. Use the Shape.intersect(shape1, shape2) routine if you need to check based on the visual shape of the Node rather than the bounding box of the visual shape.

Should I be using setLayoutX or translateX for the rectangle

The layoutX and layoutY properties are intended for positioning or laying out nodes. The translateX and translateY properties are intended for temporary changes to the visual location of a node (for example when the node is undergoing an animation). For your example, though either property will work, it is perhaps better form to use the layout properties than the translate ones, that way if you did want to run something like a TranslateTransition on the nodes, it will be more obvious what the start and end translate values should be as those values will be relative to the current layout position of the node rather than the position in the parent group.

Another way you could use these layout and translate co-ordinates in tandem in your sample is if you had something like an ESC to cancel during the course of a drag operation. You could set layoutX,Y to the initial location of your node, start a drag operation which sets translateX,Y values and if the user presses ESC, set translateX,Y back to 0 to cancel the drag operation or if the user releases the mouse set layoutX,Y to layoutX,Y+translateX,Y and set translateX,Y back to 0. The idea is that the translation is values are used for a temporary modification of the visual co-ordinates of the node from it's original layout position.

will the intersect work even though the circles are animated? I mean without dragging the circle by mouse, what will happen if I made them to move around randomly. Will the colour change in this case also?

To do this, just change where the collision detection function is called and the collision handler invoked. Rather than checking for intersections based upon a mouse drag event (like the example above), instead check for collisions within a change listener on each node's boundsInParentProperty().

block.boundsInParentProperty().addListener((observable, oldValue, newValue) ->          checkShapeIntersection(block) ); 

Note: if you have lots of shapes being animated, then checking for collisions once per frame within a game loop will be more efficient than running a collision check whenever any node moves (as is done in the boundsInParentProperty change listener above).

Additional info for handling input on non-rectangular shapes

For input detection not collision detection, so not directly related to your question, look at the node.pickOnBounds setting if you need mouse or touch interaction with a non-rectangular node.

like image 115
jewelsea Avatar answered Oct 27 '22 07:10

jewelsea