【VRChat】お砂糖監視アプリ作ってみた
はじめに
VRChatというゲーム上でお付き合いしているパートナー(お砂糖)の行動履歴を監視するWebアプリ 「Sugar Surveillance」 を開発しました。
本記事は、出来上がったプロダクトについてその開発経緯や内部実装についてまとめたものになります。
こんなアプリを作りました
プロダクト名: Sugar Surveillance
背景知識
一応VRChatを知らない人にもなんとなく記事の概要がわかるように背景知識を説明します。
VRChat
VRChatは、ユーザーが仮想空間(VR)内で他のユーザーと自由に交流できるメタバースプラットフォームです。
ユーザーはVRChatで、会話を楽しみながら特定の目的を持たずに自由に時間を過ごします(筆者はVRChatで会話しながらお酒を飲むことが多いです)。
VRChatはワールドと呼ばれる特定のコンセプトを持った仮想空間を訪れ、それぞれの場所ならではの体験を楽しむことができます(例えばゲームができるワールドや、大画面でYouTubeを鑑賞できるワールド等)。加えて、一般的なソーシャルゲームと同様にフレンド機能が実装されており、フレンド登録したユーザー同士は、お互いの現在地(ワールド)を確認でき、気軽に合流して一緒に時間を過ごすことができます。その一方で人に知られたくない場合は、自分の所在を隠すこともできます。
VRChatの雰囲気を把握してもらうために何枚か写真を添付しておきます。
VRChatの写真
串カツ神経衰弱をしている様子
バーでキャストとお喋りしている様子
皆でYouTubeを見ている様子
VRChatAPI
VRChatAPIはVRChatのWebサイトから得られるような情報(フレンドの所在や自分のVRChatの情報)をAPIを通じて取得できるコミュニティ主導のプロジェクト(公式ではない)です。例えば以下のリクエストを送ると、選択したユーザーの状態をレスポンスとして取得することができます。
このAPIを利用することで様々なアプリケーションにVRChatの情報を組み込むことができます。
そしてAPIへ直接リクエストを送る方法だけではなく、PythonやC#のライブラリとして、簡単に利用する方法もあります。
あくまで取得できる情報はホームページに載っているような情報だけで、位置情報を隠しているユーザーからそのユーザーの所在を取得することはできません。
curl -X GET "https://api.vrchat.cloud/api/1/users/{フレンドのuserId}" \
-b "auth={authCookie}"
{
"bio": "人が多い場所では基本無言です",
"current_avatar_image_url": "https://api.vrchat.cloud/api/1/file/file_xxxxxxxx/2/file",
"display_name": "hiyori", <= ユーザー名
"id": "usr_xxxxxxxx",
"is_friend": true,
"location": "wrld_ba913a96-fac4-4048-a062-9aa5db092812", <= 現在の所在
"note": "ポピ横で出会った",
"state": "online",
"status": "active",
"status_description": "アバター改変中",
"worldId": "wrld_ba913a96-fac4-4048-a062-9aa5db092812"
}
お砂糖
定義は様々ですが、本記事ではVRChat上で付き合っているパートナーやパートナー関係のことを指します。
・最近、お砂糖ができました。
・彼とはお砂糖関係です。
・あのお砂糖最近プラべに籠りっぱなしですね。
・最近のお砂糖市場は活況です。
開発動機
お砂糖の中には、パートナーのVRChat内での行動履歴を監視するようなメンヘラ色が強い方もいるらしく、その件を耳にした際に、せっかくなので技術的な勉強を兼ねて、パートナーのVRChat内の行動履歴を取得して監視するアプリを作成しようと思いました(既に似たようなアプリあるじゃんという言葉には耳を塞ぎます)。
もちろんパートナーには許可を取っています(快く許諾してもらいました)(別に許可を取らなくてもフレンドであれば行動履歴は取得できます)。
あくまでも技術的なモチベーションが大部分を占めており、私自身がメンヘラなわけではないため、今回開発にあたって定めたメンヘラのペルソナを、パートナーは自分以外の人間といる際には常に居場所を分かる状態でなければならないと考える人物であると定めました(私に知られたくないってことは何か後ろめたいことをやってるんでしょ!!!!)。
開発概要
何を作成したいのか
定めたメンヘラのペルソナに従い、今回作成するプロダクトは、「何時何分から何時何分まで、私が把握できない場所にいるんだけど、これは何?」と問い詰められるような情報を遅滞なく取得できる機能を備えたものであることとしました。
従って、具体的に以下の機能を実装することとしました。
- 継続的に最新の情報を取得できる
- 期間を絞って情報を取得できる
- 自分の把握できていない時間が全体に占める割合が分かる
- 自分の把握できていない時間帯が簡単に分かる
また、制約として、パートナーのVRChatアカウントにログインしてログを取得するのではなく、自分のアカウントのフレンドとしてログを取得することとしました。パートナーのログインパスワードは共有してもらえなかったと仮定します。
具体的な実装内容
今回はベストプラクティスな実装というよりも、触ってみたい技術を詰め込む実装としました。
画像に示すようなアーキテクチャをdocker composeを用いて実装し、Front-endとBack-endで完全に分離したコンテナ設計としました。
プロダクトのアーキテクチャ全容
Front-end
- 自分がパートナーの行動ログを確認する画面を表示する役割を担っています。
- Next.jsを用いて実装しました。
- 業務でNext.jsを使う機会が増え始めたので、キャッチアップを目的とした採用です
- 最近はv0を用いた爆速Front-end開発がありますが、「AIに頼りすぎるのもなー」と思ったので、デザインの考案までv0に任せ、実際のコンポーネントの組み立てはshadcn/uiとTailwind CSSを用い自分で行いました
- 私はデザインについて門外漢なので、デザインについては生成AIに頼り切る想定であり、その手段としてv0を用いました(無料で画面設計を生成AIに依頼するならv0が一番だと思っている)
Back-end 1
- データベースに保存したパートナーの行動ログを加工しFront-endに送る、データベースに行動ログを流し込む(テストデータ等)役割を担っています。
- Fast APIを用いて実装しました。
- Front-endとBack-endが完全に分離したアーキテクチャを組みたい点、パッケージ管理にuvを利用したいという点から、PythonのWebフレームワークとして採用しました。
- uvは凄くパッケージインストールが速かったです。最高。
- Docker環境外へのポートの解放はしておらず、内部ネットワークからのみアクセスできるよう設計しました
- セキュアな構成を意識したつもり
- Front-endとBack-endが完全に分離したアーキテクチャを組みたい点、パッケージ管理にuvを利用したいという点から、PythonのWebフレームワークとして採用しました。
Back-end 2
- VRChat APIのPythonライブラリを用い、パートナーの行動ログを取得しデータベースに保存する役割を担っています。
- Back-end 1と同様にuvを用いたPythonによる実装となっています。
- VRChat APIはJavaScript, C#等ほかの言語でもライブラリとして利用できる。
- VRChatにアクセスする機能を他のコンテナから分離したい点、ログ収集機能は素早く実装して稼働させるため開発進捗を分離させたい点からBack-end 1と分離したコンテナとしました。
Database
- Back-end 2を通じて取得した行動ログを格納する役割を担っています。
- 今回はパートナーの行動監視が目的なので、単一のテーブルに以下のような項目でデータを格納しています
カラム名 | 説明 | 具体例 |
---|---|---|
id |
ユーザーログの一意の識別子 | 12345 |
datetime |
ログが記録された日時 | 2025-01-04T12:34:56 |
display_name |
ユーザーの表示名 | Hiyori |
state |
ユーザーの状態 | offline |
status |
ユーザーのVRChat内のステータス | join me |
avatar_name |
ユーザーが使用しているアバターの名前 | Sophina-Denim |
avatar_image_url |
アバター画像のURL | https://example.com/avatar.png |
world_name |
ユーザーが現在いるワールドの名前 | SuRroom |
world_image_url |
ワールド画像のURL | https://api.vrchat.cloud/api/1/file/file_0043cc43-931c-4c4e-87a0-2e0e4622d320/3/file` |
VRChat APIからのデータ取得
データ取得の流れ
データベースに登録する行動ログを取得するためには、複数のAPIリクエストを利用する必要があります。
具体的なデータ取得の流れを以下に示します。
userId
を取得する
1. まずはパートナーの情報をAPIから取得するために、そのユーザーのuserIdを取得します。
userIdは、普通にVRChatを楽しんでいる分には目にすることはないですが、全ユーザーに割り振られている一意なidです。
この情報はホームページの開発者タブから取得できます(少し強引な手法なので、他にもっとスマートに取得できる方法があれば教えて下さい)。
2. ユーザー情報を取得する
取得したuserId
を用いて基本的なユーザー情報を取得します。
利用するAPIメソッドは[GET] /users/{userId}
です。
こちらのレスポンスより以下のプロパティを取得します。
プロパティ名 | 説明 |
---|---|
profile_pic_override |
アバターの画像URL |
location |
現在のユーザーのいるワールドのworldId ユーザーが所在を隠している場合には private となる |
state |
今オンラインかオフラインかを示す |
status |
VRChat内のステータス |
status_description |
ステータスの説明 |
display_name |
現在のユーザー名 |
3. ワールド情報を取得する
取得したworldId
から、ワールドの詳細情報を取得します。
利用するAPIメソッドは[GET] /worlds/{worldId}
です。
こちらのレスポンスより以下のプロパティを取得します。
プロパティ名 | 説明 |
---|---|
name |
ワールド名 |
image_url |
ワールド画像 |
4. アバター名を取得する
アバター名を直接取得するAPIリクエストは見つかりませんでした(アバターの画像は取得できるのにね!!)。
しかし、アバターを表示している画像URLに含まれるfieldId
を用いれば強引にアバター名の取得は可能です。
今回はそのfieldId
を用い、[GET] /file/{fileId}
を通じてアバター名を取得しました。
レスポンスは以下のように得られ、name
の文字列に埋め込まれたアバター名を抽出することで取得することができます。
(自分のアバター情報であれば簡単に取得できるのですが、他人のアバター情報の取得はひと手間増えるようです。もっと良い方法があれば教えてください)
また、無理やりアバター名を取得しているためか、VRC+に所属しているユーザーのアバター名は取得できないケースがありました。
{
"name": "Avatar - Test Avatar - Unity package - 2017․4․28f1_3_standalonewindows_Release"
}
取得したデータの加工
データベースに格納したデータをBack-end 1のAPIで加工してFront-endに送信します。
Front-endではユーザーの見やすさを優先するため、紛らわしいと感じたstate
とstatus
を1つのstatus
として統合しました(下図参照)(我々がVRChatでよく見るstatusであるonline
は、APIレスポンスではなぜかactive
と表示されていました)。
データベースにはできるだけ生データを格納し、APIでその時々に応じてユーザーに分かりやすい表現となるように実装しています。
行動履歴の表示
Front-endでのログの表示について説明します。
今回の開発における画面表示で最も重要視しなければならないのは、どれだけの時間、パートナーが「私の把握できていない状態」でVRChatで過ごしているのかがすぐに分かるかどうかでした。
従って、Back-end 1で加工したstatusについて、下表のように大分類を設け、Cannot trace
は表示ログの背景色を警告色としました。
大分類 | 状態の説明 | 判定条件 |
---|---|---|
Cannot trace | VRChat内にいるが所在が不明 |
World name = "private" |
Offline | VRChatにいない状態 |
status = "offline" もしくは status = "web active" |
Others | VRChat内にいて所在が把握可能 | 上記以外の全ての状態 |
プロダクト概要
できあがったプロダクトはこの記事の最初に示したものです。ここではそれぞれの要素について詳細に示します。左のサイドバーにstatistic
の項目がありますが、こちらは実装が間に合わず作成できていません(気が向いたら作成します、見栄え的にサイドバーがあったほうがきれいだったので残しました)。
ログ
アバター、ワールド、ステータスで一意な状態を一つのログにまとめて表示しています。
例えば、以下に示した所在が分かっているログについては、2025/1/3 21:14
から2025/1/3 21:21
までの間、rurune ストリート
というアバターでonline
のステータスで紺瑠璃の間 -Azure Blue Rooms-
に居たということを示しています。
先ほど言及したように、大分類Cannot trace
のログ表示は赤色で強調して示しています。
所在が把握できている場合のログ(Others)
所在が把握できていない場合のログ(Cannot trace)
ログ表示設定
ログの表示の形式として、以下3機能を画面右側に実装しました
- 特定の期間を指定してログを表示する機能
- 最新のログ自動更新してを表示し続ける機能
- 日付について降順、昇順を切り替える機能
ログサマリー
表示している期間について、ステータスの大分類毎に合計時間とその全体に占める割合を計算し表示しています。
改善点
実装、使用していた際に感じたプロダクトの改善点は以下です。
-
Cannot trace
の分離- 現状の実装では自分がパートナーとprivateなワールドにいる時間も
Cannot trace
に含まれてしまいます。背信と愛を育む時間が同じ大分類に含まれているので、お砂糖監視アプリとしての設計は少し破綻しています。 - 割と致命的な設計ミスなので修正しようとしたのですが、APIからでは自分の居るワールドだとしても所在を隠しているユーザーの情報も取得できませんでした(インスタンス作成者であれば全員の情報が取得可能)。
- 実装するには、パートナーのログインIDやパスワード情報が必須だと思います。
- 現状の実装では自分がパートナーとprivateなワールドにいる時間も
展望
やるかわかりませんが、時間が許せばやりたいことを以下に挙げます。
- 統計ページを作成する
- とりあえずサイドバーだけ作成しましたが、遷移先を作成していないので。
- それぞれのワールドの訪問頻度、曜日別のログイン時間帯表示等を考えています。
- ToNばかりやっていることが定量的に表れるでしょう...
- 通知機能
- n分間連続で所在を把握できなくなったら通知が来る、ログインしたら通知が来るとかできると嬉しいですね。
- デプロイ
- お金をかけずにコンテナデプロイができる良さげなサービスがあれば教えてください
- 複数ユーザーへの対応
- 今回はパートナーが対象だったので、行動を取得対象の
userId
は環境変数として埋め込みました。仮に複数ユーザーに適用できるようにするとなるとデータベース設計も含めて再度調整が必要です。
- 今回はパートナーが対象だったので、行動を取得対象の
- そもそもフレンドではなく、自分のログ管理として使いたい
使用感
実際に使ってみたのですが、かなり見やすく行動を監視できます。
今日一日VRChatで何をしていたのか、一目で分かります。楽しい。
おそらく今後は専用のPCにずっと画面を写していることでしょう...
まとめ
本記事では、お砂糖の行動を監視するWebアプリケーション「Sugar Surveillance」の個人開発についてその成果をまとめました。
結果として行動ログを見やすく表示することは実現できたのですが、自分とパートナーがprivateなワールドにいる場合であっても、所在が把握できない時間としてカウントされてしまうため、メンヘラ的にパートナーの行動を監視するツールとしては微妙なプロダクトとなってしまいました。
しかし、現状多くの拡張性を備えたアプリとなっているため、自己学習の側面として統計処理やデプロイなど多くのスキル取得に利用できればと思っています。
おわりに
作成しようと思い立ったのが2024/11/1くらいで、ログの取得だけ実装して画面表示を実装せずに放置していました。お正月の期間にまとまった時間がとれたので思い切って実装しきり、記事としてまとめました。
個人開発楽しいですね。
最後に、こんなふざけたプロダクト開発のために行動履歴の提供を快く了解してくれたパートナーと、VRChat内で実験や写真撮影につきあってくれたフレンドの皆さんありがとうございました。
Discussion
1. userIdを取得するについてです。
各ユーザーページのURLが
https://vrchat.com/home/user/{userId}
となっていて、それぞれhttps://vrchat.com/api/1/users/{userId}
のAPIを叩いてるように見えるので、開発者タブを使わずにページURLから取得するのがスマート(?)ではないでしょうか?(例外のパターンがあったらすみません)ご指摘ありがとうございます。
本当ですね。全然気づきませんでした...
追記させていただきます