少し前に「[Unity] Rigidbodyの軌道を自前で計算する」という記事を書きました。
Rigidbodyを追加すると自動でやってくれる物理的な挙動を簡易的に計算して、飛んで行く方向などを事前に計算し軌道を表示する、というサンプルです。
方向や軌跡を見せるだけならこれで問題ないのですが、実際にどこにあたるのかを知ろうとしても衝突判定まではやっていないので分かりません。
今回はこの軌道に加えて、実際にどこに当たるのかを事前に計算する方法を紹介します。
Unityの物理シミュレーションの時間を進めるのではなく、擬似的に時間を進めて位置を特定するため、すぐに結果を得ることができます。
Unity自体にこういう機能あってもいいと思っているので、もしかしたらUnityの基本機能でできるかもしれません。
(が、見つからなかったので作ってみた感じです。負荷が高いかもしれないのでご利用は計画的に)
もし「こうやれば簡単だよ」っていう情報があれば教えて下さい;
例によってサンプルはGithubに上げてあります。
今回利用したメソッド
今回の実装は Rigidbody
の以下のふたつのメソッドを使いました。
SweepTest
public bool SweepTest(Vector3 direction, out RaycastHit hitInfo, float maxDistance = Mathf.Infinity);
SweepTestは引数を見ても分かるように、「そのオブジェクトがその方向に進んだらなにかとぶつかるか」を bool
で返してくれるメソッドです。
さらに RaycastHit
オブジェクトを渡すことで、レイキャストのように「どのオブジェクトのどこにあたったか」という情報まで与えてくれます。
参考: Unity Document
MovePosition
public void MovePosition(Vector3 position);
こちらは剛体(Rigidbody)を移動させるメソッド。
基本的に物理演算対象のオブジェクトの transform
は直接操作しません。
こちらのメソッドを使うと剛体を任意の位置に移動することができます。
ただし、該当の場所で衝突が起こる場合は適切に位置を補正してくれます。
(なので壁の中にめり込まない位置に移動してくれたりする)
参考: Unity Document
フロー
検出の仕組みは以下のフローになっています。
- ある地点、ある方向への力を定義
- オブジェクトに力を与え、自身で設定したタイムインターバルごとに位置をシミュレート
-
MovePosition
を使ってシミュレート位置に剛体を移動する - 各位置ごとに
SweepTest
を実行し、他オブジェクトとの衝突判定を行う - (4)を繰り返し、衝突が検出されるか、指定回数の試行回数を超えるまで実行する
- 検知された最初の位置をイベントで通知する
という流れになっています。
ある地点、ある方向への力を定義
地点は物理的に移動させたいオブジェクトの場所です。力についてはゲームの内容やどうしたいかによりますが、実際に適用する予定の力を指定します。(例えばゴルフゲームのボールに加える力など)
位置のシミュレート
位置の計算については以前の記事をご覧ください。
剛体の位置を更新する
剛体の位置は MovePosition
を利用して行います。
なお、この位置の移動は計算上の目的のため視覚的になにかを移動する目的では利用しません。
// (2)で計算した位置に移動させる
Vector3 pos = CalcPositionFromForce(time, mass, startPosition, force, gravity);
m_rigidbody.MovePosition(pos);
SweepTestで衝突チェック
位置を移動したら、その位置から少し先までの間に衝突が検知されるかをチェックします。
// 衝突した場合の情報格納用
RaycastHit hit;
// `SweepTest` で衝突をチェック
if (m_rigidbody.SweepTest(direction, out hit, distance)) {
// 無視するオブジェクトの場合は処理をスキップ
foreach (GameObject ignore in IgnoreObjects) {
if (hit.collider.gameObject == ignore) {
needIgnore = true;
break;
}
}
// Continue the loop if collider is contained in ignore objects.
if (needIgnore) {
time += interval;
frame++;
continue;
}
if (OnDetect != null) {
OnDetect(hit.point);
}
Destroy(gameObject);
return;
}
こうして徐々に位置を進めていき、衝突が検知されたらそれをイベントとして通知して処理を抜けます。
見てもらうと分かりますが、だいぶ重い処理をループしているので処理を間引いたり、必要なときだけ計算を行うなどの処置が必要かもしれません。
コード全体はGithubのサンプルをご覧ください。