20200526のUnityに関する記事は4件です。

Unity入門 ゲームジャムでゲームを作っていくコツについて

私も この間のゲームジャムに参加しましたけど。 

1週間で ゲームを作るというのは ほんと 
大変ですからね。 

今回は 私のやっている方法を紹介します。 

 0日目から 2日目 

出来れば、最初の48時間以内に ある程度のアイデアを固めて 

それを企画書にしていきます。 

企画書といってもそんなたいそうなものではなく、
 ホワイトボードに適当に書いてく感じで まとめていきます。 

じゃあ、 アイデアはどっから出すのか という所ですが。 

私の場合は、色んなゲームを片っ端から見ています。 

特に参考にしているのは 

  • レトロゲーム 

  • インディーゲーム 

  • ハイパーカジュアルゲームです。 

この3つは普通に参考になるので おすすめです。 

あと最初にやることは 

Unityの コラボレートと クラウドビルドの設定も必要でしょう。 

コラボレートを使うと 作業の共同ができたり 

バックアップができます。 

そして、 クラウドビルドを使えば、 クラウド側でビルドができるので 

時間を大幅に節約できます。  

普通のビルドであれば、 1回につき30分以上はかかりますからね。 

クラウドビルドは有料サービスなんですが、 使ったほうがいいでしょう。 

2日目から 3日目 

ゲームのアイデアがある程度決まったら 

今度は素材集めなどをしていきます。 

アセットストアで探したり、GitHubで探したりと 

色々探して 先に入れておきます。 

サウンドなども 最後につける人もいるかと思いますが 

私は先につけるタイプです。 

使えるものはどんどん使っていくべきでしょう。 

3日目から 6日まで 

この間でゲームを仕上げていきます。 

時間は、ゲームにもよりますけどほとんどの人は 
10時間~30時間ぐらいで 作っている人が多いので それぐらいを目安にするといいと思います。

私も この間 出したときは、 だいたい15時間ぐらいかかりました。 

ゲームを作るときで重要なのは 

やること と やらないことを明確にすることです。 

1週間しかないので、 あれもこれも入れようとすると 

普通に間に合いませんからね。 

期限にちゃんと 間に合わせることがもっとも重要です。 

だって、そうゆう企画ですからね 時間厳守ですよ。

特化する部分としない部分を明確にして 

特化する部分に時間を使うようにしていきます。 

特に初心者の人はできることが限られていると思うので 

自分のできる範囲で 特化できる部分を見つけていきましょう。 

初心者の人であれば、 一つのシーンや 一つのフィールドで 

完結するゲームがいいと思います。 

シーンが沢山増えてくると、 クオリティを揃えるのがめんどくさいですからね。 まじで。

あと大事なのは、 操作性だけはちゃんとしたほうがいいです。

操作性が悪いと どんなゲームもつまんなくなるので 

操作性はちゃんとするべきでしょう。

変な所に拘る人もいますけど、 それだったら操作性に拘ったほうがいいでしょうね。

6日目から 7日目 

残りの24時間ぐらいは 基本 

デバッグとテストプレイをしていく時間です。 

作るゲームによっても 違うと思いますが 

デバッグは結構かかります。  

私もこの間出したときは、 クラウドビルド8回ぐらいやったし 

微調整も4時間ぐらいかかった。 そしてテストプレイもしないと

いけないので 1日ぐらいは平気でかかります。

ゲームを提出するときは、時間に余裕をもたせたほうがいいです。 

最後の最後で エラーが出ることは普通にありますからね。 

気をつけましょう。 

PS 

Unity講座も 販売してまーす。

 
セールは31日で終了ですよ。

    

    

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ADX2 for UnityのCRI Atom Windowを拡張する

Atom Windowの機能追加

サウンドミドルウェアADX2のUnityプラグインには、「CRI Atom Window」というエディタ拡張が同梱されています。
ADX2のツールから出力したパックデータをUnityプロジェクトに読み込み、内容を確認したり、サウンドを鳴らすためのゲームオブジェクトを配置できる機能です。

AtomWindow.png

ADX2は、サウンドの鳴り方を同梱ツールである「Cri Atom Craft」で行い、Unity側ではゲーム実行中の再生リクエストやパラメータ渡しを作っていくワークフローを提供します。
そのためUnity Editor上でADX2のデータそのものを加工する機能はありません。Atom Windowの主な機能は「データのインポート」と「データ内容の確認」です。

本投稿では、それらの機能を拡張する方法を紹介します。
今回は「キューのカテゴリと再生上限数(キューリミット)の追加表示」「ADX2データのインポート先を指定可能にする」の2つを紹介します。拡張後のCRI Atom Windowはこんな感じになります。

newatomwindow.png

アップデート時の注意

Atom Windowのスクリプトをいじると、当然正規プラグインとの互換性は壊れます。
SDKバージョンアップデートの際にはご注意ください。

無償版「ADX2 LE」と製品版「ADX2」の違い

本投稿はADX2 LEをベースとした拡張方法を紹介していますが、製品版ADX2でも同様の拡張が可能です。製品版ADX2ではAtom Windowの代わりに「Atom Browser」というエディタ拡張が同梱されています。各項目で製品版ADX2の場合についても補足します。

データのインポート先パスを指定可能にする

Atom WindowにはAtom Craftがビルド・出力したADX2のデータをUnityのプロジェクト内にコピーする機能があります。しかし、コピー先は指定することができず、すべて「Streaming Assets」直下に配置されます。

AssetBundle対応などでStreaming Assetsに様々なファイルを使用しており、ADX2データを特定のフォルダ内に配置している場合や、ビルド時にアプリに含むADX2データを選択したい場合などは、毎回手動で移動するのは面倒です。エディタ拡張に手を入れて、コピー先パスを指定可能にしましょう。

Atom Windowの設定ファイルに「インポート先フォルダ」のフィールドを足す

Atom Windowは設定ファイルを「CriAtomWindowPrefs」ScriptableObjcetとして保存しています。
コピー元のパスを保持しているフィールドがありますので、その下に「importFolderPath」としてstringのフィールドを追加しましょう。

CriAtomWindowPrefs.cs
public class CriAtomWindowPrefs : ScriptableObject
{
    public string outputAssetsRoot = String.Empty;
    public string importFolderPath = String.Empty;//このフィールドを追加

    //(略)
}

ここに保存先のパスを記録します。

Atom Windowに保存先パスを指定するInputFieldを足す

次に、Atom Window上で保存先パスの指定を行えるようにInputFieldを足します。
CriAtomWindow.csのGUIImportAssetsFromAtomCraftメソッド内、outputAssetsRootにデータを渡す処理の下に次のスクリプトを足してください。

CriAtomWindow.cs
    private void GUIImportAssetsFromAtomCraft()
    {

        //(略)
            GUILayout.EndHorizontal();

            if (criAtomWindowPrefs != null) {
                criAtomWindowPrefs.outputAssetsRoot = GUILayout.TextArea(criAtomWindowPrefs.outputAssetsRoot);
            }
            //GUILayout.Label(Application.dataPath);

        //(略)
        //以降を新規追加
            GUILayout.BeginHorizontal();
            GUILayout.Label("Import Folder Path:");

            if (GUILayout.Button("Select Folder Path")) {
                string tmpStr = String.Empty;
                tmpStr = EditorUtility.OpenFolderPanel("Select import folder", tmpStr, criAtomWindowPrefs.outputAssetsRoot);
                if (tmpStr != String.Empty) 
                {
                    criAtomWindowPrefs.importFolderPath = tmpStr;
                    criAtomWindowPrefs.Save();
                }
            }
            GUILayout.EndHorizontal();

            if (criAtomWindowPrefs != null) {
                criAtomWindowPrefs.importFolderPath = GUILayout.TextArea(criAtomWindowPrefs.importFolderPath);
            }

        //(略)
        //以上を新規追加

    }

これで保存先のパスを指定できるようになりました。例ではStreamingAssetsの下に「adx2data」フォルダを用意して、そこを指定しています。

AtomWindow2.png

なお、この2行を足したことによってAtom Windowの縦の長さが変わってしまい、スクロールバーが出てしまいます。
次の項目を更新して、スクロールバーをなくします。

CriAtomWindow.cs
    private void GUICueList()
    {
    //(略)
        var acbInfoList = acfInfoData.GetAcbInfoList(false, searchPath);
        if (acbInfoList.Length > this.selectedCueSheetId) {
            var acbInfo = acbInfoList[this.selectedCueSheetId];

            if (acbInfo.cueInfoList.Count > 0) {

                //ウィンドウの高さ設定を変更(元の数値:- 354.0f)
                float height = this.position.height - 390.0f;

    //(略)

データコピー処理に指定したフォルダパスを使用する

最後にcriAtomWindowPrefs.importFolderPathに保存したパスへデータコピーする処理を追加します。
GUIImportAssetsFromAtomCraftメソッドの最後の方、CopyDirectoryメソッドの引数を差し替えます。

CriAtomWindow.cs
    private void GUIImportAssetsFromAtomCraft()
    {
        //(略)
        //CopyDirectory(criAtomWindowPrefs.outputAssetsRoot, Application.dataPath);//これを消して
        CopyDirectory(criAtomWindowPrefs.outputAssetsRoot + "/StreamingAssets/", criAtomWindowPrefs.importFolderPath); //こうする
        //(略)
    }

outputAssetsRootで指定されるパスはデータ直上ではなく、ディレクトリStreamingAssetsが挟まりますので、それを勘案しています。
「Update Asset of "CRI Atom Craft"」をクリックすれば、指定フォルダにファイルがコピーされます。
また、コピー先のパスはScriptableObjectとして保存されるのでデータを落としても大丈夫です。

Atom Browser(製品版ADX2)の場合

スクリプトの名称は「CriAtomWindow.cs」「CriAtomWindowPrefs.cs」から変更ありません。
「「インポート先フォルダ」のフィールドを足す」「データコピー処理に指定したフォルダパスを使用する」手順も同様です。
「Atom Windowに保存先パスを指定するInputFieldを足す」については、GUIImportAssetsFromAtomCraftメソッドのGUI処理が大きく変わっていますので、次のスクリプト追加を行います。

CriAtomWindow.cs
    private void GUIImportAssetsFromAtomCraft()
    {
        //(略)
            GUILayout.BeginHorizontal();
            {
                if (criAtomWindowPrefs != null) {
                    criAtomWindowPrefs.importFolderPath = EditorGUILayout.TextField("Import To:", criAtomWindowPrefs.importFolderPath);
                }

#if !OPENFOLDERPANEL_IS_BROKEN
                if (GUILayout.Button("...", EditorStyles.miniButton, GUILayout.Width(50))) {
                    string tmpPath = "";
                    string errorMsg;
                    tmpPath = EditorUtility.OpenFolderPanel("Select Import Folder", criAtomWindowPrefs.importFolderPath, "");
                    criAtomWindowPrefs.importFolderPath = tmpPath;
                    criAtomWindowPrefs.Save();
                }
#endif
            }
            GUILayout.EndHorizontal();

        //(略)
    }

Atom Windowのキューリストにカテゴリや再生数上限などを表示

デフォルトのCri Atom Windowでは、キューの情報として「キュー名」「キューID」「User Data」が表示されます。プロジェクトや制作体制によっては、もう少しキューの情報を確認したい場合があります。

そこで、ほかのパラメータもUnity Editor上で確認できるようにAtom Windowを拡張します。
この例では、「カテゴリ」と「キューリミット」の情報を取得・表示します。

キュー情報を保存するクラスにフィールドを追加する

まずはキューのインポート時に必要なデータを取り出す処理を書きます。
CriAtomProjinfo.csのパーシャルクラスCriAtomAcfInfo内でCueInfo保存のSerializableクラスが定義されています。これに表示したいキューの情報用フィールドを足します。

CriAtomProjinfo.cs
    #region CueInfo
    [Serializable]
    public class CueInfo : InfoBase
    {
        public short numLimits; //追加
        public List<string> categoryNames; //追加
        public CueInfo(string n, int inId, string com, short numLimits, List<string> categoryNames)
        {
            this.name = n;
            this.id = inId;
            this.comment = com;
            this.numLimits = numLimits; //追加
            this.categoryNames = categoryNames; //追加
        }
    } /* end of class */
    #endregion

キュー情報の読み込み

キューが保持するデータは「カテゴリ名」ではなく「カテゴリのインデックス」になります。カテゴリ名を表示するには、CriAtomExAcfDebug.GetCategoryInfoByIndexメソッドからカテゴリ名をインデックスから取得してリストアップします。

CriAtomProjinfo.cs
private void GetAcbInfoListCore(string searchPath, ref int acbIndex)
{

        /* キュー名リストの作成 */
        CriAtomEx.CueInfo[] cueInfoList = acb.GetCueInfoList();
        foreach(CriAtomEx.CueInfo cueInfo in cueInfoList){
            //CueInfo tmpCueInfo = new CueInfo(cueInfo.name, cueInfo.id, cueInfo.userData); //これを削除

            //以降を追加
            List<string> categoryNames = new List<string>();
            CriAtomExAcfDebug.CategoryInfo categoryInfo = new CriAtomExAcfDebug.CategoryInfo();

            foreach (var category in cueInfo.categories)
            {

                CriAtomExAcfDebug.GetCategoryInfoByIndex(category, out categoryInfo);
                categoryNames.Add(categoryInfo.name);
            }

            CueInfo tmpCueInfo = new CueInfo(cueInfo.name, cueInfo.id, cueInfo.userData, cueInfo.numLimits, categoryNames);

        //(略)

キュー情報の表示

GUICueListメソッド内の以下のBeginHorizontalエリアではCue NameやCue IDのらべる部分を表示しています。ここに新しく表示する情報を足します。

CriAtomWindow.cs
    //(略)
    private void GUICueList()
    {
        GUILayout.BeginHorizontal();
        {
            GUIStyle style = new GUIStyle(EditorStyles.miniButtonMid);
            style.alignment = TextAnchor.LowerLeft;
            if (GUILayout.Button("Cue Name", style)) {
                this.SortCueList(1);
            }
            //以下を追加
            if (GUILayout.Button("Category", style, GUILayout.Width(190))) {
                this.SortCueList(0);
            }
            if (GUILayout.Button("Cue Limit", style, GUILayout.Width(70))) {
                this.SortCueList(0);
            }
            //以上を追加
            if (GUILayout.Button("Cue ID", style, GUILayout.Width(70))) {
                this.SortCueList(0);
            }
        //(略)

つぎに、キューが持つ情報を一覧で表示します。このとき、

CriAtomWindow.cs
    //(略)
    private void GUICueList()
    {
    //(略)
        GUILayout.Label(string.Join(", " , acbInfo.cueInfoList[i].categoryNames), GUILayout.Width(220));
        //以下を追加               
        GUILayout.Label(acbInfo.cueInfoList[i].numLimits.ToString(), GUILayout.Width(60));
        GUILayout.Label(acbInfo.cueInfoList[i].id.ToString(), GUILayout.Width(40));
        //以上を追加
        EditorGUILayout.EndHorizontal();
    //(略)

これでAtomWindowにカテゴリとキューリミットが表示できるようになりました。

Atom3.png

その他のキュー情報も表示可能です。プロジェクトに応じて拡張しましょう。
表示が長くなる場合は、下部のSelectedCueエリアに表示させる方法もありです。

Atom Browser(製品版ADX2)の場合

製品版ADX2に同梱されているAtom Browserでは、CriAtomWindowPrefsにキューの情報も同時に保存していることがADX2 LEとの大きな違いになります。

ADX2 LEでは読み込み時に即Windowに表示する処理ですが、製品版ADX2ではインポート時にデータ取り出し→CriAtomWindowPrefsでパースしてSerialisedフィールドに保存→Atom Browerで表示処理、という流れになります。スクリプト部分の差分は以下のとおり。

キュー情報を保存するクラスにフィールドを追加する

CriAtomWindowPrefs.cs
    public class CueInfo : InfoBase {
        public bool isPublic;
        public short numLimits; //追加
        public List<string> categoryNames; //追加
        public CueInfo(string name, int id, string comment, bool isPublic, short numLimits, List<string> categoryNames) {
            this.name = name;
            this.id = id;
            this.comment = comment;
            this.isPublic = isPublic;
            this.numLimits = numLimits; //追加
            this.categoryNames = categoryNames; //追加
        }
    } /* end of class */

キュー情報の読み込み

CriAtomWindowPrefs.cs
        List<string> categoryNames = new List<string>();

        foreach (var category in cueInfo.categories)
        {
            CriAtomExAcf.CategoryInfo categoryInfo = new CriAtomExAcf.CategoryInfo();
            CriAtomExAcf.GetCategoryInfoByIndex(category, out categoryInfo);
            categoryNames.Add(categoryInfo.name);
        }

        acbInfo.cueInfoList.Add(new CueInfo(cueInfo.name, cueInfo.id, cueInfo.userData, Convert.ToBoolean(cueInfo.headerVisibility),cueInfo.numLimits, categoryNames));

キュー情報の表示

CriAtomWindow.cs
    private void GUICueList()
    {
            //(略)
            using (var cueListTitleScope = new EditorGUILayout.HorizontalScope()) {
                if (GUILayout.Button("Cue Name", toolBarButtonStyle)) {
                    if (isCueSheetAvailable) {
                        acbInfoList[selectedCueSheetId].SortCueInfo(CriAtomWindowInfo.CueSortType.Name);
                        this.selectedCueInfoIndex = 0;
                    }
                }

            //以下を追加
                if (GUILayout.Button("Category", toolBarButtonStyle, GUILayout.Width(200))) {
                    if (isCueSheetAvailable) {
                        acbInfoList[selectedCueSheetId].SortCueInfo(CriAtomWindowInfo.CueSortType.Id);
                        this.selectedCueInfoIndex = 0;
                    }
                }
                if (GUILayout.Button("Cue Limit", toolBarButtonStyle, GUILayout.Width(70))) {
                    if (isCueSheetAvailable) {
                        acbInfoList[selectedCueSheetId].SortCueInfo(CriAtomWindowInfo.CueSortType.Id);
                        this.selectedCueInfoIndex = 0;
                    }
                }
            //以上を追加


                if (GUILayout.Button("Cue ID", toolBarButtonStyle, GUILayout.Width(70))) {
                    if (isCueSheetAvailable) {
                        acbInfoList[selectedCueSheetId].SortCueInfo(CriAtomWindowInfo.CueSortType.Id);
                        this.selectedCueInfoIndex = 0;
                    }
                }
            }
            //(略)
                if (GUILayout.Button(acbInfo.cueInfoList[i].name, EditorStyles.label)) {
                    if (selectedCueInfoIndex != i) {
                        StopPreview();
                    }
                    this.selectedCueInfoIndex = i;
                }

        //以下を追加   
                GUILayout.Label(string.Join(", " , acbInfo.cueInfoList[i].categoryNames), GUILayout.Width(200));
                GUILayout.Label(cueLimtStr, GUILayout.Width(70));
        //以上を追加   

                GUILayout.Label(acbInfo.cueInfoList[i].id.ToString(), GUILayout.Width(40));
        //(略)

さらに拡張できること

今回は2カ所だけ拡張しましたが、Unity Editor上で確認できたら便利なことは他にもありそうです。たとえば、AISACやセレクタ等、スクリプトから設定を与える音についてAtom Windowで変更できるようになると良さそうです。

また、ADX2のサウンド再生はバックグラウンドで動作しているランタイム経由で鳴らすため、「CRI ADX2 LE」同梱のバージョンではUnity Editorがプレイ状態でないと再生テストができません。音の確認は主にAtom Craft側で行うワークフローになります。
最新の有償版「ADX2」については、この内部機構が改良されてAtom Browser内でプレビュー再生ができます。この延長線上で、「カテゴリの変更」など、多少のパラメータ変更はUnity Editor側でできるようになるワークフローを調査中です。
製品版ADX2は法人向けプロダクトですが、ADX2 LEの更新があった場合は、製品版で培った新機能が無償版へ降りてきます。そのあたりで、上記2つの課題にチャレンジする予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

色んなコントローラーでラジコン動かしたい_01

UnityとRaspberryPi使ってラジコンをハンコンで動かしてみたいメモ

検討中の方法

  1. 既製品を用いる方法(Web io Pi)
  2. webに事例が多い方法(Web io Pi)
  3. Unityと相性が良さそうな方法(WebSocket)

1.既製品を用いる方法(Web io Pi)

専用ソフトウエアをどこまでカスタマイズできるか?

世界最強のラズパイ・ラジコン基板 RC Berry
https://eleshop.jp/shop/g/gH7C411/
たのしい電子工作クラブ
http://elec-nuts.cocolog-nifty.com/blog/cat64607689/index.html
http://elec-nuts.cocolog-nifty.com/blog/rcb01-.html
SnapCrab_NoName_2020-5-26_14-54-31_No-00.png

2.webに事例が多い方法(Web io Pi)

WebioPiがちょっと古そうで不安(ver.が1未満だったり)
ブラウザGUI以外で操作したい。Unityから直接指示出せる?

WebIOPiを使ってRaspberryPiのGPIOを外出先から操作する
https://westgate-lab.hatenablog.com/entry/2020/01/09/231539
Raspberry PiでWebから操作できるラジコンクローラーを作る
http://kazuki-room.com/create_a_radio_control_that_can_be_operated_from_the_web_with_raspberry_pi/
SnapCrab_NoName_2020-5-26_14-55-34_No-00.png

3.Unityと相性が良さそうな方法(WebSocket)

Node.js入れたり環境構築から始める必要あり、先ずは手短に完成させたい。

UnityとRaspberry Pi間でWebSocket通信
https://tomosoft.jp/design/?p=4466
UnityでWebSocketを使用する
https://qiita.com/oishihiroaki/items/bb2977c72052f5dd5bd9

SnapCrab_NoName_2020-5-26_14-59-27_No-00.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【VRChat】 Udon開発する上での注意点【Unity】

はじめに

この記事はVRChatにおけるUdonおよびUdonSharp(U#)を使った際の備忘録です。
これからUdonを使い始める人のために書き連ねておきます。

この記事は2020/5/26現在のVRChatを前提に書いています。

Udonとパフォーマンスチューニング

Udonは実質、UnityのAPIをラップして呼び出しているだけにすぎません。
そのためUnityでプログラミングをするときと同じ様にパフォーマンスにもこだわる必要があります。

プロファイラを見よう

Unityには標準でProfilerという機能が備わっています。
1フレーム単位でどのような処理が実行され、それにどれくらいの時間がかかっているかをチェックすることができます。

OpenProfiler.png

Profiler.png

詳しい使い方はこちらを参考にしてください。

なお、プロファイラで動作を監視すること自体がかなりの負荷となります。
プロファイラを使っている間はfpsがガタ落ちしますが、しょうがないと割り切ってください。
(普通のUnity開発なら回避策があるのですが、VRChatだと仕様上どうしようもできないです)

Raw Hierarchyで調査

プロファイラの表示をRaw Hierarchyに切り替えて時間がかかっている順にソートすると何がボトルネックか調査することができます。
とくにこのモードだとUdon VMの中身の実行順も見ることが出来ます。

Profiler2.png

Udonの仕様上、どのスクリプトであるか名前はわからないのですが、メソッド呼び出しの様子からどのスクリプトかあたりをつけることはできます。

GCアロケートを避けよう

とくに負荷の原因となりやすいものはGC Allocと表示されているものです。
これはUnity上で、プログラムを実行するために必要なメモリを確保する動作を表しています。
GCアロケートと呼ぶ)

そしてこの確保したメモリですが、解放される瞬間にVRChatが一瞬フリーズしてしまいます。
GC(ガベージコレクタ)が実行される、と呼びます)

GCが実行される頻度は少ないほどfpsに与える影響は小さくなります。
逆に高頻度でGCが実行されると、体感できるレベル(ひどいと数十fps)で影響がでてきます。
そのためGCの実行をさける、つまりGC Allocの頻度を下げる工夫が必要となります。

stringは避けよう

C#の仕様上、string(文字列)は定義するだけでかなりのGC Allocを引き起こします。
そのためUpdate()で毎フレーム文字列を生成するなどしていると、パフォーマンスにかなりの悪影響を及ぼします。

極力stringは使わない、使うにしても必要なタイミングで必要なだけ生成する工夫が必要です。

U#は別コンポーネントのメソッド呼び出しがコスト

非常に便利なUdonSharpですが、見えないところでコストがかかります。
それはUdonSharpBehaviourから別のUdonSharpBehaviourなオブジェクトのメソッドを呼びだす時です。

たとえば、次のようなU#スクリプトがあったとして。

Runner
using UdonSharp;
using UnityEngine;

namespace DebugTest
{
    public class Runner : UdonSharpBehaviour
    {
        private Rigidbody _rigidbody;

        void Start()
        {
            _rigidbody = GetComponent<Rigidbody>();
        }

        public Vector3 GetCurrentVelocity()
        {
            return _rigidbody.velocity;
        }
    }
}
Observer
using UdonSharp;
using UnityEngine;

namespace DebugTest
{
    public class Observer : UdonSharpBehaviour
    {
        [SerializeField] private Runner _runner;

        private void Update()
        {
            // 取得するだけで何も使わない
            var velocity = _runner.GetCurrentVelocity();
        }
    }
}

これのObserver.cs側をUdon Assemblyにトランスパイルした結果をみるとこうなっています。

_update:

    PUSH, __0_const_intnl_SystemUInt32

    # {

    # var velocity = _runner.GetCurrentVelocity();
    PUSH, _runner
    PUSH, __0_const_intnl_SystemString
    EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEvent__SystemString__SystemVoid"
    PUSH, _runner
    PUSH, __1_const_intnl_SystemString
    PUSH, __0_intnl_SystemObject
    EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariable__SystemString__SystemObject"
    PUSH, __0_intnl_SystemObject
    PUSH, __0_intnl_UnityEngineVector3
    COPY
    PUSH, __0_intnl_UnityEngineVector3
    PUSH, __0_velocity_Vector3
    COPY
    PUSH, __0_intnl_returnTarget_UInt32 #Function epilogue
    COPY
    JUMP_INDIRECT, __0_intnl_returnTarget_UInt32

注目して欲しいところは、他のUdonSharpBehaviourへのメソッド呼び出しがSendCustomEventGetProgramVariableに変換されているところです。
(メソッドに引数を渡すとSetProgramVariableも追加される)

そしてこのSendCustomEventGetProgramVariableですが、なぜかGC Allocします

GCAlloc.png
(Udon内部実装の問題なのでおそらく回避不可)

ということで、U#を用いた場合、気軽にメソッド呼び出しを実行するとそれだけでGC Allocが発生します。
普通のUnity開発ではノーコストな操作が、U#ではコストがかかる点はかなり罠な気がします。

OnTriggerStay大暴走

UdonBehaviourにはOnTriggerStayが定義されています。
そのためVRC_Pickup + UdonBehaviourなオブジェクト」を一箇所に大量にまとめて配置するとOnTriggerStayが暴走します。

OnTriggerStay.png
(50個ほど重ねて配置した例)

数個程度なら問題ないですが、数十個レベルで一箇所にまとめるとfpsがガタ落ちするレベルで影響がでてきます。
アイテムを一箇所にまとめておいて擬似的な「無限湧き」を作るようなことはやめておきましょう。

Udon Synced Variables役に立たない問題

結論からいうとUdon Synced Variablesはパフォーマンスのために 「使わない」 が正解です。

UdonにはUdon Synced Variablesという機能があります。
こちらは指定したプリミティブな変数をネットワークをまたいで同期する機能です。
U#でいうところの[UdonSynced]

ですがこのUdon Synced Variables、挙動が結構ヤバイです。

  • Owner常時パラメータを相手に送信し続ける
  • 転送量が増えるとパケットロスして不着となる
  • スループットがかなり低い

同期するオブジェクト、変数の数が増えるとDeath Run Detected: dropped N eventsというエラーが大量に出てきます。
これが発生してしまうと、変数同期の成功率が極端に下がってしまいます。

image.png
(大量にパケロスしている様子)

そのため、Udon Synced Variablesで大量のデータを同期することはまったくオススメできません。

たとえば、オブジェクトの位置と姿勢(Vector3 + Quaternion)をUdon Synced Variablesで同期するのは止めたほうがいいでしょう。
私が試した場合ではオブジェクト数が20個を超えたあたりからパケロスが発生しました。
さらにVRChatの通信にかなりの負荷をかけるためか、Playerの挙動までもが不安定になりました。

ちなみに、この仕様ではほぼ使い物にならないのでフィードバック報告済みではあります。

補足: Udon Synced Variablesについての公式フォーラムでの報告

VRChatのフォーラムのこちらの投稿では次のように報告されています。

  • 2つのUdon Synced Variablesな文字列をもつUdon Behaviourをシーンにいくつか配置
  • 8個置いた程度ではパケロスはほぼゼロ
  • 16個置くとパケロスが発生する

とのことなので、Udon Synced Variablesを使う場合はオブジェクト数が少ない場合のみにした方が無難でしょう。

文字列にエンコードして同期する、は高コスト

また、とある場所で「オブジェクトの状態をstringにエンコードしてUdon Synced Variablesで同期する」という手法が提案されていました。

Udon Synced Variablesで配列が同期できないのを回避するために編み出された手法ですが、こちらかなりコストが高いです。

  • 大量のstringを生成することによるGC Alloc
  • 長い文字列を常時伝送するネットワークへの負荷

そのため本当にどうしようもないときの最終手段としとっておいて、常用はしないほうが無難でしょう。
(とはいえどこれしか方法が無いならば使わざるを得ないのがUdonのツライところなのですが…。)

位置同期

オブジェクトの位置を同期する方法ですが、次の2とおり(実質1とおり)があります。

  • A: Udon Synced Variablesで位置姿勢を送る
  • B: Udon BehaviourSynchronize Positionを使う

Aのパターンは前述の問題があるのでオススメできません。
ということで実質的にBの「Synchronize Position」一択になります。

このSynchronize Positionはちゃんと差分同期してくれるため、大量にオブジェクトがあってもネットワークへの負荷は小さいです。

Synced.png

Synchronize Positionの同期ズレ問題

Synchronize Positionは差分同期してくれるためネットワーク負荷は小さいのですが、大量にオブジェクトがある場合、後からワールドに参加した人には正しく位置と姿勢が同期されない場合があります。
こちらはワールドにいるプレイヤー数とオブジェクト数によりますが、「5人以上かつ20個くらいオブジェクトを動かした」あたりから発生してきます。

原因はハッキリとはしていないのですが、どうも次の複数の問題が絡んでいるっぽいです。

前者についてはバグ報告済みですが、後者についてはいまいち挙動がつかめていないため報告していません。

Synchronize Positionの同期ズレ対策

対処療法として、次の対策をいれましょう。
実際にモノレールワールドで実施している対策がこれです。

  • 触れていないPickupオブジェクトはすべてMasterが所有権をもつ

    • オブジェクトを持っている間は持っている人にOwnerを渡す
    • 手を離したらMasterに所有権を返すようにする
    • 若干安定するが、それでもまだ同期ズレは起きる
  • 強制的に位置を同期する仕組みをいれる

    1. Master側で同期対象のオブジェクトをすべて少しだけ位置と姿勢をズラす
    2. 数秒後に元の位置姿勢に戻す

image.png
(同期ズレの発生をゼロにはできないので、同期ズレが起きる前提で対策した方が早い)

かなりツラミがある仕組みですが、現状これくらいしか大量のオブジェクトを安定して同期する方法がありません。

まとめ

Udonつらいし、UdonSharpも結構ツライです。
それなりのUnity開発経験と、Unityでパフォーマンスチューニングをできるスキルが求められますね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む