- 投稿日:2020-05-26T22:13:44+09:00
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
セールは31日で終了ですよ。
- 投稿日:2020-05-26T15:33:29+09:00
ADX2 for UnityのCRI Atom Windowを拡張する
Atom Windowの機能追加
サウンドミドルウェアADX2のUnityプラグインには、「CRI Atom Window」というエディタ拡張が同梱されています。
ADX2のツールから出力したパックデータをUnityプロジェクトに読み込み、内容を確認したり、サウンドを鳴らすためのゲームオブジェクトを配置できる機能です。ADX2は、サウンドの鳴り方を同梱ツールである「Cri Atom Craft」で行い、Unity側ではゲーム実行中の再生リクエストやパラメータ渡しを作っていくワークフローを提供します。
そのためUnity Editor上でADX2のデータそのものを加工する機能はありません。Atom Windowの主な機能は「データのインポート」と「データ内容の確認」です。本投稿では、それらの機能を拡張する方法を紹介します。
今回は「キューのカテゴリと再生上限数(キューリミット)の追加表示」「ADX2データのインポート先を指定可能にする」の2つを紹介します。拡張後のCRI Atom Windowはこんな感じになります。アップデート時の注意
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.cspublic class CriAtomWindowPrefs : ScriptableObject { public string outputAssetsRoot = String.Empty; public string importFolderPath = String.Empty;//このフィールドを追加 //(略) }ここに保存先のパスを記録します。
Atom Windowに保存先パスを指定するInputFieldを足す
次に、Atom Window上で保存先パスの指定を行えるようにInputFieldを足します。
CriAtomWindow.csのGUIImportAssetsFromAtomCraftメソッド内、outputAssetsRootにデータを渡す処理の下に次のスクリプトを足してください。CriAtomWindow.csprivate 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」フォルダを用意して、そこを指定しています。
なお、この2行を足したことによってAtom Windowの縦の長さが変わってしまい、スクロールバーが出てしまいます。
次の項目を更新して、スクロールバーをなくします。CriAtomWindow.csprivate 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.csprivate 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.csprivate 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.csprivate 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にカテゴリとキューリミットが表示できるようになりました。
その他のキュー情報も表示可能です。プロジェクトに応じて拡張しましょう。
表示が長くなる場合は、下部のSelectedCueエリアに表示させる方法もありです。Atom Browser(製品版ADX2)の場合
製品版ADX2に同梱されているAtom Browserでは、CriAtomWindowPrefsにキューの情報も同時に保存していることがADX2 LEとの大きな違いになります。
ADX2 LEでは読み込み時に即Windowに表示する処理ですが、製品版ADX2ではインポート時にデータ取り出し→CriAtomWindowPrefsでパースしてSerialisedフィールドに保存→Atom Browerで表示処理、という流れになります。スクリプト部分の差分は以下のとおり。
キュー情報を保存するクラスにフィールドを追加する
CriAtomWindowPrefs.cspublic 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.csList<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.csprivate 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つの課題にチャレンジする予定です。
- 投稿日:2020-05-26T15:04:27+09:00
色んなコントローラーでラジコン動かしたい_01
UnityとRaspberryPi使ってラジコンをハンコンで動かしてみたいメモ
検討中の方法
- 既製品を用いる方法(Web io Pi)
- webに事例が多い方法(Web io Pi)
- 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
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/
3.Unityと相性が良さそうな方法(WebSocket)
Node.js入れたり環境構築から始める必要あり、先ずは手短に完成させたい。
UnityとRaspberry Pi間でWebSocket通信
https://tomosoft.jp/design/?p=4466
UnityでWebSocketを使用する
https://qiita.com/oishihiroaki/items/bb2977c72052f5dd5bd9
- 投稿日:2020-05-26T03:21:48+09:00
【VRChat】 Udon開発する上での注意点【Unity】
はじめに
この記事は
VRChat
におけるUdon
およびUdonSharp
(U#
)を使った際の備忘録です。
これからUdon
を使い始める人のために書き連ねておきます。この記事は2020/5/26現在のVRChatを前提に書いています。
Udonとパフォーマンスチューニング
Udon
は実質、UnityのAPIをラップして呼び出しているだけにすぎません。
そのためUnityでプログラミングをするときと同じ様にパフォーマンスにもこだわる必要があります。プロファイラを見よう
Unityには標準で
Profiler
という機能が備わっています。
1フレーム単位でどのような処理が実行され、それにどれくらいの時間がかかっているかをチェックすることができます。詳しい使い方はこちらを参考にしてください。
- Profiler ウィンドウ
- 【Unity】CPUプロファイラでパフォーマンスを改善する 前編
- 【Unity】CPUプロファイラでパフォーマンスを改善する 後編
- 【Unite 2017 Tokyo】最適化をする前に覚えておきたい技術
なお、プロファイラで動作を監視すること自体がかなりの負荷となります。
プロファイラを使っている間はfpsがガタ落ちしますが、しょうがないと割り切ってください。
(普通のUnity開発なら回避策があるのですが、VRChat
だと仕様上どうしようもできないです)Raw Hierarchyで調査
プロファイラの表示を
Raw Hierarchy
に切り替えて時間がかかっている順にソートすると何がボトルネックか調査することができます。
とくにこのモードだとUdon VM
の中身の実行順も見ることが出来ます。
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#
スクリプトがあったとして。Runnerusing UdonSharp; using UnityEngine; namespace DebugTest { public class Runner : UdonSharpBehaviour { private Rigidbody _rigidbody; void Start() { _rigidbody = GetComponent<Rigidbody>(); } public Vector3 GetCurrentVelocity() { return _rigidbody.velocity; } } }Observerusing 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
へのメソッド呼び出しがSendCustomEvent
とGetProgramVariable
に変換されているところです。
(メソッドに引数を渡すとSetProgramVariable
も追加される)そしてこの
SendCustomEvent
とGetProgramVariable
ですが、なぜかGC Alloc
します。ということで、
U#
を用いた場合、気軽にメソッド呼び出しを実行するとそれだけでGC Alloc
が発生します。
普通のUnity開発ではノーコストな操作が、U#
ではコストがかかる点はかなり罠な気がします。OnTriggerStay大暴走
UdonBehaviour
にはOnTriggerStay
が定義されています。
そのため「VRC_Pickup
+UdonBehaviour
なオブジェクト」を一箇所に大量にまとめて配置するとOnTriggerStay
が暴走します。数個程度なら問題ないですが、数十個レベルで一箇所にまとめると
fps
がガタ落ちするレベルで影響がでてきます。
アイテムを一箇所にまとめておいて擬似的な「無限湧き」を作るようなことはやめておきましょう。Udon Synced Variables役に立たない問題
結論からいうと
Udon Synced Variables
はパフォーマンスのために 「使わない」 が正解です。
Udon
にはUdon Synced Variables
という機能があります。
こちらは指定したプリミティブな変数をネットワークをまたいで同期する機能です。
(U#
でいうところの[UdonSynced]
)ですがこの
Udon Synced Variables
、挙動が結構ヤバイです。
Owner
は常時パラメータを相手に送信し続ける- 転送量が増えるとパケットロスして不着となる
- スループットがかなり低い
同期するオブジェクト、変数の数が増えると
Death Run Detected: dropped N events
というエラーが大量に出てきます。
これが発生してしまうと、変数同期の成功率が極端に下がってしまいます。そのため、
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 Behaviour
のSynchronize Position
を使うAのパターンは前述の問題があるのでオススメできません。
ということで実質的にBの「Synchronize Position
」一択になります。この
Synchronize Position
はちゃんと差分同期してくれるため、大量にオブジェクトがあってもネットワークへの負荷は小さいです。Synchronize Positionの同期ズレ問題
Synchronize Position
は差分同期してくれるためネットワーク負荷は小さいのですが、大量にオブジェクトがある場合、後からワールドに参加した人には正しく位置と姿勢が同期されない場合があります。
こちらはワールドにいるプレイヤー数とオブジェクト数によりますが、「5人以上かつ20個くらいオブジェクトを動かした」あたりから発生してきます。原因はハッキリとはしていないのですが、どうも次の複数の問題が絡んでいるっぽいです。
- オブジェクトの
Owner
が新規参加した人に正しく同期されず、Master
がOwner
にみえる問題Owner
がオブジェクトの位置同期が完了しない問題前者についてはバグ報告済みですが、後者についてはいまいち挙動がつかめていないため報告していません。
Synchronize Positionの同期ズレ対策
対処療法として、次の対策をいれましょう。
実際にモノレールワールドで実施している対策がこれです。
触れていないPickupオブジェクトはすべてMasterが所有権をもつ
- オブジェクトを持っている間は持っている人に
Owner
を渡す- 手を離したら
Master
に所有権を返すようにする- 若干安定するが、それでもまだ同期ズレは起きる
強制的に位置を同期する仕組みをいれる
Master
側で同期対象のオブジェクトをすべて少しだけ位置と姿勢をズラす- 数秒後に元の位置姿勢に戻す
(同期ズレの発生をゼロにはできないので、同期ズレが起きる前提で対策した方が早い)かなりツラミがある仕組みですが、現状これくらいしか大量のオブジェクトを安定して同期する方法がありません。
まとめ
Udon
つらいし、UdonSharp
も結構ツライです。
それなりのUnity開発経験と、Unityでパフォーマンスチューニングをできるスキルが求められますね。