LTシステムの開発 VRChat

# 1 この記事は

VRChatのLTシステムの設計、導入方法を説明します。なお、本記事で設計したシステムは、下記の「vrc-lt」の大半を流用しておりますが、一部機能を追加しております。

https://github.com/vrc-lt/SlidePlayer

<b>追加した機能</b>
・スライドのページ数を取得する。(前提条件：1スライド1秒の動画になっていること)
・GUIにURLを入力しない場合、あらかじめInspectorで設定した動画を読み込む設定にした。
・スライドが最終頁まで到達した場合、ページ送りを停止する。
・UI,コードの最適化

# 2 スライドシステムの説明

本LTシステムは1ページ1秒のコマ画像を複数つないだ動画を動画プレイヤーで読み込み、1ページ1秒の動画を1秒ごとに前後に移動し停止するという制御を行うことでLTシステムを実現しております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/eeafb8fb-a394-ad05-2f03-2d6f71503b43.png)


# 3 LTシステムの導入方法 & 使い方

下記URLをご覧ください。

https://github.com/fdfpy/tak1123slidesystem#readme



# 4 設計方法解説

本LTシステムの設計内容をここでは説明します。

## 4-1 コード説明
下記に本LTシステムをコントロールするコードを記載します。

```c#:Slidecontrol
using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.SDK3.Components;
using VRC.SDK3.Video.Components;
using VRC.SDK3.Components.Video;


namespace VRCLT
{
    [AddComponentMenu("SlidePlayer")]
    [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
    public class SlidePlayer : UdonSharpBehaviour
    {

        //****** (1) 使用する変数を定義する *********

        [UdonSynced] [FieldChangeCallback(nameof(URL))] VRCUrl _syncedURL; //(同期変数)再生する動画のURL
        [UdonSynced] [FieldChangeCallback(nameof(Page))]private int _syncedPage; //(同期変数)スライドのページ番号
        [UdonSynced] [FieldChangeCallback(nameof(Cont0))] private bool _cont0=true; //(同期変数)true:スライドのページ数総数をワールドに埋め込む。false:スライドのページ数総数を動画から読み取る
        public float  AllPagenum;　//スライドのページ数総数(Inspectorで設定する)
        [UdonSynced(UdonSyncMode.None), FieldChangeCallback(nameof(allpagenum))] private float _allpagenum;//(同期変数)動画から取得したスライドのページ総数
        [UdonSynced(UdonSyncMode.None), FieldChangeCallback(nameof(Endpage))] private float _endpage;//(同期変数)スライドのページ総数
        public VRCUrl seturl; //Inspectorで与えた再生する動画のURL
        public Text Allpage,Nowpage; // Allpage:スライドページ総数を表示するテキストオブジェクト, Nowpage:現在のスライドのページ数を表示するテキストオブジェクト
        public VRCUrlInputField inputField; //再生する動画のURLを入力するテキストボックスオブジェクト
        public Text statusText,seigyo0Text; //statusText:現在の状態を示すテキストオブジェクト, seigyo0Text:スライドのページ数総数をワールドに埋め込む or スライドのページ数総数を動画から読み取るのどちらの設定になっているかを表示するテキストオブジェクト
        public VRCUnityVideoPlayer unityVideoPlayer; //動画プレイヤーオブジェクトを定義する。
        public float timeSpan = 1f;
        private float timeOffset = 0.1f; // (Page-1) * timeSpan + timeOffset の地点で動画を停止させる。そのためのparameter値を設定する。


        //****** (2) 同期変数にプロパティーを定義する。 *********

        private VRCUrl URL //(同期変数)再生する動画のURL
        {
            get => _syncedURL;
            set
            {
                _syncedURL = value;
                Debug.Log("ChangeVideoURL: " + value);
                unityVideoPlayer.LoadURL(value);
            }
        }

        private int Page //(同期変数)スライドのページ番号
        {
            get => _syncedPage;
            set
            {
                _syncedPage = value;
                ChangeVideoPosition();
                DisplayNowpage();
            }
        }


        private float allpagenum  //(同期変数)動画から取得したスライドのページ総数
        {
            get => _allpagenum;
            set
            {
                _allpagenum = value; 
                DisplayAllpage();
            }
        }

        private bool Cont0   //(同期変数)true:スライドのページ数総数をワールドに埋め込む。false:スライドのページ数総数を動画から読み取る
        {
            get => _cont0;
            set
            {
                _cont0 = value; 
                DisplaySeigyo0();
            }
        }


        private float Endpage //(同期変数)スライドのページ総数
        {
            get => _endpage;
            set
            {
                _endpage = value; //valueキーワードが使用でき、これにアクセス元から渡された値が格納されています。
                DisplayEndpage();
            }
        }


        //****** (3) 各種メソッドを定義する。 *********


        //オブジェクトの操作権限を取得する。
        public void OnTakeOwnershipClicked()
        {
            Debug.Log("Take ownership");
            Networking.SetOwner(Networking.LocalPlayer, gameObject); //ゲームオブジェクトの所有権をボタンをクリックしたユーザーに移す
            statusText.text = "Changed owner";
            inputField.gameObject.SetActive(true);
        }

        //スライドの読み込みを行う関数
        public void OnURLChanged()
        {

            VRCUrl url;
            url = inputField.GetUrl();


            if (url.ToString() == "") {
              url = seturl;
             }

            if (Networking.IsOwner(gameObject) && url != null)
            {
                Debug.Log("OnURLChanged url: " + url.ToString());
                statusText.text = "Loading...";
                //Page = 1;
                URL = url;
                RequestSerialization();
            }
            else
            {
                statusText.text = "You must be the owner to set the URL ";
            }
        }

        //スライドのページを1ページ進める関数。ただし、スライドのページが最終ページまで到達した場合はPage++の動作を停止する。
        public void OnNextSlideButtonClick()
        {
            if (Networking.IsOwner(gameObject))
            {
                Debug.Log("OnNextSlideButtonClick as owner");

      

                if (Page < Endpage)
                {
                    Page++;
                    RequestSerialization();
                }
            }
            else
            {
                statusText.text = "Owner: " + Networking.GetOwner(gameObject).displayName;
            }
        }

        //スライドのページを1ページ戻す関数
        public void OnPrevSlideButtonClick()
        {
            if (Networking.IsOwner(gameObject))
            {
                Debug.Log("OnPrevSlideButtonClick as owner");
                if (Page > 1)
                {
                    Page--;
                    RequestSerialization();
                }
            }
            else
            {
                statusText.text = "Owner: " + Networking.GetOwner(gameObject).displayName;
            }

        }

        //スライドのページを初めに戻す関数
        public void OnResetButtonClick()
        {
            if (Networking.IsOwner(gameObject))
            {
                Debug.Log("OnResetButtonClick as owner");
                Page = 1;
                RequestSerialization();
            }
            else
            {
                statusText.text = "Owner: " + Networking.GetOwner(gameObject).displayName;
            }


        }


        //再生していている動画に対し、設定した時間に移動し、動画を停止する関数
        private void ChangeVideoPosition()
        {
            Debug.Log("ChangeVideoPosition: " + Page);
            unityVideoPlayer.SetTime((Page-1) * timeSpan + timeOffset);
            //Nowpage.text = Page.ToString();
        }

        public override void OnOwnershipTransferred()
        {
            if (!Networking.IsOwner(gameObject))
            {
                inputField.gameObject.SetActive(false);
            }
        }

        //動画の読み込みを終了したら実行する関数
        public override void OnVideoReady()
        {
            Debug.Log("OnVideoReady");
            Page = 1;

            //ChangeVideoPosition();
            if (!Networking.IsOwner(gameObject))
            {
                statusText.text = "Video ready. Owner: " + Networking.GetOwner(gameObject).displayName;

               
            }
            else
            {
                statusText.text = "Video ready. click \"next\" on control panel to start presentation.";
               
                allpagenum = unityVideoPlayer.GetDuration(); //動画の全再生時間を取得する。
                Endpage = AllPagenum; //Inspectorで与えた動画の全再生時間をEndpageにアサインする。

            }
        }

        //変数Endpageの値を AllPagenum(外部から与えたページ数)またはallpagenum(動画の長さを自動で読み取った数値)のいずれかに切り替える。
        public void Sengyo0()
        {
            if (Networking.IsOwner(gameObject)) 
                {
                Cont0 = !Cont0;
                RequestSerialization();
                Endpage = Cont0 ? AllPagenum : allpagenum;  //Cont0 =Trueならば AllPagenum(外部から与えたページ数)を使う、 Cont0=Falseならば allpagenum(動画の長さを自動で読み取った数値)を使う
                RequestSerialization();

            }
        }


        public void DisplaySeigyo0()
        {
            if (Cont0 == true)
            {
                seigyo0Text.text = "手動入力";    // データ表示更新
            }
            else if(Cont0 == false)
            {
                seigyo0Text.text = "自動入力";    // データ表示更新
            }



        }

        // 同期変数の値をUIに表示する処理
        public void DisplayNowpage()
        {
            Nowpage.text = Page.ToString();    // データ表示更新
        }


        // 同期変数の値をUIに表示する処理
        public void DisplayEndpage()
        {
            Allpage.text = Endpage.ToString();    // データ表示更新
        }



        // 同期変数の値をUIに表示する処理
        public void DisplayAllpage()
        {
            Allpage.text = allpagenum.ToString();    // データ表示更新
        }

        //動画再生時に発生したエラーを表示させる。
        public override void OnVideoError(VideoError videoError)
        {

            switch (videoError)
            {
                case VideoError.RateLimited:
                    statusText.text = "Rate limited, try again in a few seconds";
                    break;
                case VideoError.PlayerError:
                    statusText.text = "Video player error";
                    break;
                case VideoError.InvalidURL:
                    statusText.text = "Invalid URL";
                    break;
                case VideoError.AccessDenied:
                    statusText.text = "Video blocked, enable untrusted URLs";
                    break;
                default:
                    statusText.text = "Failed to load video";
                    break;
            }
        }
    }
}

```


上記コードの関数(メソッド)とオブジェクトの配置関係は下記の図のとおりです。
![409.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/77e98549-424d-8450-ead3-667249828704.jpeg)

## 4-2 処理の流れフローチャート説明

![405.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/22cc9eaf-ba1a-996f-1ea2-701f2a18da72.jpeg)
![406.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/d90a44ee-206d-c74b-69a5-9491a6cc9d7d.jpeg)
![407.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/71058cea-28b1-ff0e-c357-ac5e5b736465.jpeg)
![408.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/1f245d20-83a1-a6e5-722c-b89b541639cb.jpeg)


# 5 複数のLTシステムを配置する。

ここでは複数のLTシステムを配置し稼働させる方法を説明します。

【STEP1】Hierarcy windowにて本LTオブジェクト(LTSYS)をコピーし、2つ目のLTオブジェクト(LTSYS(1))を生成します。

![410.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/1f310e86-c818-4449-f32c-a6cdeb32e910.jpeg)

LTオブジェクト(LTSYS)がprefab化されている場合は、Unpack Prefab Compltelyとすること。

![418.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/ec509f01-4feb-ebf5-fef5-b0d9ba28ce2d.jpeg)



【STEP2】新たにRender Textureを作成します。Assets->Create->Render Texture

![411.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/2811e2f3-d9bf-30d8-7f51-b3c70c641620.jpeg)

【STEP3】新たにRender Textureのパラメータを設定します。sizeは1280×720とし、Rendar Textureの名前をVideo1にします。

![412.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/250d3d04-483e-95ba-2b63-10c69037b9c8.jpeg)

【STEP4】作成したRender Texture(名前：Video1)をスライド表示画面にドラッグ & Dropします。
![413.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/07855b49-36ff-3ad5-ed58-d5e5436072bd.jpeg)

【STEP5】
2つ目のLTオブジェクト(LTSYS(1))のスライド表示画面(Quad)のInspecter画面を開き、作成したRender Texture(名前：Video1)をTarger Textureにアタッチメントします。
![414.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/02d0237a-54f7-affb-fe35-16220e2bced2.jpeg)

【STEP6】
2つ目のLTオブジェクト(LTSYS(1))のスライド表示画面(Quad)のInspecter画面を開き、再生したいスライドのURLを入力します。
![415.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/12b0b0af-e566-7c7e-9e43-06d5a4c57848.jpeg)

【STEP7】
それぞれのLTオブジェクトで独立してスライドが制御できることを確認します。

![416.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150223/951ebc90-56b9-6662-2986-65a367384fc7.jpeg)