2Dゲームメモ 〜ユニットの移動

2Dゲームメモ。ユニットの移動方法について。

ユニットの移動ロジック

目標に向かって、一定のスピードで直進

点Aから点Bに向かって、一定のスピードで進む

  • ユニットの移動方法

ABベクトルの単位ベクトルにユニットのスピード(1フレームあたりに進む距離)をかけたものが、移動ベクトルになります。

(1)ABベクトルの距離を求め、一定以下の場合Bに到達。そうでなければ(2)へ

(2)ABベクトルの単位ベクトルを求める

(3)求めた単位ベクトルにユニットのスピードをかける

(4)点Aに1フレームあたりに進むベクトルを加算して移動する

(5)(1)へ

  • サンプル

以下のようなベクトルを扱うクラスを用意すると計算がらくです。
Vector2D.java

import android.graphics.PointF;

public class Vector2D extends PointF {
	public static final Vector2D LEFT  = new Vector2D(-1f, 0);
	public static final Vector2D UP    = new Vector2D(0, -1f);
	public static final Vector2D RIGHT = new Vector2D(1f, 0);
	public static final Vector2D DOWN  = new Vector2D(0, 1f);
	
	public Vector2D() {
		super();
	}
	
	public Vector2D(float x, float y) {
		super(x, y);
	}
	
	public Vector2D(PointF vec) {
		super(vec.x, vec.y);
	}
	
	public Vector2D(Vector2D vec) {
		super(vec.x, vec.y);
	}
	
	public Vector2D(PointF from, PointF to) {
		super(to.x - from.x, to.y - from.y);
	}
	
	public Vector2D set(PointF from, PointF to) {
		this.set(to.x - from.x, to.y - from.y);
		return this;
	}
	
	public Vector2D normalize() {
		float len = length();
		x = x / len;
		y = y / len;
		return this;
	}

	public void offset(PointF offset) {
		offset(offset.x, offset.y);
	}
	
	public float dot(PointF vec) {
		return x * vec.x + y * vec.y;
	}
	
	public Vector2D times(float pow) {
		x = x * pow;
		y = y * pow;
		return this;
	}
	
	public float distance(PointF to) {
		return length(to.x - x, to.y - y);
	}
	
	public static float distance(PointF from, PointF to) {
		return length(to.x - from.x, to.y - from.y);
	}
	
	public Vector2D add(float ax, float ay) {
		x += ax;
		y += ay;
		return this;
	}
	
	public Vector2D dec(float ax, float ay) {
		x -= ax;
		y -= ay;
		return this;
	}

	public Vector2D add(PointF add) {
		return this.add(add.x, add.y);
	}
	
	public Vector2D dec(PointF dec) {
		return this.dec(dec.x, dec.y);
	}

	public boolean isEquals(Vector2D vec) {
		if(vec == null)
			return false;
		if(x == vec.x && y == vec.y)
			return true;
		return false;
	}
}
  • sample

PointF targetA・・・点A
PointF targetB・・・点B
float unitSpeed・・・ユニットのスピード

// ABベクトル
Vector2D vectorAB = new Vector2D(targetA, targetB);

// 長さが閾値(unitSpeed)以下のとき
if(vectorAB.length() < unitSpeed) {
    targetA.set(targetB.x, targetB.y);
    // 到達状態の処理を行う
    ....
}

// 単位ベクトルを求め、スピードをかける
vectorAB.normalize().times(unitSpeed);

// targetAを移動する
targetA.offset(vectorAB.x, vectorAB.y);
回転しながらターゲットの方向を向く

移動ではありませんが、ターゲットを追跡するときの基本になるので・・・。回転砲台などで使います。
ここでは内積の性質を利用します。

(1)ABベクトルとACベクトルの角度 < 90
内積 > 0
 ABベクトルとACベクトルの長さが等しく、内積 == 1のとき、ABベクトルとACベクトルが一致
(2)ABベクトルとACベクトルが垂直
内積 == 0
(3)ABベクトルとACベクトルの角度 > 90
内積 < 0
 ABベクトルとACベクトルの長さが等しく、内積 == -1のとき、ABベクトルとACベクトルが逆向きで一致

  • ユニットの回転方法

uベクトル・・・ユニットの向き
vベクトル・・・uベクトルを右に90度回転させたベクトル

vベクトルとABベクトルが垂直(内積==0)となるようにuベクトルを回転させます。
uベクトルの向きがABベクトルと間逆になる場合もありうるので、uベクトルとABベクトルの内積>0のチェックも行います。

(1)ABベクトル、ユニットAの向きuベクトルとuベクトルに垂直なvベクトルを取得
 右に90度回転させた垂直なベクトルは回転行列より、u(X, Y) → v(-Y, X)のように求められます。
(2)uベクトルとABベクトルの内積(uAB)、vベクトルとABベクトルの内積(vAB)を求める

(3)uAB > 0かつvAB == 0のとき、終了(実際にはvABが閾値以下のときuベクトルをABベクトルに重ねる)。それ以外のとき、(4)へ

(4)uAB、vABに応じて回転
 回転行列についてはhttp://ja.wikipedia.org/wiki/%E5%9B%9E%E8%BB%A2%E8%A1%8C%E5%88%97を参考にしてください。

(5)(1)へ

  • サンプル

Vector2D vectorAB・・・ABベクトル
Vector2D uVector・・・ユニットの向き。単位ベクトル
double degree・・・回転角

public void lockOnTarget(Vector2D vectorAB, Vector2D uVector) {

    // vベクトル(uVectorを右に90°回転)
    Vector2D vVector = new Vector2D(-uVector.y, uVector.x);

    // 内積
    float uAB = uVector.dot(vectorAB);
    float vAB = vVector.dot(vectorrAB);

    if(uAB > 0) {
        // vAB == 0だとなかなか重ならないので、閾値以下で重ならせる
        if(Math.abs(vAB) < 閾値) {
            // ユニットの向きが一致
            uVector.set(vectorAB).normalize();
        } else if(vAB > 0) {
            // 右回転
            rotateVector(uVector, degree);
            uVector.normalize();
        } else {
            // 左回転
            rotateVector(uVector, -degree);
            uVector.normalize();
        }
    } else {
        if(vAB > 0) {
            // 左回転
            rotateVector(uVector, -degree);
            uVector.normalize();
        } else {
            // 右回転
            rotateVector(uVector, degree);
            uVector.normalize();
        }
    }
}

private void rotateVector(Vector2D vec, double degree) {
    double radian = Math.toRadians(degree);
    float x = vec.x;
    float y = vec.y;
    vec.x = (float)(Math.cos(radian)*x - Math.sin(radian)*y);
    vec.y = (float)(Math.sin(radian)*x + Math.cos(radian)*y);
}

Canvasで描画を行うような場合、Y軸が反転している点に注意してください。
また、こんな感じにするとif文が少なくなります。

    rotateVector(uVector, uAB*vAB/Math.abs(uAB*vAB)*degree);
    uVector.normalize();
ターゲットをホーミングする

ターゲットを追いかける移動方法
「回転しながらターゲットの方向を向く」動作に「目標に向かって、一定のスピードで直進」を組み合わせるだけです。

  • サンプル

PointF targetA・・・点A
PointF targetB・・・点B
float unitSpeed・・・ユニットのスピード
Vector2D uVector・・・ユニットの向き

// ABベクトル
Vector2D vectorAB = new Vector2D(targetA, targetB);

// 長さが閾値(unitSpeed)以下のとき
if(vectorAB.length() < unitSpeed) {
    targetA.set(targetB.x, targetB.y);
    // 到達状態の処理を行う
    ....
}

// ユニットの向きを調整
lockOnTarget(vectorAB, uVector);

// ユニットの向きに進むベクトルを求める
float dx = uVector.x * unitSpeed;
float dy = uVector.y * unitSpeed;

// targetAを移動する
targetA.offset(dx, dy);


実際に使用する場合は、インスタンスを極力生成しないようにする必要があります。work変数を用意して、ガーベジコレクタが動作を抑えるようにしてください。