Blog, VRChat

VRChatで住んでいる地域を取得して表示してみる

VRChatワールドを制作している中で、「アクセスしているユーザーが日本のどの地域から来ているのかを表示できないか」と考えることがありました。 特にVRChatの特殊なSDKにAnalyticsというものがあります。 これはVRChatに申請してWorldに訪問したPlayerがどこにインタラクトしたか何時にjoinしたかなどを記録できます。 今回の実装では推定地域をAnalyticsで取得したいと思い、検証しております。 例えば、地域別に表示を切り替えたり、イベントワールドで「推定アクセス地域」を表示したりする用途です。 そこで今回は、IPアドレスを基に地域情報をJSONで返すAPIを利用し、UdonSharpから取得した都道府県・都市名相当の情報をTextMeshProに表示する方法を検証しました。 最初に結論を書いておくと、技術的には取得できます。ただし、IPアドレスから得られる地域情報は正確な現在地ではありません。また、VRChatの外部URL制限により、公開ワールドで全ユーザーに確実に動作する仕組みとして採用するには問題があります。

実装したいこと

今回実装する内容は次の通りです。 ・ワールドに参加したユーザーの端末から位置情報APIへアクセスする ・APIからJSON形式で地域情報を受け取る ・country_code が JP の場合のみ、日本国内の推定地域として表示する ・region と city をTextMeshProへ表示する 表示例は次のような形です。

推定アクセス地域 Tokyo / Tokyo

APIの結果によっては、次のように表示される可能性もあります。

推定アクセス地域 Niigata / Niigata

ただし、ここで表示される都市名は「ユーザーが実際にいる市町村」と一致するとは限りません。例えば、長岡市から接続していても、新潟市や別の都市名として判定される可能性があります。

使用するAPI

今回は ipapi.co を使用しました。 APIへ次のURLでアクセスすると、アクセス元の公開IPアドレスに基づいた地域情報がJSONで返されます。

https://ipapi.co/json/

返却されるJSONは、おおよそ次のような形式です。

{
  "ip": "xxx.xxx.xxx.xxx",
  "city": "Tokyo",
  "region": "Tokyo",
  "region_code": "13",
  "country_code": "JP",
  "country_name": "Japan",
  "postal": "100-0000",
  "latitude": 35.6895,
  "longitude": 139.6917,
  "timezone": "Asia/Tokyo"
}

今回使用するのは、主に次の3項目です。

country_code: 国コード。日本の場合は JP region: 都道府県相当の推定地域 city: 都市名・市区町村相当の推定地域

緯度経度や郵便番号も返却されますが、今回は必要ありません。必要以上に細かい位置情報を表示する理由もないため、地域名のみを利用します。

VRChatでJSONを取得する方法

VRChatのUdonでは、外部のテキストデータを取得するために VRCStringDownloader.LoadUrl() を使用できます。 JSONも文字列データとして取得できるため、APIの返却結果を受け取り、その後 VRCJson.TryDeserializeFromJson() で解析します。 処理の流れは次の通りです。

VRCStringDownloaderでAPIへアクセス ↓ OnStringLoadSuccessでJSON文字列を取得 ↓ VRCJsonでDataDictionaryへ変換 ↓ country_code / region / city を取得 ↓ TextMeshProへ表示

今回は外部APIから受け取るJSONを扱うため、値を直接決め打ちで読み取らず、TryGetValue() で型を確認しながら取得します。 外部データは常に想定通りの形式で返るとは限らないためです。

UdonSharpスクリプト

以下が今回の実装です。

using UdonSharp;
using UnityEngine;
using TMPro;
using VRC.SDK3.Data;
using VRC.SDK3.StringLoading;
using VRC.SDKBase;
using VRC.Udon.Common.Interfaces;

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class IpLocationDisplay : UdonSharpBehaviour
{
    [Header("API URL")]
    [SerializeField] private VRCUrl locationApiUrl;

    [Header("UI")]
    [SerializeField] private TMP_Text locationText;
    [SerializeField] private TMP_Text statusText;

    [Header("Settings")]
    [SerializeField] private bool loadOnStart = true;

    private bool _isLoading;

    private void Start()
    {
        if (loadOnStart)
        {
            LoadLocation();
        }
    }

    public void LoadLocation()
    {
        if (_isLoading) return;

        if (VRCUrl.IsNullOrEmpty(locationApiUrl))
        {
            SetStatus("API URLが設定されていません。");
            return;
        }

        _isLoading = true;

        if (locationText != null)
        {
            locationText.text = "-";
        }

        SetStatus("推定地域を取得しています...");

        VRCStringDownloader.LoadUrl(locationApiUrl, (IUdonEventReceiver)this);
    }

    public override void OnStringLoadSuccess(IVRCStringDownload result)
    {
        _isLoading = false;

        DataToken rootToken;

        if (!VRCJson.TryDeserializeFromJson(result.Result, out rootToken))
        {
            SetStatus("JSONの解析に失敗しました。");
            return;
        }

        if (rootToken.TokenType != TokenType.DataDictionary)
        {
            SetStatus("APIの返却形式が想定と異なります。");
            return;
        }

        DataDictionary locationData = rootToken.DataDictionary;

        DataToken countryToken;
        if (!locationData.TryGetValue("country_code", TokenType.String, out countryToken))
        {
            SetStatus("国情報を取得できませんでした。");
            return;
        }

        string countryCode = countryToken.String;

        if (countryCode != "JP")
        {
            if (locationText != null)
            {
                locationText.text = "日本国外";
            }

            SetStatus("日本国外からのアクセスとして判定されました。");
            return;
        }

        DataToken regionToken;
        DataToken cityToken;

        bool hasRegion = locationData.TryGetValue("region", TokenType.String, out regionToken);
        bool hasCity = locationData.TryGetValue("city", TokenType.String, out cityToken);

        string region = hasRegion ? regionToken.String : "不明";
        string city = hasCity ? cityToken.String : "不明";

        if (locationText != null)
        {
            locationText.text = region + " / " + city;
        }

        SetStatus("IPアドレスに基づく推定地域です。");
    }

    public override void OnStringLoadError(IVRCStringDownload result)
    {
        _isLoading = false;

        if (locationText != null)
        {
            locationText.text = "取得失敗";
        }

        SetStatus("地域情報の取得に失敗しました。");
        Debug.LogError("[IpLocationDisplay] Load Error: " + result.Error);
    }

    private void SetStatus(string message)
    {
        if (statusText != null)
        {
            statusText.text = message;
        }
    }
}

スクリプトの処理内容

VRCUrl にAPIのURLを設定する

[SerializeField] private VRCUrl locationApiUrl;

Udonで外部URLを利用する場合、URLは VRCUrl として扱います。 今回設定するURLは次のものです。

https://ipapi.co/json/

URLをコード上で動的に生成するのではなく、UnityのInspectorから設定します。

ワールド読み込み時にAPIへアクセスする

private void Start()
{
    if (loadOnStart)
    {
        LoadLocation();
    }
}

loadOnStart を有効にすると、ワールド参加後に自動で地域情報を取得します。 ただし、APIへ何度もアクセスする必要はないため、基本的には参加時に1回取得する設計で十分です。

JSONをDataDictionaryへ変換する

if (!VRCJson.TryDeserializeFromJson(result.Result, out rootToken))
{
    SetStatus("JSONの解析に失敗しました。");
    return;
}

APIから返された文字列をJSONとして解析します。 解析に失敗した場合は、そのまま値を使用せずに処理を中断します。外部APIを利用する場合、通信失敗や返却形式の変更が起こる可能性があるため、エラー処理は省略しない方が安全です。

日本国内かどうかを判定する

if (countryCode != "JP")
{
    locationText.text = "日本国外";
    return;
}

今回の用途では日本国内の地域表示のみを行うため、country_code が JP ではない場合は都市名を表示しないようにしています。

都道府県・都市名相当を表示する

string region = hasRegion ? regionToken.String : "不明";
string city = hasCity ? cityToken.String : "不明";

locationText.text = region + " / " + city;

region と city を取得して、TextMeshProへ表示します。 値が返らなかった場合は 不明 と表示することで、空欄のままにならないようにしています。

Unity側の設定方法

1. TextMeshProを配置する Canvasまたはワールド空間上に、次のTextMeshProを配置します。 地域名表示用テキスト 状態表示用テキスト 例として、次のような表示にします。

推定アクセス地域 - 取得前

2. 空のGameObjectを作成する Hierarchy上で空のGameObjectを作成し、名前を次のようにします。

IpLocationManager

3. スクリプトをアタッチする 作成した IpLocationDisplay.cs を IpLocationManager にアタッチします。 4. Inspectorを設定する Inspector上で次の項目を設定します。

Location Api Url: https://ipapi.co/json/ Location Text: 地域名表示用のTextMeshPro Status Text: 状態表示用のTextMeshPro Load On Start: 自動取得する場合は有効

これで、ワールド開始時にAPIへアクセスし、取得できた場合は推定地域が表示される構成になります。

この表示はローカル処理になる

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]

そのため、各ユーザーの端末で個別にAPIへアクセスし、それぞれの画面上で自分自身の推定地域が表示されます。 例えば、東京から接続しているユーザーには次のように表示されます。

Tokyo / Tokyo

新潟から接続しているユーザーには、APIの推定結果に応じて次のような表示になります。

Niigata / Niigata

同じTextMeshProを使用していても、同期していない限り、他のユーザーへその内容が共有されるわけではありません。 地域情報を全体へ同期して表示する設計にすると、ユーザーの接続地域を他者へ公開することになるため、用途を慎重に検討する必要があります。

実装上の大きな問題:VRChatの外部URL制限

ここまで実装方法を書きましたが、実際にVRChatワールドで利用する場合には重要な制約があります。 VRChatでは、Udonから自由にすべての外部URLへアクセスできるわけではありません。String Loadingで標準的に利用できるドメインは制限されています。 ipapi.co は、VRChatの標準許可ドメインには含まれていません。 そのため、ユーザーがVRChatの設定で Allow Untrusted URLs を有効にしていない場合、APIへのアクセスは失敗します。 つまり、次のような用途では問題があります。 全参加者に必ず表示したい案内 地域によって必ず分岐させたいギミック 入場制限 イベント運用に必要な管理機能 ユーザー設定によって動作したりしなかったりするため、公開ワールドの必須機能として組み込むのは適切ではありません。 一方で、次のような用途であれば検証対象としては成立します。 技術検証 Allow Untrusted URLsを有効にした限定環境 個人用・開発者向けワールド 地域表示の試作

IPアドレスから市町村を正確に取得できるわけではない

今回取得できる city は、市区町村相当の情報です。 しかし、これはGPSや端末の位置情報から取得した現在地ではありません。あくまで、公開IPアドレスに関連付けられた推定地域です。 そのため、以下のようなことが起こり得ます。 実際には長岡市にいるが、新潟市として表示される 東京都内にいるが、区ではなく Tokyo とだけ表示される モバイル回線で接続しているため、離れた都市として判定される VPNを使用しているため、実際とは異なる地域として判定される 企業回線や共有ネットワークにより、拠点所在地として判定される したがって、この値を「ユーザーの現在地」や「正確な住所情報」として扱うべきではありません。 表示する場合も、次のように「推定」であることを明示する必要があります。

IPアドレスに基づく推定地域です。

利用時のプライバシー上の注意

IPアドレスから地域情報を取得する処理は、ユーザーにとってプライバシーに関わる処理です。 今回の実装では、ワールド側でIPアドレスを表示したり保存したりはしていません。しかし、APIへアクセスする時点で、API提供者はアクセス元の公開IPアドレスを受け取ることになります。 そのため、実際にワールドへ導入する場合は、少なくとも次の点を明確にしておくべきです。 IPアドレスに基づく推定地域を取得すること 正確な現在地ではないこと 地域情報を表示または集計する場合は、その用途 必要以上に細かい情報を表示しないこと 他ユーザーへ同期して公開しない設計を優先すること 便利そうだからという理由だけで、ユーザーに説明せず導入する機能ではないと考えています。

今回実装して分かったこと

今回の検証で、UdonSharpからJSON APIへアクセスし、返ってきたデータを表示する処理自体は比較的単純に実装できることが分かりました。 特に、VRCStringDownloader と VRCJson を組み合わせることで、外部JSONを基にした表示ギミックは実装できます。ニュース表示やタイムテーブル表示など、事前に用意したJSONを読み込む用途であれば非常に使いやすい仕組みです。 一方で、IP位置情報APIのように外部サービスへ直接アクセスする構成では、VRChatの許可URL制限が大きな問題になります。 また、取得できる地域情報も正確な市町村ではなく、あくまで推定値です。技術的に取得できることと、ワールドの機能として採用してよいことは別に考える必要があります。

まとめ

今回は、IPアドレスから推定地域をJSONで返すAPIを利用し、VRChatのUdonSharpで地域名を表示する実装を試しました。 実装の流れは次の通りです。

ipapi.coへアクセス ↓ JSONで地域情報を取得 ↓ VRCJsonで解析 ↓ region / city をTextMeshProへ表示

都道府県相当の情報は region、都市名・市区町村相当の情報は city から取得できます。 ただし、IPアドレスから取得した都市名は正確な現在地ではありません。さらに、ipapi.co はVRChatの標準許可ドメイン外であるため、ユーザーがAllow Untrusted URLsを有効にしていない場合は動作しません。 そのため、この実装は技術検証や限定用途には利用できますが、公開ワールドで全ユーザーを対象にした必須機能や、アクセス制御の判定材料として使用するのは難しいという結論になりました。 ::: コードは公式仕様に基づいて構成していますが、こちらではあなたのUnity / VRChat SDK環境上でのコンパイルおよび実機動作確認までは行っていません。公開前には、UdonSharpのコンパイル、Build & Test、Allow Untrusted URLs の有効・無効双方での失敗挙動を確認してください。記事内の根拠となる仕様は、VRChat公式の String Loading・VRCJSON・External URLs と、ipapi.co のAPI仕様です。

https://rin0700.com/blog/ee4f195d-0d95-4c9c-94da-213393587882