2017年4月27日

Unity 地表に沿ってオブジェクトを配置する


使いたいと思ったときに「あれ…どうやるんだっけ…」となりがちなRaycastの基本的な使い方をメモメモ…


※このページの内容の動作確認にはUnity5.3を使用しています。
スーパーマリオ64に登場する「取れないコイン」の謎を解明するこの動画を見ていて、無性に坂道にオブジェクトを配置したくなってしまいました。というわけで、手っ取り早くRaycastで実装してみたいと思います。

動画で紹介されるスーパーマリオ64のコインの配置方法

やることは単純で、オブジェクトの真下にRay(まっすぐな光線)を発射し、地面に当たればその位置にオブジェクトを移動するだけです。

Physics.Raycast


public float distanceFromSurface;
public LayerMask targetLayer;
RaycastHit hitInfo;
if (Physics.Raycast(transform.position, Vector3.down, out hitInfo, Mathf.Infinity, targetLayer))
{
 Vector3 newPos = transform.position;
 newPos.y = hitInfo.point.y + distanceFromSurface;
 transform.position = newPos;
}

Physics.Raycast( Rayの発射開始位置, Rayの発射方向, out 衝突した点の情報を入れるRaycastHit構造体の変数, Rayが届く距離, 対象のレイヤー )で、Rayが何かしらのコライダーにぶつかるとtrueを返します。距離の指定以降は特に必要がなければ省略してOK。距離はデフォルトでMathf.Infinity(無限)に設定されます。

Rayが地表に衝突すれば、その衝突地点のY座標(hitInfo.point.y)+指定した高さ(distanceFromSurface)の位置にオブジェクトを移動します。

あらかじめRayを作成して、開始地点と方向だけ先に決めておくこともできます。

RaycastHit hitInfo;
Ray ray = new Ray(transform.position, Vector3.down);
if (Physics.Raycast(ray, out hitInfo, Mathf.Infinity, targetLayer))
{
 ...
}

わー!

Physics.RaycastAll


Physics.RaycastはRayがどこかしらに衝突した時点でtrueを返し終了しますが、場合によってはRayの直線上にあるオブジェクトをすべて取得したいときもあります。そういうときに使えるのがPhysics.RaycastAllです。

public string targetTag;
RaycastHit[] hitInfo;
hitInfo = Physics.RaycastAll(transform.position, Vector3.down);
foreach (RaycastHit hit in hitInfo)
{
 if (hit.transform.gameObject.CompareTag(targetTag))
 {
  Vector3 newPos = transform.position;
  newPos.y = hit.point.y + distanceFromSurface;
  transform.position = newPos;
 }
}

まずhitInfoが配列になっていることに注意。Physics.Raycastは戻り値が「衝突した」か「衝突してない」かのbool値でしたが、Physics.RaycastAllは衝突した各地点の情報をすべて返すので、それをhitInfoで受け取っているわけです。ちなみに配列に入る順番はRayがぶつかった順番とは限らないので、そういう前提で使うのは避けましょう。

あとはhitInfoに格納されている情報をひとつひとつ取り出し、ゲームオブジェクトを取得。設定されたタグ名と一致しているか確認し、一致している場合のみ処理を続行する…という方法をとっています。おそらくパフォーマンス的にはレイヤーを使ったほうが良いと思いますが、なんとなくタグ名とかゲームオブジェクト名とかで指定したほうが地に足ついた感じがありますよね。いや、ないか。

Physics.SphereCast


RaycastやRaycastAllは、1点をある方向に伸ばしたものでしたが、それだとマズい場合もあります。


中心点の真下しか調べることができないため、画像のような隙間に衝突地点がある状態だと、オブジェクトを移動したときにめり込みます。このめり込みを回避する方法はいろいろあると思いますが、簡単なのがSphereCastです。

public float radius;
if (Physics.SphereCast(transform.position, radius, Vector3.down, out hitInfo, Mathf.Infinity, targetLayer))
{
 ...
}

変わったのは引数にradius(半径)が追加されたことだけです。これで1点のみのRayを発射するかわりに、半径を持った球体を発射して衝突地点を取得することができます。あとはオブジェクトの大きさに合わせた半径を指定しておけば、めり込みは避けられます。

ピタッ

球を発射するShpereCastのほかに、CapsuleCast(カプセル型)やBoxCast(ボックス型)もあります。引数が違うだけで、使い方はほぼ同じです。もちろんRaycastAllのようにそれぞれShpereCastAll、CapsuleCastAll、BoxCastAllもあります。詳しくはPhysicsのドキュメントで。

ちなみに、冒頭で紹介した動画、スーパーマリオ64で特定のコインが取れない理由は「コインの初期位置が坂道より下に設定されていた場合、配置するべき地面が見つけられないので、そのコインだけアンロードされてしまうから」でした。

0 件のコメント:

コメントを投稿