【Java】自由曲線Pathの利用と曲線を含む図形と矩形の当たり判定

2018年1月3日

本稿はJavaで自由曲線、曲線を含む図形を表現できるパスについて紹介します。

moveToで座標移動

現在の座標を移動したいときに利用します。

moveTo(移動先x座標,移動先y座標)

と指定しましょう。

import java.awt.geom.Path2D;

public class Test {
	public static void main(String[] args) {
		Path2D.Double p = new Path2D.Double();
		p.moveTo(100, 100);
	}
}

lineToで線を引く

現在のパスの位置から線を描きます。

lineTo(x座標,y座標)

と定義しましょう。

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Path2D;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Test {
	public static void main(String[] args) {
		TestWindow tw = new TestWindow("テスト", 400, 300);
		tw.add(new DrawCanvas());
		tw.setVisible(true);
	}
}
//ウィンドウクラス
class TestWindow extends JFrame{
	public TestWindow(String title, int width, int height) {
		super(title);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setSize(width,height);
		setLocationRelativeTo(null);
		setResizable(false);
	}
}

class DrawCanvas extends JPanel{
	public void paintComponent(Graphics g){
		Graphics2D g2 = (Graphics2D)g;
		Path2D.Double p = new Path2D.Double();
		p.moveTo(100, 100);
		p.lineTo(300, 100);
		g2.draw(p);
	}
}
実行結果

quadToで曲線を引く

一つの制御点からなる曲線を描きます。

quadTo(制御点x,制御点y,終点x,終点y)

と定義します。

ペイントソフトのパスツールを使った時の制御点を指定しているようなものですね。

制御点を指定した位置から線を引っ張って曲げているようなイメージです。

始点と終点は必ず通るように描かれます。

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Path2D;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Test {
	public static void main(String[] args) {
		TestWindow tw = new TestWindow("テスト", 400, 300);
		tw.add(new DrawCanvas());
		tw.setVisible(true);
	}
}
//ウィンドウクラス
class TestWindow extends JFrame{
	public TestWindow(String title, int width, int height) {
		super(title);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setSize(width,height);
		setLocationRelativeTo(null);
		setResizable(false);
	}
}

class DrawCanvas extends JPanel{
	public void paintComponent(Graphics g){
		Graphics2D g2 = (Graphics2D)g;
		Path2D.Double p = new Path2D.Double();
		p.moveTo(180, 100);
		p.quadTo(300, 150, 180 , 200);
		g2.draw(p);
	}
}
実行結果

curveToで曲線を引く

二つの制御点からなる曲線を描きます。

curveTo(制御点x1,制御点y1,制御点x2,制御点y2,終点x,終点y)

と定義します。

quadToの引っ張る箇所を増やしたバージョンですね。

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Path2D;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Test {
	public static void main(String[] args) {
		TestWindow tw = new TestWindow("テスト", 400, 300);
		tw.add(new DrawCanvas());
		tw.setVisible(true);
	}
}
//ウィンドウクラス
class TestWindow extends JFrame{
	public TestWindow(String title, int width, int height) {
		super(title);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setSize(width,height);
		setLocationRelativeTo(null);
		setResizable(false);
	}
}

class DrawCanvas extends JPanel{
	public void paintComponent(Graphics g){
		Graphics2D g2 = (Graphics2D)g;
		Path2D.Double p = new Path2D.Double();
		p.moveTo(180, 100);
		p.curveTo(300, 100, 300, 200, 180 , 200);
		g2.draw(p);
	}
}
実行結果

closePathで始点まで線を引く

closePathメソッドを利用すると現在の座標から始点の座標までを直線で繋げることができます。

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Path2D;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Test {
	public static void main(String[] args) {
		TestWindow tw = new TestWindow("テスト", 400, 300);
		tw.add(new DrawCanvas());
		tw.setVisible(true);
	}
}
//ウィンドウクラス
class TestWindow extends JFrame{
	public TestWindow(String title, int width, int height) {
		super(title);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setSize(width,height);
		setLocationRelativeTo(null);
		setResizable(false);
	}
}

class DrawCanvas extends JPanel{
	public void paintComponent(Graphics g){
		Graphics2D g2 = (Graphics2D)g;
		Path2D.Double p = new Path2D.Double();
		p.moveTo(150, 100);
		p.lineTo(250, 150);
		p.lineTo(150, 200);
		p.closePath();
		g2.draw(p);
	}
}
実行結果

containsメソッド

containsメソッドを利用するとPath2Dで描いた図形の中に点が含まれているか判定できます。

Path2Dオブジェクト.contains(x座標,y座標)と定義しましょう。

注意しなければならないのはpathで描いた線が内側だと判断されればtrueになる点です。

例えば、pathが閉じられてなくてもtrueを返す可能性はあります。

import java.awt.geom.Path2D;
public class Test {
	public static void main(String[] args) {
		Path2D.Double path = new Path2D.Double();
		path.moveTo(100, 100);
		path.curveTo(120, 100, 200, 120, 120, 150);
		path.closePath();
		System.out.println(path.contains(120,120));
	}
}
実行結果

true

intersectsメソッド

intersectsメソッドを利用すれば曲線を含んだ図形と矩形の当たり判定ができます。

Path2Dオブジェクト.intesects(Rectangleオブジェクト)と定義しましょう。

こちらもパスの内部領域がどうなっているかによって思った通りの処理にならない可能性がありますので、その点は注意して使用したほうがよさげ。

恐らく他のシンプルな当たり判定よりも計算量が多いはずなので速度的な意味で多用は避けましょう。

それではサンプルをご覧ください。

import java.awt.Rectangle;
import java.awt.geom.Path2D;

public class Test {
	public static void main(String[] args) {
		Path2D.Double path = new Path2D.Double();
		path.moveTo(100, 100);
		path.curveTo(120, 100, 200, 120, 120, 150);
		path.closePath();
		Rectangle r = new Rectangle(100, 100, 50, 50);

		System.out.println(path.intersects(r));
	}
}
実行結果

true

曲線図形と矩形をアニメーションさせながら判定できているか確かめる

実際にアニメーションさせながら当たり判定できているか確認してみましょう。

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;

import javax.swing.JFrame;
import javax.swing.JPanel;
public class Test {
	public static void main(String[] args) {
		TestWindow tw = new TestWindow("テスト", 400, 300);
		tw.change(new DrawCanvas());
		tw.setVisible(true);
		tw.startGameLoop();

	}
}
//ウィンドウクラス
class TestWindow extends JFrame implements Runnable{
	private Thread th = null;
	private double sleepAddTime;
	private int fps=60;
	public TestWindow(String title, int width, int height) {
		super(title);
		setDefaultCloseOperation(EXIT_ON_CLOSE);
		setSize(width,height);
		setLocationRelativeTo(null);
		setLayout(null);
		setResizable(false);
		setFps(fps);
	}
	public synchronized void change(JPanel p) {
		getContentPane().removeAll();
		Insets inset = getInsets();
		p.setBounds(-inset.left,-inset.top,getWidth(),getHeight());
		add(p);
		validate();
		repaint();
	}
	public synchronized void startGameLoop(){
		if ( th == null ) {
			th = new Thread(this);
			th.start();
		}
	}
	public synchronized void stopGameLoop(){
		if ( th != null ) {
			th = null;
		}
	}
	public void run(){
		double nextTime = System.currentTimeMillis() + sleepAddTime;
		while(th != null){
			try{
				long res = (long)nextTime - System.currentTimeMillis();
				if ( res < 0 ) res = 0;
				Thread.sleep(res);
				repaint();
				nextTime += sleepAddTime;
			}catch(InterruptedException e){
				e.printStackTrace();
			}
		}
	}
	public void setFps(int fps){
		if ( fps < 10 || fps > 60 ) {
			throw new IllegalArgumentException("fpsの設定は10~60の間で指定してください。");
		}
		this.fps = fps;
		sleepAddTime = 1000.0 / fps;
	}
}

//移動アニメーション用クラス
class VertexMove2D{
	private double x;
	private double y;
	private final int[] xpoints;
	private final int[] ypoints;
	private final int npoints;
	private boolean isInvert;
	private int currentPoint;
	private int nextPoint;
	private int speed;
	private double mx;
	private double my;
	private double checkValue;
	public VertexMove2D(int[] xpoints, int[] ypoints, int npoints, int speed, int initPoint) {
		this.xpoints = xpoints;
		this.ypoints = ypoints;
		this.npoints = npoints;
		this.speed = Math.abs(speed);
		this.isInvert = speed < 0;
		this.currentPoint = isInvert ? initPoint + 1 : initPoint - 1;
		prepareNext();
	}
	private void prepareNext() {
		if ( isInvert ) {
			int currentPoint = this.currentPoint - 1;
			if ( currentPoint < 0 ) currentPoint = npoints - 1;
			this.currentPoint = currentPoint;
			int nextPoint = currentPoint - 1;
			this.nextPoint = nextPoint < 0 ? npoints - 1 : nextPoint;
		} else {
			int currentPoint = this.currentPoint + 1;
			if ( currentPoint >= npoints ) currentPoint = 0;
			this.currentPoint = currentPoint;
			this.nextPoint = (currentPoint + 1) % npoints;
		}
		int sx = xpoints[currentPoint];
		int sy = ypoints[currentPoint];
		int ex = xpoints[nextPoint];
		int ey = ypoints[nextPoint];
		int vx = ex - sx;
		int vy = ey - sy;
		double rad = Math.atan2(vy, vx);
		mx = Math.cos(rad) * speed;
		my = Math.sin(rad) * speed;
		checkValue = vx * vx + vy * vy;
		x = sx;
		y = sy;
	}
	public void draw(Graphics g) {
		g.drawPolygon(xpoints, ypoints, npoints);
	}
	public void fill(Graphics g) {
		g.fillPolygon(xpoints, ypoints, npoints);
	}
	public void move() {
		double xx = x + mx;
		double yy = y + my;

		int sx = xpoints[currentPoint];
		int sy = ypoints[currentPoint];
		double vx = sx - xx;
		double vy = sy - yy;
		if ( checkValue <= vx * vx + vy * vy ) {
			prepareNext();
		} else {
			x = xx;
			y = yy;
		}
	}
	public void invert() {
		isInvert = !isInvert;
	}
	public boolean isInvert() {
		return isInvert;
	}
	public int getSpeed() {
		return speed;
	}
	public void setSpeed(int speed) {
		if ( speed < 0 ) {
			invert();
			speed = -speed;
		}
		this.speed = speed;
	}
	public double getX() {
		return x;
	}
	public double getY() {
		return y;
	}
}
class DrawCanvas extends JPanel{
	int[] x = {60,150,280,340,270,160,90,70};
	int[] y = {80,200,90,120,150,250,240,260};

	//図形の頂点を移動するオブジェクト。
	VertexMove2D m1 = new VertexMove2D(
			x
			,y
			,8//頂点の数
			,2//移動速度
			,0//配列の0番を開始地点に
	);
	//もう一つの図形の頂点を移動するオブジェクト。
	VertexMove2D m2 = new VertexMove2D(
			x
			,y
			,8//頂点の数
			,-1//移動速度
			,4//配列の4番を開始地点に
	);
	Path2D.Double r1 = new Path2D.Double();
	Rectangle r2 = new Rectangle(100, 100, 50, 50);
	{
		//パスを描く
		r1.moveTo(100, 100);
		r1.curveTo(120, 100, 200, 120, 120, 150);
		r1.closePath();
	}
	public void paintComponent(Graphics g){
		Graphics2D g2 = (Graphics2D)g;
		g.setColor(Color.RED);
		g2.draw(r1);
		g.setColor(Color.BLUE);
		g2.draw(r2);
		g.setColor(Color.GREEN);
		m1.draw(g2);

		m1.move();
		Rectangle r1_rect = r1.getBounds();
		r1.transform(AffineTransform.getTranslateInstance(
				m1.getX()-r1_rect.getCenterX()
				,m1.getY()-r1_rect.getCenterY()
			)
		);

		m2.move();
		r2.x = (int) (m2.getX()-r2.width / 2);
		r2.y = (int) (m2.getY()-r2.height / 2);

		g.setFont(new Font(Font.MONOSPACED,Font.BOLD,20));
		if ( r1.intersects(r2) ) {
			g.setColor(Color.RED);
			g.drawString("当たっています", 50,50);
		} else {
			g.setColor(Color.BLUE);
			g.drawString("当たってません", 50,50);
		}
	}
}
実行結果

これでちゃんと当たり判定ができていることが確認できました。