Unityで位置ゲーを作るために必要な技術 その1 現在地取得と逆ジオコーディング


はじめに

UnityでポケモンGOのような位置ゲーを作るために必要な要素をまとめて、
なにができるのか、実装しながら確認していきます。

ここでは開発はUnityで行い、Android/iOSで動くものを想定します。
※ただしiOSの開発環境がないので、Androidのみで検証していきます

とりあえず必要だと思うもの

現在地取得

端末の現在位置を取得する機能です。
端末のGPSから情報を拾えれば可能なのではないかと思います。

以下にまとめがあるのでこれは問題なさそう。
[Unity] Android/iOSで位置情報を取得する

地図表示

地図がない位置ゲー、というのも考えられますが、とりあえず普通は地図を何らかの形で使うでしょう。
地図は1から作るわけにはいかないので、何らかのAPI的なサービスを使用する必要があるでしょう。

逆ジオコーディング

緯度経度から住所を検索する機能。
必須ではありませんが、国取りゲーのように住所を絡めるなら必要です。
住所から緯度経度であれば、住所と緯度経度を紐づけたDBがあれば取得できますが、
逆はそうはいきません。

たとえば、ある緯度経度が「何県何市にあたるのか」を検索するためには、
市の形状に点が含まれるか、というGIS的演算が必要になります。
つまり、「ある面にある点が含まれているか」という計算です。

GIS的には「ジオメトリ演算」と呼ばれるものです。

現在地を取得し、逆ジオコーディングで市区町村名を表示する

手始めに、現在地の取得は前述の記事にある通りだと思うので、逆ジオコーディングで市区町村名を
表示するところまでやってみたいと思います。

現在地取得

前述のリンクの実装方法ほぼそのままです。

LonLatGetter.cs
using System.Collections;
using UnityEngine;

/// <summary>経緯度取得クラス</summary>
public class LonLatGetter : MonoBehaviour
{
    /// <summary>経緯度取得間隔(秒)</summary>
    private const float IntervalSeconds = 1.0f;

    /// <summary>ロケーションサービスのステータス</summary>
    private LocationServiceStatus locationServiceStatus;

    /// <summary>経度</summary>
    public float Longitude { get; private set; }

    /// <summary>経度</summary>
    public float Latitude { get; private set; }

    /// <summary>緯度経度情報が取得可能か</summary>
    /// <returns>可能ならtrue、不可能ならfalse</returns>
    public bool CanGetLonLat()
    {
        return Input.location.isEnabledByUser;
    }

    /// <summary>経緯度取得処理</summary>
    /// <returns>一定期間毎に非同期実行するための戻り値</returns>
    private IEnumerator Start()
    {
        while (true)
        {
            locationServiceStatus = Input.location.status;
            if (Input.location.isEnabledByUser)
            {
                switch (locationServiceStatus)
                {
                    case LocationServiceStatus.Stopped:
                        Input.location.Start();
                        break;
                    case LocationServiceStatus.Running:
                        Longitude = Input.location.lastData.longitude;
                        Latitude = Input.location.lastData.latitude;
                        break;
                    default:
                        break;
                }
            }

            yield return new WaitForSeconds(IntervalSeconds);
        }
    }
}

逆ジオコーディングで市区町村名を取得

今回使用するAPIは以下です。
https://www.finds.jp/rgeocode/index.html.ja
パラメータに経緯度を渡し、住所コードや文字列を取得することができます。
上記を使用するクラスを実装しました。
大字以下も取得できるようですが、とりあえずテストなので市区町村までを取得するようにしています。

LonLatToAddr.cs
using System.Collections;
using System.Collections.Generic;
using MiniJSON;
using UnityEngine;

/// <summary>逆ジオコーディングクラス</summary>
public class LonLatToAddr : MonoBehaviour
{
    /// <summary>APIのパラメータテンプレートつきURL</summary>
    private const string ApiBaseUrl = "https://www.finds.jp/ws/rgeocode.php?json&lon={0}&lat={1}";

    /// <summary>住所文字列</summary>
    public string Address { get; private set; }

    /// <summary>経緯度から住所文字列を取得</summary>
    /// <param name="longitude">経度</param>
    /// <param name="latitude">緯度</param>
    /// <returns>遅延評価用戻り値</returns>
    public IEnumerator GetAddrFromLonLat(float longitude, float latitude)
    {
        // URLに経緯度パラメータを埋め込み
        string url = string.Format(ApiBaseUrl, longitude, latitude);

        // APIを実行して経緯度を保持
        using (WWW www = new WWW(url))
        {
            // API非同期実行用yield return
            yield return www;

            // 結果JSONのデシリアライズ
            var desirializedData = (Dictionary<string, object>)Json.Deserialize(www.text);

            // 成功した場合のみ処理
            if ((long)desirializedData["status"] == 200)
            {
                // 都道府県+市区町村を文字列として保持
                var result = (Dictionary<string, object>)desirializedData["result"];
                var prefecture = (Dictionary<string, object>)result["prefecture"];
                var municipality = (Dictionary<string, object>)result["municipality"];
                Address = (string)prefecture["pname"] + " " + (string)municipality["mname"];
            }
        }
    }
}

テキストUIに表示

最後に、取得した経緯度、住所文字列の情報を表示するUI周りの処理を実装します。

LonLatToUI.cs
using UnityEngine;
using UnityEngine.UI;

/// <summary>UI更新クラス</summary>
public class LonLatToUI : MonoBehaviour
{
    /// <summary>テキストテンプレート</summary>
    private const string LonLatInfoTemplate = "緯度: {0}\n経度: {1}\n住所: {2}";

    /// <summary>表示用テキストUIオブジェクト</summary>
    private Text lonLatInfo;

    /// <summary>経緯度取得オブジェクト</summary>
    private LonLatGetter lonLatGetter;

    /// <summary>逆ジオコーディングオブジェクト</summary>
    private LonLatToAddr lonLatToAddr;

    /// <summary>初期化</summary>
    private void Start()
    {
        // テキストラベルオブジェクトを保持
        lonLatInfo = GameObject.Find("LonLatInfo").GetComponent<Text>();

        // 経緯度取得オブジェクトオブジェクトを保持
        lonLatGetter = GetComponent<LonLatGetter>();

        // 逆ジオコーディングオブジェクトを取得
        lonLatToAddr = GetComponent<LonLatToAddr>();
    }

    /// <summary>経緯度の値をテキストUIに反映</summary>
    private void Update()
    {
        // 経緯度の値を取得できるか判定
        if (lonLatGetter.CanGetLonLat())
        {
            StartCoroutine(lonLatToAddr.GetAddrFromLonLat(lonLatGetter.Longitude, lonLatGetter.Latitude));
            lonLatInfo.text = string.Format(LonLatInfoTemplate, lonLatGetter.Latitude.ToString(), lonLatGetter.Longitude.ToString(), lonLatToAddr.Address);
        }
        else
        {
            lonLatInfo.text = string.Format(LonLatInfoTemplate, "測定不能", "測定不能", "測定不能");
        }
    }
}

ヒエラルキーとインスペクター

スクリプトをアタッチしているManagerオブジェクト


GetComponentメソッドで別スクリプトの処理を呼び出しているので、
3つのスクリプトは同じオブジェクトにアタッチする必要があります。
Test Mapというのは地図表示用に作っているものですのでとりあえず無視してください。

テキスト表示用オブジェクト


Findで名称検索してオブジェクトを検出しているため、「LonLatInfo」という名前にする必要があります。
もちろん、テキスト設定側のスクリプトでテキストコンポーネントを直接アタッチできる形にしてもかまいません。
(というか、そのほうがいいです。このくらいの規模感だと問題になりにくいですが、ヒエラルキーが肥大化した際はFind処理が重くなるので)

できたもの

Androidビルドをして、実機を確認します。
東京都文京区某所に実際に実機を持っていってアプリを起動しました。

まとめ

今回はAndroid端末でしか試していませんが、位置情報の取得のような実装を端末に異存せずに
シンプルに実装できるのはUnityの強みなのかなと感じました。
逆ジオコーディングもシンプルなAPIが多数提供されているので、こういった開発もやりやすくなっているなぁと。

次回

位置情報の取得は位置ゲーの基本ですが、これだけでは味気ないので
位置情報に基づいた地図の表示をやっていきたいと思います。