hyoromoのブログ

最近はVRSNS向けに作ったものについて書いています

VRChat Sketchbook Jam にエントリーしました


2021/05/22から2021/06/01期間に開催されていたVRChat公式主催のWorldJamにエントリーしました。
itch.io

決められた10日でお題に沿ったVRChatワールド作って公開しよう!ってイベントです。
本エントリーでは作成したワールドに関する情報を書きます。
※注意事項:VRChatではなく純粋なUnityの情報を得たくて来てしまった人はブラウザバックしてください。本記事はUnityであってUnityで無いものについて書いています。

公開ワールド

公開情報は以下の通り。

実装機能

以降については1度ワールドを訪れてから読まれるのを推奨致します。ちなみに今回が初ワールド公開な初心者なため、知識はあんまり無い状態で書いています。

ローカライズ対応



ワールド起動直後、会話形式のウィンドウ上で言語を選んで貰うようにしています。
このようなローカルユーザー毎に設定したい値は Networking.LocalPlayer.SetPlayerTag API で管理すると楽です。サンプルコードは以下の通り。

// 日本語が選ばれた時のTag設定。今回は2択なので JPN or ENG が値として設定される。
Networking.LocalPlayer.SetPlayerTag("Language", "JPN");

// 登録しておいたTagの値を取得
var language = Networking.LocalPlayer.GetPlayerTag("Language");
Collider内でPickup出来ない問題


色付きの台座に乗るとペンの色が台座色に変わる仕掛けを実装しました。

ペンはPickupで拾い、上図のCollider内に入ると色が変わります。
Pickupが曲者で、Collider内にPickup対象物が存在するとVRのHandで拾うことが出来なくなります。
そのため上図のCollider内で「ペンを落とすと拾えない」となってしまいます。

これ回避する良案が思いつかずに以下のような下策を取っています。PhysicsビューのLayer Collision Matrixで解決したかった...

// Pickup出来なくて困る原因となるColliderたち
[SerializeField]
Collider[] _colliders;

private VRCPlayerApi _player;

private bool _isCollider = true;

void Start()
{
    _player = Networking.LocalPlayer;
}

private void Update()
{
    var rightHand = _player.GetPickupInHand(VRC_Pickup.PickupHand.Right);
    var leftHand = _player.GetPickupInHand(VRC_Pickup.PickupHand.Left);
    if (_isCollider) {
        if (rightHand == null && leftHand == null) {
            // 両手に何も持っていなければColliderをOFF
            _isCollider = false;
            foreach (var collider in _colliders) {
                collider.enabled = false;
            }
        }
    } else {
        if (rightHand != null || leftHand != null) {
            // 両手のどちらかに何か持っていればColliderをON
            _isCollider = true;
            foreach (var collider in _colliders) {
                collider.enabled = true;
            }
        }
    }
}

手に何も持っていない状態だとCollider内にあるPickup Objectが拾えます。
ですが両手にペンを持っており、片方だけ落とすと拾えないです。
どなたか良い方法をご存知なら教えて欲しいです。

落ちているPickup Objectを初期位置に戻す

ペンを無意識に落としたり、誰かが隠してしまうと誰も描けなくなる問題が発生します。
なので、ペンが落とされてから15秒経過すると初期位置へ戻す対応をしています。

private const float RESPAWN_TIME = 15.0f;

private bool _isRespawn = false;
private float _dropTimer = 0f;
private Vector3 _initPos;
private Quaternion _initRotate;

 // 以下は後述のOwnerがワールドを抜けた時に必要
[UdonSynced(UdonSyncMode.None)]
private string _useDisplayName = "";

private void Start()
{
    _initPos = transform.position;
    _initRotate = transform.rotation;
}

private void Update()
{
    if (_isRespawn) {
        _dropTimer += Time.deltaTime;
        if (_dropTimer >= RESPAWN_TIME) {
            // しばらく放置されたのでリスポン
            _isRespawn = false;
            transform.SetPositionAndRotation(_initPos, _initRotate);
        }
    }
}

// 持った時に呼ばれる
public override void Interact()
{
    _isRespawn = false;
    _useDisplayName = Networking.LocalPlayer.displayName; // これは後述のOwnerがワールドを抜けた時に必要
}

// 離した時に呼ばれる
public override void OnDrop()
{
    _isRespawn = true;
    _dropTimer = 0.0f;
}

// 使い始めた時に呼ばれる
public override void OnPickupUseDown()
{
    _isRespawn = false;
}

// 使い終えた時に呼ばれる
public override void OnPickupUseUp()
{
    _isRespawn = false;
}

本来、OnPickupUseDown/OnPickupUseUpでフラグをoffにする必要無いと思うのですが
何故かVRの場合に限りペンで描いている途中に OnDrop がコールされてリスポン処理が実行されてしまう現象が発生しました。
最終チェック中に気付いたため応急措置的な対応をしてしまいましたが、もっと良い方法があるハズ...

Pickup中にOwnerがワールドを抜けた場合

前述した方法には1つ穴があります。
Pickupしている人がワールドを抜けた場合、ローカル処理のOnDropが行われないためPickup Objectが残り続けます。
なので、拾った人のdisplayNameを共有しておき、誰かがワールドを出た時にコールされる OnPlayerLeft内でMasterがチェックして措置します。

// 誰かがワールドから出た時に呼ばれる
public override void OnPlayerLeft(VRCPlayerApi player)
{
    // ワールドを出たのが現在座っていた人だった場合
    if (Networking.IsMaster) {
        if (player.displayName == _useDisplayName) {
            OnDrop();
        }
    }
}

引数で渡ってくる player に対して両手を GetPickupInHand でチェックしようとすると参照できずにエラー。Networking.IsOwner でチェック掛けても原因忘れましたが駄目でした。
displayName はアカウント名なのでユニークかと思われるため、この方法でも問題無い....といいなと思っています。

UdonGraph変数に値を受け渡す

UdonBehaviour#SetProgramVariable で受け渡せます。UdonSharp使っていると全く出番がありませんが、公式提供機能はUdonGraphになるため知っておくと今後の役に立ちそうです。
ちなみにメソッド呼び出しは UdonBehaviour#SendCustomEvent です。こちらはButtonイベントでUdonSharpで書かれたメソッドを呼び出したい時に使われるので皆知っているかなと。

経路探索

Unity APIである NavMesh はVRChatでも使えました。NavMesh自体については説明すると長くなる&他に良いサイトが沢山ありそうなので割愛します。経路探索する為のAPIという認識でいてください。
なお、動的に経路をbake可能な NavMeshComponents は使えません。そのため静的な経路探索しか行えないのですが、予め全ルートをbakeしておき進路妨害したい場所に NavMeshObstacle を配置しておけば問題解決する場合があります。


上図の2枚目の水色地面がBake済み地面です。各道と色付き台座の間にあるColliderが NavMeshObstacle となります。
色付き台座にたどり着くと魂が台座の中に入らず、次の台座への道が拓けると入ってくる。という仕掛けは NavMeshObstacle を非アクティブにしているだけです。

全日程どのように作業を進めたか

1日目(5/22)

とりあえず公式から配布された「VRChat Sketchbook Jam World」プロジェクトを少しだけ改造し、UnityChan SDトーコちゃんをNavMesh使ってペンを追ってくる実装だけしました。

その程度しか進まず、あとはミリオン7thライブday1のライブ生配信を堪能していました。
idolmaster-official.jp

2日目(5/23)

NavMeshがVRChatでどこまで利用可能かをチェックし、あとは仮マップの作成を進めていました。

それとこの日もミリオン7thライブday2のライブ生配信を堪能していました。ただこの時間は全く無駄な時間ではなく、(※ライブのセトリバレ配慮で白文字)「ココロがかえる場所」と「夢色トレイン」をライブで聞き、虹色と魂に着目するようになりました。たぶん
idolmaster-official.jp

3日目(5/24)

ここから平日となり作業時間が大幅に減ります。
ペンのUdonGraphを開いて読んでいました。色の同期対応をしようとして失敗しています。

4日目 (5/25)

2日目に作った仮モデルのままだったため、モデルの作り直しをProBuilderで行いました。

5日目 (5/26)

消しゴム機能に修正が入り、「VRChat Sketchbook Jam World v1.0.1」プロジェクトが公開されました。それに伴ってか期間が2日ほど伸びました。
なので、この日は遊んでいました。メッセージウィンドウを1文字ずつ表示させたり、次文字が表示されるまでに記号表示させてみたり。といった機能実装をしています。

6日目 (5/27)

4日目に作ったモデルに対し、ユーザーが落ちないように見えない壁をProBuilderで作成しました。

7日目 (5/28)

大きいペンを用意する事にして、大きいペンには箒のように乗れて線を後ろから射出できる。機能実装をしていましたが、ワールド作成経験が無い状況下で実装が難航しそう&テストがゲキヤバで残り日数で終わら無さそうだったため1日で諦めました。

8日目 (5/29)

大きいペンはペンで描いた線に乗れるよう実装しておきました。
ここまでで正常系の通しは問題なく、現在公開しているものとほぼ同程度のものが完成しました。

9日目 (5/30)

異常系のテストとバグ潰しをひたすら行いました。
ワールド公開用のサムネイル画像やワールド名、説明書きの内容を考えてこの日にCommunity Labへ公開&WorldJamエントリー要件のitchへの情報公開手続きを実施しました。
itch.io

感想

疲れました。

ワールド制作歴4ヶ月しかなく、ワールド公開経験が無かったため他の人が手間取らないであろう事にも手間取っていました。
この短い期間にデスクトップPC/PCVRそれぞれで問題なく動作するワールド作るの大変過ぎなのでは?同期テストするだけでメッチャ時間取られるんですけどーーーー

もし次回があるなら今度は単機能に絞りたいです。2-3日で実装、あとは余裕を持ったテスト期間に回せる規模が理想的じゃないかなぁ...