VerseEngine使ってみた/空間内の設備を貸し借りする

メタバースに興味があるわけではないですが、ちょっと触ってみた感じ面白そうだったので思いついたことをやってみました。

## VerseEngineとは

https://verseengine.cloud/ja/

Appland inc.によって開発/提供されているWebベースのメタバースエンジンです。メタバースエンジンはその名の通りメタバース実現に必要なベース機能（複数人間の状態同期など）を提供するものです（と捉えています）。
このエンジンはメタバース関連の機能のみの実装を含み、レンダリングなどはコア機能としては提供されていないためレンダリングや入力周りは自由に他のライブラリ/エンジンと統合する事が可能です。

:::note info

VerseEngineの公式からはthree.jsとA-Frame向けの統合ライブラリが提供されています。

:::

VerseEngineの最大の特徴としては**ユーザー間同期の仕組みにP2P通信（WebRTC）を使っている点**で、これにより独自のメタバースを展開する場合でも常時稼働のサーバが不要となり、静的ホスティングができるサービスを使うだけで構築が可能です。WebRTCだとシグナリングサーバはいるんじゃないのかと思うところですが、公式がメタバースの識別[^ident]とWebRTCのシグナリングを行ってくれるエントランスサーバを立てているので、基本的にはそこを使うことになります。

公式がわりと丁寧なGet startedを公開しているので、入門としてはこれを見るだけでほぼ所感を掴むことはできると思います（SSL化の方法とかも触れられていて結構丁寧）。
https://verseengine.cloud/ja/guide/getting-started/three.html

## 空間内設備の利用権コントロールを実現する技術

このVerseEngineを使って、空間内に設置した大型モニターに対して同時に一人だけWebCameraの映像を写せるような[^webcam]制御を組み込んだワールドをお試しで作ってみました。

https://github.com/Pctg-x8/verse-test

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">WebCamera経由でOBSの映像流し込んだり共用リソースの所有権コントロールができるようにしたりできた <a href="https://twitter.com/hashtag/VerseEngine?src=hash&amp;ref_src=twsrc%5Etfw">#VerseEngine</a> <a href="https://t.co/xkQVif2daP">pic.twitter.com/xkQVif2daP</a></p>&mdash; 🥴S.Percentage🐾 (@Pctg_x8) <a href="https://twitter.com/Pctg_x8/status/1673026552458805251?ref_src=twsrc%5Etfw">June 25, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

（本当はWeb会議システムの画面共有機能みたいなのが作りたかったけど、映像の同期は自前実装しないといけないのでまだできてない）

### 簡単な仕組み

今回は簡単に作ってみただけなので、あまり細かいところは考えず以下のようにシンプルな仕組みで大型モニターの所有権コントロールを実装しました。

1. モニターを使いたい人が「使います！」宣言をする
   * このときまだ誰も使っていなければそのまま利用者になる（所有状態をオンにする）
1. モニターを使っている人が上記宣言を検知した場合はダイアログを出して譲渡してもいいか確認する
1. 譲渡してもいいとなった場合、今の利用者は利用状態をオフにして「〇〇さんに譲渡します」宣言をする
1. 使いたい人が自分宛ての譲渡します宣言を確認した場合、自身の利用状態をオンにして映像再生を開始する

このフローを、中央サーバを用いないP2Pネットワークのみで実装します[^server]。

上記フローに出てくる「宣言」と「利用状態」をP2Pネットワークで相互にやり取りする必要があるわけですが、ここではVerseEngineの機能である「（ユーザー固有の）テキストデータ」を使ってやりとりします。

### ユーザ固有のテキストデータ

VerseEngineではユーザー（Player）ごとに固有のテキストデータというものをひとつ持つことができます。このデータは本来はユーザー名/表示名を扱うためのものだとは思いますが、入れる内容自体は自由ですし取り扱い方も特にエンジンで定められているものではないため名前以外の情報を仕込むことも可能です。

:::note warn

汎用性の高いテキストデータですが、以下のような制限があるため注意して使う必要があります。

* 上限が**50KB**
* ユーザーがメタバースから離れるとテキストデータは**消滅する**
  * 永続であってほしいデータはLocal Storageに入れるなどして別途永続化しておく必要があります

:::

また、このテキストデータは更新されると自分以外の全員に変更が通知されるほか、メタバースに入った際にも他の人のテキストデータが全員分通知されるようになっています。そのため、テキストデータの設定API（[`Player.setTextData`](https://github.com/VerseEngine/verse-three/blob/main/docs/verse-three.player.settextdata.md)）をうまく使うことで任意の情報のBroadcastを実現することができます。

:::note warn

注意点として、同じテキストデータを設定しても他者に通知されないため、Broadcastの目的で使う場合は設定するごとに異なる内容になるようにする必要があります。
UUIDなどを仕込んでもいいですが、P2Pネットワーク内で一意である必要はないので単純にローカルで数値をカウントアップしてそれをテキストデータにくっつけるだけで十分です。

:::

テキストデータの内容はテキストであれば自由なので、今回は以下のようなオブジェクトのJSON表記を持たせてやりとりさせています。

```ts
type TextData = {
    readonly user: {
        /**
         * ユーザーID(初回起動時に自動でUUIDを設定する)
         * ワールド内ではこれを使ってユーザーを一意に識別する
         */
        readonly identifier: string;
        /** 表示名 */
        readonly displayName: string;
    };
    readonly state: {
        /** 大型モニターの利用者か？ */
        readonly screenViewOwner: boolean;
    };
    /** 宣言（プログラム中ではsignalという）の内容。ない場合はフィールドごと消える */
    readonly signal?: PlayerRequestScreenViewOwnerSignal | PlayerGrantScreenViewOwnerSignal;
    /** signalごとのユニーク値。signalがない場合はフィールドごと消える */
    readonly signalNonce?: number;
};
```

### 利用宣言

リソースを使用したい場合は先のフローの通り利用宣言をBroadcastします。ただし、現在利用者がいない場合はBroadcastしても誰も反応しないので宣言をせずそのまま利用者になります[^conflict]。現在利用者がいるかどうかは、ワールドに入った際に送られてくる各ユーザーのテキストデータの内容で初期化し、その後テキストデータ更新が発生するたびに更新します。

利用宣言は特に難しいことはなくて、以下のような単純なメッセージです。

```typescript
type PlayerRequestScreenViewOwnerSignal = { readonly type: "RequestScreenViewOwner"; };
```

現在所有者になっているユーザーは、この宣言が含まれたテキストデータの更新メッセージを受け取ったら譲渡のフローにうつります。

### 譲渡宣言

リソースの利用権を譲渡する場合は、自身の利用状態をオフにしたうえで誰に譲渡するかを明示して譲渡宣言をBroadcastします。

```typescript
type PlayerGrantScreenViewOwnerSignal = {
    readonly type: "GrantScreenViewOwner";
    /** 譲渡するユーザーのidentifier */
    readonly to: string;
};
```

今回の簡易的な実装では`to`の値は受け取った側が自分であることを検証するために使っているだけです。
自分宛ての譲渡宣言を確認した場合、利用宣言を消して`state.screenViewOwner`を`true`に変更することで自身が現在の所有者であることをBroadcastします。

## おわり

[公式Tips](https://verseengine.cloud/ja/guide/verse-three-tips/text-data.html)にもテキストデータを使った簡易チャットの例がありますが、そこから大幅に応用してP2Pネットワーク上での中央集権でない利用権コントロールの仕組みを軽く考えて作ってみました。
テキストデータに任意のnonceを加えるだけで実質BroadcastのAPIになるので、そう考えると他にもいろいろ使い道が思いつきそうです。上限が50KBなのでいろいろ機能追加しすぎるとJSONでは溢れそうですが、そしたら他のコンパクトなシリアライズフォーマットにするなり通信内容を工夫したりできますし、仮にバイナリになってもbase64化すれば取り扱えるのでいろいろ検討の余地はあると思います。

VerseEngine自体は本当に静的ホスティングさえできる環境があればよいので（Cloudflare Tunnelやngrokでも可）[^codepen]非常に開発開始までのスピードが早かったと思います。出たばかりなのでコア機能はまだ少ないですが、今後機能が増えるともっといろんなことがサッと検証したり実装できたりしそうです。

[^ident]: メタバースの識別はURLによって行われているようなので、1つの静的ホスティングサービスで複数の異なるメタバースを展開することも可能そうです
[^webcam]: WebCameraについては特殊なことはしていないため今回の記事では記述しません
[^server]: サーバ作らないのは単にメンテがめんどくさいからです しっかり使えるものを作るならちゃんとした管理サーバ作ったほうが遥かに簡単かつ堅牢です
[^conflict]: ここで本来は同時に利用者になってしまった場合にどうするかを決めないといけないのですが、今回は簡単に作っているため特に対処はしていないです
[^codepen]: そういえばCodePenでもつかえますね