simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

VRoidHubにログインしないでゲームを続けられる実装調査

ログインしないときの動作を策定するにあたり、現状を整理し、作業ログを残します。

簡単に今のフローをおさらい

VRoid Hub のログインは VRoid SDK のサンプルに任せっきり
ゲームを起動すると

f:id:simplestar_tech:20190915150327p:plain
起動直後
「VRoid Hub に接続」ボタンを表示、同時に「☓」の閉じるボタンがある
接続ボタンを押すと、次のとおり Web ブラウザが起動し、連携許諾の画面の後、認証コードを表示する画面が出てくる
f:id:simplestar_tech:20190915150745p:plain
連携します→認証コード入力画面
一応毎回認証コード違うので、もうこの値を入れても有効にはなりません…モザイクもかけない

で、ゲーム画面に戻ると

f:id:simplestar_tech:20190915150944p:plain
認証コード入れて
このような画面になっているので、先程のコードを入力する
f:id:simplestar_tech:20190915151034p:plain
キャラクター選択画面
キャラクター選択ができるが、このときも「☓」ボタンでダイアログを閉じることが可能

キャラを選択すると「利用する」「キャンセル」ボタンがある

f:id:simplestar_tech:20190915151314p:plain
選択した後

キャンセルすると、一つ前のキャラクター選択画面に戻る

「利用する」を選ぶと、次の通りキャラクターがゲームに登場する

f:id:simplestar_tech:20190915151655p:plain
ゲーム画面

ログインをやめたとき

「次のディフォルトキャラを利用しますか?」
「利用する」「キャンセル」

f:id:simplestar_tech:20190915151314p:plain
選択した後
を出してみようと思う

この画面は CharacterLicenseScrollView といって

まぁ、調べると複雑なのでユーザーは VRoidHubController の SetOnCancelHandler ですべてのキャンセルをハンドリングします。
ここで CharacterLicenseScrollView をアクティブ化するロジックをハンドラに追加してみましょう。
期待では、キャラ情報のない画面が出る

具体的にはこのように書き

        this.controller.SetOnCancelHandler(() => {
            this.licensePanel.SetActive(true);
            this.menuCanvas.SetActive(true);
            this.controller.gameObject.SetActive(true);
        });

結果は次の通り

f:id:simplestar_tech:20190915155525p:plain
キャラ情報のない画面が出る

ボタンとして「利用する」「キャンセル」があるが
「利用する」を押した場合は、独自の VRM ロードロジックに付け替える
「キャンセル」を押した場合は、元のログイン画面の表示につけかえる
想定できる問題に、キャンセルした後から普通のフローをたどると、「利用する」ボタンのロジックが切り替わったままになるので
キャンセル時にすべてを元に戻すことをしなければならない

では、まず、ロジック付替えるテストコード
動的に永続イベントを無効化して付け替えるには、このように書くしかなさそう…いい知見を得ました。

    private void OnCancelButtonClicked()
    {
        // 固定キャラクター利用を拒否されたので、最初のログイン画面に切り替え
        this.loginCanvas.SetActive(true);
        this.mainPanel.SetActive(true);
        this.menuCanvas.SetActive(false);
        this.licensePanel.SetActive(false);
        // 切り替えたボタンのロジックを元に戻す
        this.acceptButton.onClick.RemoveAllListeners();
        this.acceptButton.onClick.SetPersistentListenerState(0, UnityEventCallState.EditorAndRuntime);
        this.retryButton.onClick.RemoveAllListeners();
        this.retryButton.onClick.SetPersistentListenerState(0, UnityEventCallState.EditorAndRuntime);
        this.cancelButton.onClick.RemoveAllListeners();
        this.cancelButton.onClick.SetPersistentListenerState(0, UnityEventCallState.EditorAndRuntime);
    }

    private void OnAcceptButtonClicked()
    {
        // streamingAssets から vrm をインスタンス化
        var vrmFilePath = Path.Combine(Application.streamingAssetsPath, "VRM/defaultvrm.vrm");
        var context = new VRMImporterContext();
        context.Parse(vrmFilePath);
        context.LoadAsync(() => {
            context.ShowMeshes();
            var vrmModel = context.Root;
            vrmModel.name = "defaultvrm";

            // プレイヤーとしてコンポーネントを追加
            ComponentUtil.DeleteAllChildren(this.transform);
            int layerMask = LayerMask.NameToLayer(LayerMask_Player);
            vrmModel.SetLayerRecursively(layerMask);
            vrmModel.SetTagRecursively(Tag_Player);
            // ただし characterModelId はディフォルト値とする
            this.SetModel("defaultvrm", vrmModel, isUserPlayerFlag: true);

            // VRoid Hub メニューを閉じる
            this.controller.Close();
        }, (exception) => { Debug.LogError($"defaultvrm load error: {exception.Message}"); });
    }

    private void Start()
    {
        this.controller.Open();
        this.controller.SetOnLoadHandler((characterModelId, vrmModel) =>
        {
            ComponentUtil.DeleteAllChildren(this.transform);
            int layerMask = LayerMask.NameToLayer(LayerMask_Player);
            vrmModel.SetLayerRecursively(layerMask);
            vrmModel.SetTagRecursively(Tag_Player);
            this.SetModel(characterModelId, vrmModel, isUserPlayerFlag:true);
        });
        this.controller.SetOnCancelHandler(() => {
            // キャラクター利用画面を表示
            this.licensePanel.SetActive(true);
            this.menuCanvas.SetActive(true);
            this.mainPanel.SetActive(false);
            this.loginCanvas.SetActive(false);
            this.controller.gameObject.SetActive(true);
            // 既存の「利用する」ボタンと「キャンセル」ボタンの動作を無効化
            this.acceptButton.onClick.SetPersistentListenerState(0, UnityEventCallState.Off);
            this.acceptButton.onClick.RemoveAllListeners();
            this.retryButton.onClick.SetPersistentListenerState(0, UnityEventCallState.Off);
            this.retryButton.onClick.RemoveAllListeners();
            this.cancelButton.onClick.SetPersistentListenerState(0, UnityEventCallState.Off);
            this.cancelButton.onClick.RemoveAllListeners();
            // 新しいロジックに付け替え
            this.acceptButton.onClick.AddListener(this.OnAcceptButtonClicked);
            this.cancelButton.onClick.AddListener(this.OnCancelButtonClicked);

            // streamingAssets から vrm 情報を取得
            var characterLicensePanel = this.licensePanel.GetComponent<CharacterLicensePanel>();
            characterLicensePanel.Init(new VRoidSDK.CharacterModel
            {
                portrait_image = new VRoidSDK.PortraitImage { original = new VRoidSDK.WebImage { url = "" } },
                name = @"<b><size=28>千駄ヶ谷 篠</size></b>
<size=24> Welcome School Uniform 2019 </size>
<size=24> 作者:<b> VRoidプロジェクト </b></size>",
                license = new VRoidSDK.CharacterLicense {
                    characterization_allowed_user = "everyone",
                    violent_expression = "allow",
                    sexual_expression = "allow",
                    corporate_commercial_use = "allow",
                    personal_commercial_use = "profit",
                    modification = "allow",
                    redistribution = "allow",
                    credit = "unnecessary"
                }
            });
            this.caracterImage.texture = this.defaultvrmIcon;
        });
    }

キャンセルして元に戻すことができているので、あとは利用するボタンを押した時に、リソースから VRM を読み出そうと思います。
Prefab のインスタンス化で良いかな?→惜しいところで、vrmAvatar が内蔵されないので、キャラクターを動かす時に不都合を確認
ファイルから直接読み込むように StreamingAssets から直接読み込むようにしました。

色々修正して上の実装の通り

現在、VRoid Hub の接続を拒むと、次の画面が出て、利用するボタンを押すと、この子が使えるようになってます。

f:id:simplestar_tech:20190915210658p:plain
VRoid Hub の接続を拒むと出てくる画面

あとは、通信時にキャラクターIDを正しく相手に送る必要があるので、正しい ID とやらを確認します。
648876553405728395 でした。

そして、VRoid Hub への接続を拒んだクライアントでは、常にキャラクターをこの子とする実装が求められます。
それもテストしてみましょう。

あれ、方やログインしてないのに動いちゃった
そういうものなのかな

ああ、キャッシュをクリアしたらやっぱり認証でエラーが確認できました。
そういうときは、ディフォルトの vrm を読み込むようにしましょう。

これで行けると思うんだけど…

    IEnumerator CoWaitEmptyMapAndLoadAsync(ContentUrlResponse response)
    {
        // ロードは複数同時に走らせない
        while (this.isVRoidLoading)
        {
            yield return null;
        }
        this.isVRoidLoading = true;
        this.lastUrlResponse = response;
        if (Authentication.Instance.IsAuthorized())
        {
            // ログインできている場合は API 経由でキャラデータをロード
            var characterModel = new CharacterModel();
            characterModel.id = response.URL;
            HubModelDeserializer.Instance.LoadCharacterAsync(characterModel, OnLoadComplete, OnDownloadProgress, OnError);
        }
        else
        {
            // ログインしていない場合は、ディフォルト VRM で代用
            var vrmFilePath = Path.Combine(Application.streamingAssetsPath, "VRM/defaultvrm.vrm");
            var context = new VRM.VRMImporterContext();
            context.Parse(vrmFilePath);
            context.LoadAsync(() => {
                context.ShowMeshes();
                var vrmModel = context.Root;
                vrmModel.name = response.UserUniqueId.ToString();
                this.OnLoadComplete(vrmModel);
            }, (exception) => { Debug.LogError($"defaultvrm load error: {exception.Message}"); });
        }
    }

できました!

f:id:simplestar_tech:20190915220315p:plain
VRoid Hub にログインせずにオンラインチャット

色んなユーザーがルームに入ってきても、全部ディフォルトのキャラとして映ります…
これにて機能追加完了