マウスクリック(スマホではタップ)したところの3Dオブジェクトを取得する

2022年11月21日

クリックしたオブジェクトを取得して、何か処理したい時の手順について考えましょう

マウスがクリックされたかを知る

マウスの左ボタンがクリックされたか(スマホの場合はタップ)を知るメソッドです
trueなら押された、falseなら押されていないことがわかります
通常Updateメソッドとともに使います

Input.GetMouseButtonDown(0);

マウスクリックした座標の取得

クリックした場所は、スクリーン座標としてx、yの値を取得できるメソッドが用意されていますのでこれを使います

Vector3 mousePosition = Input.mousePosition;

マウスが押されたら、座標を取得する

マウスの左ボタンを押されたらtrueを返すメソッドを使って判断します

bool isMouseClick = Input.GetMouseButtonDown(0);

if (isMouseClick)
{
    Vector3 mousePosition = Input.mousePosition;

    Debug.Log(mousePosition);
}

結果のサンプル

x軸は966, y軸は499, z軸は0)となっているのがわかります
スクリーン座標なのでz軸は常に0になります

(966.00, 499.00, 0.00)

マウスが押されたら、座標を取得することを常に確認する

Updateメソッドに記述することで、マウスが押されたら処理できるように準備します

void Update()
{
    bool isMouseClick = Input.GetMouseButtonDown(0);

    if (isMouseClick)
    {
        Vector3 mousePosition = Input.mousePosition;

        Debug.Log(mousePosition);
    }
}

マウスが押された先にある3Dゲームオブジェクトを知る

Rayオブジェクト

Unityには、見えないですがベクトルを表すオブジェクトが用意されていますので、これを使います

マウスの先にある3Dオブジェクトとは

ここで使うRayについて考えてみましょう
矢印のスタートポイントは、カメラの位置でいいでしょう
矢印の向きですが、カメラの位置→マウスの位置で取得できそうです
ここで、カメラの奥行き(z軸)はマイナスであるとします(標準では-10となっています)
マウスの位置は、上記のように奥行き(z軸)は0ですね
なので、方向が取得できるのです

よく使う機能なので、Unityではメソッドが用意されています
(mousePositonは上記で得られた値です)

Ray ray = Camera.main.ScreenPointToRay(mousePosition);

Rayの当たった3Dゲームオブジェクトを取得(3Dオブジェクトにはコライダーが必要)

Rayに接触した3Dオブジェクトを取得するメソッドが用意されています

isHitには、接触したかどうかの結果が入ります(trueで接触あり)
hitInfoには、接触したオブジェクトの情報が入ります

bool isHit = Physics.Raycast(ray, out RaycastHit hitInfo);

articulationBody
ヒットされたコライダの ArticulationBody。コライダがアーティキュレーション ボディに接続されていない場合は、NULLになります
barycentricCoordinate
ヒットした三角形の重心座標
collider
ヒットしたコライダー
colliderInstanceID
ヒットしたColliderのインスタンスID
distance
レイの原点から衝突点までの距離
lightmapCoord
衝突点の UV ライトマップ座標
normal
レイがヒットしたサーフェスの法線
point
レイがコライダーにヒットした位置 (ワールド空間)。
rigidbody
ヒットしたコライダーの Rigidbody。コライダーに Rigidbody がアタッチされていない場合は null になります。
textureCoord
コリジョン位置の uv テクスチャ座標
textureCoord2
衝突したセカンダリ UV テクスチャの座標
transform
衝突したコライダーまたは Rigidbody の Transform
triangleIndex
衝突したメッシュの三角形におけるインデックス

ゲームオブジェクトを取得したい場合

GameObject hitObject = hitInfo.collider.gameobject;

動作するテストコード

マウスをクリックして、そこに3Dオブジェクトがあるとその名前が表示されます

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RayCastTest : MonoBehaviour
{
    void Update()
    {
        bool isMouseClick = Input.GetMouseButtonDown(0);

        if (isMouseClick)
        {
            Vector3 mousePosition = Input.mousePosition;

            Debug.Log(mousePosition);

            Ray ray = Camera.main.ScreenPointToRay(mousePosition);

            bool isHit = Physics.Raycast(ray, out RaycastHit hitInfo);

            if (isHit)
            {
                Debug.Log(hitInfo.collider.name);
            }
        }
    }
}

クリックしたところにオブジェクトがあると配列にいれます
オブジェクトがないと配列の要素数が0になります
もし0でなければ、オブジェクトがあるので、0番目のオブジェクトを表示させます
注意ですが、奥にもオブジェクトがあると一番手前が得られるとは限りませんので注意が必要です
これについては後述します

RaycastHit[] hitInfos = Physics.RaycastAll(ray);

if (hitInfos.Length > 0)
{
    Debug.Log(hitInfos[0].collider.name);
}

後ろに隠れている3Dオブジェクトも取得したい場合

上記の手順でほとんどの目的は達せられると思いますが、奥のオブジェクトも取得したい場合についても考えてみましょう

次のコードに置き換えます
RaycastAllメソッドが用意されています
戻り値には接触したすべてのオブジェクトの情報が配列として返されます
なので、確認するためにすべての情報を取り出して、オブジェクトの名前を表示させています

注意事項としては、この配列の順が保証されていないことです
近い順ではありません

RaycastHit[] hits = Physics.RaycastAll(ray);

foreach (var hit in hits)
{
    Debug.Log(hit.collider.name);
}

テストコード

マウスで、3Dオブジェクトをクリックして動作のチェックをすることができます

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RayCastTest : MonoBehaviour
{
    void Update()
    {
        bool isMouseClick = Input.GetMouseButtonDown(0);

        if (isMouseClick)
        {
            Vector3 mousePosition = Input.mousePosition;

            Debug.Log(mousePosition);

            Ray ray = Camera.main.ScreenPointToRay(mousePosition);


            RaycastHit[] hits = Physics.RaycastAll(ray);

            foreach (var hit in hits)
            {
                Debug.Log(hit.collider.name);
            }
        }
    }
}

テスト用のシーン構成

5つのCube(立方体)を奥行きを変えて並べておきます
正面から見ると1つ目以外のオブジェクトは奥に隠れて見えなくなります
マウスでCubeをクリックすると奥のあるオブジェクトもすべて取得できるかを確認できるコードの実験をします

スクリプトは、新しく作った空のゲームオブっジェクトにアタッチしています

結果(実際バラバラ)

近い順に並び替えるには・・

せっかくなので並び替えを考えてみましょう
距離を知ることができるプロパティが用意されていますので、これを利用しましょう

hit.distance

hits配列を並び替えるコードは次のようになります
LINQを学んだ方は理解できると思いますが、まだ、学んでいない方はこれで並び替えができるんだくらいでいいと思います
勉強が進んで理解できるようになったら、その時に把握すればいいでしょう

var orderByHits = hits.OrderBy(hit => hit.distance);
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class RayCastTest : MonoBehaviour
{
    void Update()
    {
        bool isMouseClick = Input.GetMouseButtonDown(0);

        if (isMouseClick)
        {
            Vector3 mousePosition = Input.mousePosition;

            Debug.Log(mousePosition);

            Ray ray = Camera.main.ScreenPointToRay(mousePosition);


            RaycastHit[] hits = Physics.RaycastAll(ray);


            var orderByHits = hits.OrderBy(hit => hit.distance);

            foreach (var hit in orderByHits)
            {
                Debug.Log(hit.collider.name);
            }
        }
    }
}

結果(並び替えられています)

すごいおまけ

汎用的に使えるように取得をメソッドとして独立させるには

クリックしたところのすべてのオブジェクト(奥に隠れているのも含む)を取得するメソッドを自作した場合です
メソッド名(GetMouseClickObjects)を呼び出すと、戻り値としてすべてのオブジェクトが近い順で配列にセットされます

GameObject[] hitObjects = GetMouseClickObjects();

メソッドです
これまでのコードをギュッと凝縮しているだけなのですが、1行になっています
・・・通常ここまでする必要はありません
これまでのコードのように分けて記述する方が初学の時期にはわかりやすいです(行数は増えますが問題ではありません)

private static GameObject[] GetMouseClickObjects() =>
    Physics.RaycastAll(Camera.main.ScreenPointToRay(Input.mousePosition))
    .OrderBy(hit => hit.distance)
    .Select(orderByHits => orderByHits.collider.gameObject)
    .ToArray();

テストコード

テストコードとしてまとめると次のようになります
表示するコードもArrayメソッドを使ったりと短くするようにしていますが、先程と同様、ここまではする必要はありませんから気にしないでくださいね
あくまで、こんなこともできるんだくらいの感じで捉えてもらえればいいです(向学のため)

using System;
using System.Linq;
using UnityEngine;

public class RayCastTest : MonoBehaviour
{
    void Update()
    {

        if (Input.GetMouseButtonDown(0))
        {
            GameObject[] hitObjects = GetMouseClickObjects();

            Array.ForEach(hitObjects, hitObject => Debug.Log(hitObject.name));
        }
    }

    private static GameObject[] GetMouseClickObjects() =>
        Physics.RaycastAll(Camera.main.ScreenPointToRay(Input.mousePosition))
        .OrderBy(hit => hit.distance)
        .Select(orderByHits => orderByHits.collider.gameObject)
        .ToArray();
}

ちなみに、このコードはすべてのオブジェクトを取得していますが、一番手前のオブジェクトだけが欲しい時は次のようにすればいいですね
配列の要素番号0に格納されているオブジェクトがそれに当たりまので・・

GameObject[] hitObjects = GetMouseClickObjects();

Debug.Log(hitObjects[0].name);

クリックしたオブジェクトを削除したい時

上記コードの次にDestroyメソッドを呼び出せば、消えますよ

Destroy(hitInfos[0]);

参考

Unityの座標について

Physics.RaycastNonAlloc(Physics.RaycastAll と似ていますがゴミを発生させません)を使った例

メソッド化をしていますので、接触がない場合はnullを返すようにしています
最初に配列の要素数を登録するため、これ以上、奥行きにオブジェクトがあると取りこぼします
今回は、10個にしています

学習のための解読してみましょう
詳細については解説しません
(違う学習説明になり、学習の焦点が定まらないことになるため)

using System.Linq;
using UnityEngine;

public class RayCastTest : MonoBehaviour
{
    void Update()
    {

        if (Input.GetMouseButtonDown(0))
        {
            GameObject[] hitObjects = GetMouseClickObjects();

            if (hitObjects == null)
            {
                return;
            }

            Debug.Log(hitObjects[0].name);
        }
    }

    private static GameObject[] GetMouseClickObjects()
    {
        RaycastHit[] result = new RaycastHit[10];

        int hitsCount = Physics.RaycastNonAlloc(Camera.main.ScreenPointToRay(Input.mousePosition), result);

        if (hitsCount == 0)
        {
            return null;
        }

        return result.Where(result => result.collider != null)
                     .OrderBy(result => result.distance)
                     .Select(result => result.collider.gameObject)
                     .ToArray();
    }
}

Unity

Posted by hidepon