1. ホーム
  2. javafx-2

[解決済み] JavaFXで図形の衝突を確認する

2022-02-25 23:13:18

質問

衝突判定を行おうとしています。このテストでは、単純な矩形の Shape をチェックし、その Bound というように、衝突しているかどうかを判断します。しかし、この検出は期待したように動作しません。オブジェクトの移動方法(relocate, setLayoutX,Y)やバウンドチェック(boundsInLocal, boundsInParrentなど)をいろいろ試してみましたが、まだうまくいきません。ご覧のように、検出は1つのオブジェクトに対してのみ機能し、3つのオブジェクトがある場合でも、1つだけが衝突を検出します。以下は、この問題を示す動作中のコードです。

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;
    }
}

解決方法は?

checkBoundsルーチンにわずかなロジックエラーがあるようです。(boundsに基づいて)正しく衝突を検出しているのに、同じルーチンでその後の衝突チェックを行うと、ブロックのfillが上書きされます。

これは、衝突が検出されたことを忘れることがないようにフラグを追加するものです。

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);
  }
}

あなたが行っているチェック(親の境界に基づいて)は、同じ親グループ内のノードの可視境界を囲む矩形の交点を報告することに注意してください。

代替実装

必要であれば、元のサンプルを更新して、ビジュアルシェイプのバウンディングボックスではなく、ノードのビジュアルシェイプに基づいてチェックできるようにしました。 これにより、円のような非矩形形状の衝突を正確に検出することができます。 このキーとなるのは Shape.intersects(シェイプ1, シェイプ2) メソッドを使用します。

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; }
}

サンプルプログラムの出力です。 このサンプルでは、円がドラッグされ、ユーザーは現在、他の円と衝突するようにマークされた円をドラッグしています(青く塗られています) - デモの目的のために、現在ドラッグされている円だけが衝突の色でマークされています。

追加質問に基づくコメント

貼ったリンク先が 交差点デモアプリケーション は、特定のタイプの衝突検出のサンプルとしてではなく、さまざまな境界タイプの使用方法を説明するためのものでした。 あなたの用途では、チェンジリスナーやさまざまな種類の境界タイプをチェックするような複雑な機能は必要なく、1つのタイプに決めるだけで十分です。 ほとんどの衝突検出は、レイアウト境界やノードのローカル境界のような他の JavaFX 境界タイプではなく、視覚的境界の交差にのみ関心があるようです。 ですから、どちらかを選択することができます。

  1. の交差をチェックします。 getBoundsInParent (最初の質問でやったように) これはノードの視覚的な両端を包含する最小の長方形のボックスで動作します。
  2. を使用します。 Shape.intersect(shape1, shape2) Node の視覚的な形状のバウンディングボックスではなく、視覚的な形状に基づいてチェックする必要がある場合は、ルーチンを使用します。
<ブロッククオート

矩形には setLayoutX と translateX のどちらを使うべきでしょうか?

は、その レイアウトX とlayoutYプロパティは、ノードの位置決めやレイアウトを行うためのものです。 また トランスレートX と translateY プロパティは、ノードの視覚的な位置を一時的に変更するためのものです(例えば、ノードがアニメーションの最中である場合など)。 この例では、どちらのプロパティも機能しますが、おそらく translate プロパティよりも layout プロパティを使用する方が良い方法でしょう。 翻訳遷移 の場合、translate の開始と終了の値は、親グループ内の位置ではなく、ノードの現在のレイアウト位置からの相対値であるため、より明確になります。

このレイアウトと平行移動の座標をサンプルで同時に使用するもう一つの方法は、ドラッグ操作の途中でESCキーを使ってキャンセルするような場合です。 レイアウトX,Yをノードの初期位置に設定し、ドラッグ操作を開始してトランスレートX,Yの値を設定し、ユーザーがESCキーを押したらトランスレートX,Yを0に戻してドラッグ操作をキャンセルするか、ユーザーがマウスを離したらレイアウトX,YをレイアウトX,Y+トランスレートX,YにしてトランスレートX,Yを0に戻してください。このアイデアは、ノードの視覚座標を元のレイアウト位置から一時的に変更するのにトランスレートの値を使用することです。

円はアニメーションしていますが、交差はうまくいくのでしょうか?マウスで円をドラッグすることなく、ランダムに移動させたらどうなるのでしょうか。この場合、色も変わるのでしょうか?

これを行うには、衝突検出関数の呼び出しと衝突ハンドラの呼び出しを変更すればよいのです。 上の例のように、マウスのドラッグイベントに基づいて交差をチェックするのではなく、各ノードの boundsInParentProperty() .

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

注:たくさんの図形をアニメーションさせる場合、衝突のチェックはフレームごとに ゲームループ は、ノードが移動するたびに衝突チェックを実行するよりも効率的です(上記のboundsInParentProperty変更リスナーで実行されているように)。

非矩形形状の入力処理に関する追加情報

衝突検出ではなく、入力検出については、ご質問とは直接関係ありませんが、以下のサイトをご覧ください。 node.pickOnBounds の設定は、非矩形のノードでマウスやタッチのインタラクションが必要な場合に使用します。