1

I have a question regarding the correct way to implement a mouse drag event in JavaFX.

My playGame() method currently makes use of onMouseClicked, however this is just a placeholder for now

Ideally, I would like the 'frisbee' to be "tossed" in the direction of the mouse drag.

What would be a good way to do this?

package FrisbeeToss;

import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class FrisbeeTossMain extends Application {

private Text info = new Text();
private Entity frisbee, target;

private static final int APP_W = 800;
private static final int APP_H = 600;

private static class Entity extends Parent {
    public Entity(double x, double y, double r, Color c) {
        setTranslateX(x);
        setTranslateY(y);
        Circle circ = new Circle(r, c);
        getChildren().add(circ);
    }
}

private Parent createContent() {
    Pane root = new Pane();
    root.setPrefSize(APP_W, APP_H);

    info.setTranslateX(50);
    info.setTranslateY(50);

    target = new Entity(APP_W /2, APP_H /2, 75, Color.RED);
    frisbee = new Entity(APP_W -20, APP_H -20, 60, Color.GREEN);

    root.getChildren().addAll(info, target, frisbee);

    return root;
}

private void checkCollision(Entity a, Entity b){
    if (a.getBoundsInParent().intersects(b.getBoundsInParent())) {
        info.setText("Target caught frisbee!");
    }
    else {
        info.setText("");
    }
}

private void playGame() {
    frisbee.setOnMouseClicked(event -> {
        System.out.println("Frisbee clicked");

        checkCollision(frisbee, target);
    });
}

@Override
public void start(Stage primaryStage) throws Exception {
    Scene scene = new Scene(createContent());

    primaryStage.setTitle("Frisbee Toss");
    primaryStage.setScene(scene);
    primaryStage.show();

    playGame();

    }
}
Ken White
  • 123,280
  • 14
  • 225
  • 444

1 Answers1

1

Animation

There are a few ways this could be done, but taking the concept of a Frisbee into consideration transitions would work well. There's an official tutorial here:

From those available, the PathTransition would work well. By converting the direction the user "tosses" the Frisbee in, you can generate a Path that the Frisbee Node can follow. By modifying the cycles and applying reversal you could also make the Frisbee behave like a boomerang

As Frisbee's spin, you could also take advantage of a RotationTransition and apply it alongside the movement along the path


Applying the animation (s)

You can apply the above transition(s) with just a mouseReleased event on the Frisbee, but as you've specifically mentioned dragging, I've modified your code below to show both approaches. One with the released event and the other using drag-and-drop

If you want to read more on the drag-and-drop feature, it's covered here:


Minor changes made to original source

In the implementations below, I've removed your Entity class replacing it with a Circle as the Entity wasn't adding anything, with the purpose seeming to be just creating a Circle

I've also removed the static declarations. In this particular example there is no benefit to having or removing them, but the static keyword should only be used where it needs to be. Hopefully this popular post can better explain why:


Implementations:

I've added comments to clarify some steps but if anything isn't clear, or you have some improvements please add a comment

mouseReleased approach:

public class FrisbeeTossMain extends Application {
    private Pane root;
    private Text info = new Text();
    private Circle frisbee, target;
    private PathTransition transition;

    private final int APP_W = 800;
    private final int APP_H = 600;
    private final double frisbeeX = APP_W -20;
    private final double frisbeeY = APP_H -20;

    private Parent createContent() {
        root = new Pane();
        root.setPrefSize(APP_W, APP_H);

        info.setTranslateX(50);
        info.setTranslateY(50);

        target = new Circle(75, Color.RED);
        target.setLayoutX(APP_W /2);
        target.setLayoutY(APP_H /2);

        frisbee = new Circle(60, Color.GREEN);
        frisbee.setLayoutX(frisbeeX);
        frisbee.setLayoutY(frisbeeY);
        frisbee.setFill(new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE,
                new Stop[] { new Stop(0, Color.BLACK), new Stop(1, Color.GREEN)}));

        SimpleBooleanProperty isFrisbeeVisuallyCollidingWithTarget = new SimpleBooleanProperty(false);
        frisbee.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
            isFrisbeeVisuallyCollidingWithTarget.set(
                    Shape.intersect(frisbee, target).getBoundsInParent().getWidth() >= 0 ? true : false);
        });

        isFrisbeeVisuallyCollidingWithTarget.addListener((observable, oldValue, newValue) -> {
            if(newValue && transition != null){
                //Stop the animation making it appear as though the frisbee was caught
                transition.stop();
            }
        });

        info.textProperty().bind(Bindings.when(isFrisbeeVisuallyCollidingWithTarget)
                .then("Target caught frisbee!").otherwise(""));
        root.getChildren().addAll(info, target, frisbee);

        return root;
    }

    private void playGame() {
        frisbee.setOnMouseReleased(event -> {
            //Starting point for the line
            double fromX = frisbeeX - frisbee.getRadius();
            double fromY = frisbeeY - frisbee.getRadius();

            //Only "throw" the frisbee if the user has released outside of the frisbee itself
            if(frisbee.getBoundsInParent().contains(event.getSceneX(), event.getSceneY())){
                return;
            }

            //Create a path between the frisbee and released location
            Line line = new Line(fromX, fromY, event.getSceneX(), event.getSceneY());
            transition = new PathTransition(Duration.seconds(1), line, frisbee);
            transition.setAutoReverse(true); //Set the node to reverse along the path
            transition.setCycleCount(2); //2 cycles, first to navigate the path, second to return
            frisbee.relocate(0, 0); //Allow the path to control the location of the frisbee

            RotateTransition rotateTransition =
                    new RotateTransition(Duration.seconds(1), frisbee);
            rotateTransition.setByAngle(360f);
            rotateTransition.setCycleCount(2);
            rotateTransition.setAutoReverse(true);

            rotateTransition.play();
            transition.play();
        });
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Scene scene = new Scene(createContent());

        primaryStage.setTitle("Frisbee Toss");
        primaryStage.setScene(scene);
        primaryStage.show();

        playGame();
    }
}

drag-and-drop implementation:

The only difference is from the above is within the playGame method:

private void playGame() {
    frisbee.setId("frisbee");

    frisbee.setOnDragDetected(event -> {
        Dragboard db = frisbee.startDragAndDrop(TransferMode.ANY);
        ClipboardContent content = new ClipboardContent();
        // Store node ID in order to know what is dragged.
        content.putString(frisbee.getId());
        db.setContent(content);
        event.consume();
    });

    root.setOnDragOver(event -> {
        event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
        event.consume();
    });

    root.setOnDragDropped(event -> {
        //Starting point for the line
        double fromX = frisbeeX - frisbee.getRadius();
        double fromY = frisbeeY - frisbee.getRadius();

        //Only "throw" the frisbee if the user has released outside of the frisbee itself
        if(frisbee.getBoundsInParent().contains(event.getSceneX(), event.getSceneY())){
            return;
        }

        //Create a path between the frisbee and released location
        Line line = new Line(fromX, fromY, event.getSceneX(), event.getSceneY());
        transition = new PathTransition(Duration.seconds(1), line, frisbee);
        transition.setAutoReverse(true); //Set the node to reverse along the path
        transition.setCycleCount(2); //2 cycles, first to navigate the path, second to return
        frisbee.relocate(0, 0); //Allow the path to control the location of the frisbee

        transition.setOnFinished(finishedEvent -> {
            event.setDropCompleted(true);
            event.consume();
        });
        transition.play();
    });
}


Adding rotation:

The rotation can be applying by pre-pending the below snippet before playing the PathTransition:

RotateTransition rotateTransition = new RotateTransition(Duration.seconds(1), frisbee);
rotateTransition.setByAngle(360f);
rotateTransition.setCycleCount(2);
rotateTransition.setAutoReverse(true);
rotateTransition.play();

You can make the rotation more notice-able by applying a GradientFill to the Frisbee, as opposed to a block color

E.g:

frisbee.setFill(new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE,
    new Stop[] { new Stop(0, Color.BLACK), new Stop(1, Color.GREEN)}));


Visual output

Order: mouseReleased | drag-and-drop | mouseReleased with rotation

(Note the cursor change in the drag-and-drop implementation)

Frisbee Released Frisbee Drag Frisbee Released with rotation

Community
  • 1
  • 1
Peter
  • 1,592
  • 13
  • 20