2020-12-03に投稿
なんでも 3日目

Unity3D上でマウスポインタの位置のグリッド座標を簡単に取得する

はじめに

いつもお世話になっております。
この度アドベントカレンダーに参加したいなと思いまして、
なんでも Advent Calendar 2020に記事を投稿することにしました。
普段はnoteやTwitterで活動しております。

さて、今回のテーマは「Unity3D上でマウスカーソルの位置のグリッド座標を得られるようにする」というものですが、言葉だけつらつら並べてもわかりにくいのでサンプル画像を用意しました。最終的にこんな感じで選択できるようになります。

グリッド座標取得サンプル

この画像のようにマウスカーソルの位置の座標を光らせられるようにしましょう。
(キャラクター移動方法などは省略致します。ご了承下さい。また地形に高低差がある場合、この方法では上手くいかないと思います)

使用する機能

上記を実現する方法としてパッと思いつくのはPlaneやCubeを敷き詰めて全てにコライダーコンポーネントをアタッチし、マウスカーソルが当たっているかどうかを都度計算するという方法ですが、これはコストがかかり過ぎるので実用向きではありません。

ならばどうするかと言うと、Unityが標準でサポートしているGridTilemapを使います。
これはUnity2Dでメインに使われている機能になります。普通はその名の通りタイル(マップチップ)を敷き詰めてフィールドを表現する機能なのですが、これを活用することで3D上でも簡単にグリッド座標を取得することができるようになります。

大まかな手順

大まかには以下の手順で進めていきます。

  • PlaneとGrid、Tilemapオブジェクトの用意
  • Plane上にカーソルが乗っている時、光らせる為の小さいPlane(=SelectPlane)を表示
  • Plane上からカーソルが外れた際、SelectPlaneを非表示にする
  • RaycastでPlane上のマウスカーソルのある位置を取得する
  • その地点をTilemapのWorldToCellメソッドでグリッド座標に変換
  • 更にCellToWorldメソッドでもう一度ワールド座標に変換し、SelectPlaneのpositionに代入

とりあえず以上になります。なおグリッド座標を単純に取得したいだけなら太字部分だけを行えばOKです。

実際の手順

それでは行って参りましょう。

PlaneとGrid、Tilemapオブジェクトの用意

まずは空のGameObjectを一つ作成し、その子要素に「3Dオブジェクト」-「平面」を1枚用意します。大きさや位置などは任意で変更して下さい。
更にGameObjectの階層下に「2Dオブジェクト」-「タイルマップ」を作成します。
(光らせたい方は1グリッドのサイズ/10の大きさで平面をもう一つ作成します=SelectPlane
任意でマテリアルを変更しておきましょう。位置のY座標を0.01程度上げておき、MeshColliderコンポーネントを外しておきます。また、通常は非表示にしておくといいです)
するとヒエラルキータブはこんな感じになるはずです。

ヒエラルキータブ

そしてGridオブジェクトとTilemapオブジェクトを以下のように変更して下さい。

Gridオブジェクト

Tilemapオブジェクト

一応全て載せていますが、基本はGridコンポーネントのセルスウィズルを「XZY」にすることと、Tilemapコンポーネントの向きを「XZ」にすることが大事です。

あとは任意ですがカメラの向きも変えておくと確認しやすいかもしれません。

これでオブジェクト側の用意は終わりました。

SelectPlaneの表示/非表示

それではスクリプトを書いていきます。まずは手始めにSelectPlaneの表示と非表示から。
OnGridPointerスクリプトを作成して下さい。そこに以下のように記述します。

using UnityEngine;

public class OnGridPointer : MonoBehaviour
{
    [SerializeField]
    private GameObject selectPlane;

    private void OnMouseEnter()
    {
        selectPlane.SetActive(true);
    }

    private void OnMouseExit()
    {
        selectPlane.SetActive(false);
    }
}

ビルドしたらPlaneにアタッチし、Select PlaneにSelectPlaneオブジェクトを設定しておきます。
実行してみます。以下のようにマウスカーソルが上に乗ったらSelectPlaneオブジェクトが表示され、外れたら非表示になればOKです。

SelectPlaneの表示・非表示

マウスカーソルのあるPlane上の位置の取得

マウスカーソルのあるPlane上の位置はRaycastを飛ばすことで取得します。
OnGridPointerクラスを以下のように変更します。

// メソッドを追加
private void OnEnter()
{
    if (Camera.main == null) return;
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit))
    {
        selectPlane.transform.position = hit.point + new Vector3(0, 0.01f, 0);
    }
}

private void OnMouseOver()
{
    OnEnter();
}

// メソッドを変更
private void OnMouseEnter()
{
    selectPlane.SetActive(true);
    OnEnter();
}

試しにSelectPlaneオブジェクトの位置を変更することで実際に取得できているか確認できるようにしています。
また、そのままだとPlaneオブジェクトと重なってしまってSelectPlaneオブジェクトが埋もれてしまう為、若干上に座標をずらしています。Y座標に0.01足しているのはその為です。
実行してみます。以下のようにマウスカーソル位置にSelectPlaneオブジェクトが移動すると思います。

マウスカーソルのPlane上の位置

グリッド座標を取得し、その位置に移動

最後にグリッド座標を取得し、SelectPlaneオブジェクトをその位置に移動させましょう。
OnGridPointerクラスを以下のように変更します。

// スクリプトの最初に以下を追記
using UnityEngine.Tilemaps;

// パラメーターを追加
[SerializeField]
private Tilemap tilemap;

// メソッドを変更
private void OnEnter()
{
    if (Camera.main == null) return;
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    RaycastHit hit;
    if (Physics.Raycast(ray, out hit))
    {
        Vector3Int gridPos = tilemap.WorldToCell(hit.point);
        Vector3 complementPos = new Vector3(tilemap.cellSize.x / 2, 0.01f, tilemap.cellSize.y / 2);
        Vector3 worldPos = tilemap.CellToWorld(gridPos) + complementPos;
        selectPlane.transform.position = worldPos;
    }
}

ビルドしたら、コンポーネントのTIlemapにはTilemapオブジェクトを設定して下さい。

一度グリッド座標に直してから再度ワールド座標に直すことで、位置を正規化しています。
ただしこれで取得できるのはあくまでグリッドの左上の座標の為、1グリッドのサイズ(=tilemap.cellSize)/2を足して中央にずらす必要があります。
実行してみます。以下のようにグリッド座標単位でSelectPlaneオブジェクトが移動できればOKです。

グリッド座標移動

終わりに

Unity3D上でマウスカーソルのある位置のグリッド座標を取得できました。これを応用すれば最初の画像のようにキャラクターをその位置に移動させたりといったことが可能になると思います。RPGやローグライクゲームなどにいかがでしょうか。

最後に今回の参加について。もともとネタがなかったのでアドベントカレンダーには参加しないつもりだったんですが、もしかしたらこのネタはいいかもしれないと思い結局参加してしまいました。
検索しても出てこなかったので多分二番煎じではないと......ないといいですね。
(もしネタ被りしていたらすみません......!)

ここまでお付き合い下さり、ありがとうございました。

おまけ:最小のコード

もしSelectPlaneを使わず、グリッド座標のみを取得したい場合、コードは以下のようになると思います。

using UnityEngine;
using UnityEngine.Tilemaps;

public class OnGridPointer : MonoBehaviour
{
    [SerializeField]
    private Tilemap tilemap;

    public Vector3Int GetGridPointer()
    {
        if (Camera.main == null) return new Vector3Int();
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit))
        {
            Vector3Int gridPos = tilemap.WorldToCell(hit.point);
            return gridPos;
        }
        return new Vector3Int();
    }
}

ご査収下さい。

ツイッターでシェア
みんなに共有、忘れないようにメモ

みにに

noteにて講座っぽいもの投稿中(現在は更新停止)。動けばいいや感覚でゆるく実装してます。

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント