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

【Unity】1週間ゲームジャムに『妖言』で参加した話

今回はUnity1Week 1週間ゲームジャム「回」に参加しました。 その際に新たな試みを取り入れてみたのですが、今回はそれを中心にお伝えしたいと思います。 記事本文はこちら
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

forとforeach、どちらのループを使用するのがいいのか考えてみる(現在List編は書きかけです…)

正直プログラム経験がまだまだ浅いから間違いはあるかも。ご指摘下さればありがたいです。 ※すみません。訳あってList編の途中のまま投稿しています。ご了承ください。月曜日にはまた時間が取れそうなので、月曜日には書き終える予定です。 きっかけ https://akinow.livedoor.blog/archives/52474053.html そもそものきっかけは、上記の記事でforeachの使用を控えるような内容を見たからだった。 (後々分かったが、これはUnity5.5で解決済みだった) 軽量化に力を入れていきたい身としては、重いとかメモリの効率が~なんて話は聞き捨てならない。 たとえ解決済みだとしても、気になる。一度気になりだしたら止まらない。 ・・・まあ、結果。ハマりましたよ。 見事にループ処理の沼にハマったので、同じような疑問を感じた人と、未来の自分用のメモとして、結局何がどういいのかまとめてみる。 コンパイル結果は、sharplab様にお世話になりました。 内部分のコードなんかはactual implementationを参照させていただきました。 配列編 コンパイル用に以下のようなコードを作成したので、コンパイルしてみる。 sumに配列の中身を足していく処理だ。 コンパイルしたいだけなので、sumがリセットされてないとか、配列に何も入っていないとか。 その辺は見逃してほしい・・・。 int[] TestArray = new int[100]; int sum = 0; //forの処理 for (int i = 0; i < TestArray.Length; i++) { sum += TestArray[i]; } //foreachの処理 foreach (int i in TestArray) { sum += i; } ↓ コンパイル int[] TestArray = new int[100]; int sum = 0; //forの処理 int num = 0; while (num < TestArray.Length) { sum += array[num]; num++; } //foreachの処理 int[] array2 = TestArray; int num2 = 0; while (num2 < array2.Length) { int num3 = array2[num2]; sum += num3; num2++; } 上記のような結果が出力された。 ・・・おんなじやん! 配列におけるループの内部処理は、単純にint型の変数を一つ用意して、ループ毎に足し、配列の大きさより大きくなったら、whileから抜けるだけのようだ。 まあ、強いて言えばここで配列が参照されているぐらいか。 int[] array2 = TestArray; ちなみに配列は参照型なので、コピーはされない。上記のように書いても、渡されるのは配列のアドレス情報のみ。この程度なら問題はない。 もしも知らない!気になった!って方はその辺は論点がずれてしまうので、以下サイト様がわかりやすいと思う。 https://ufcpp.net/study/csharp/oo_reference.html#type-category さて、結果としては、やってることが同じならforを使うメリットは無い。 そもそも、foreachは糖衣構文( 要は読み書きのしやすさを重要視されている処理 )だから、優先してforeachを使いたい。 List編 Listを回す際に、どちらを使用すべきか。 配列に対してのコンパイル結果は、分かりやすく非常に簡単な結果であったが、ListはEnumeratorというものが関わってくるため、少々厄介だったりする。 まずはコンパイルしてみる。内容はさっきと同じ。 ( ListにはLINQのCount()関数と、List内のcount変数があるが、変数でやる場合の方が個人的に多いし、こちらの方が処理が速いためこちらを使用している。他にも理由はあるけど後述する。) List<int> TestList = new List<int>(); int sum = 0; //forの処理 for (int i = 0; i < TestList.Count; i++) { sum += TestList[i]; } //foreachの処理 foreach (int i in TestList) { sum += i; } ↓ コンパイル List<int> list = new List<int>(); int sum = 0; //forの処理 int num = 0; while (num < list.Count) { sum += list[num]; num++; } //foreachの処理 List<int>.Enumerator enumerator = list.GetEnumerator(); try { while (enumerator.MoveNext()) { int current = enumerator.Current; sum += current; } } finally { ((IDisposable)enumerator).Dispose(); } ちょっとごちゃごちゃしてきたので、混乱するかもしれないが順番に紐解いていこう。 まず、forの部分は先ほどの配列とやり方は変わっていないので、言わなくても分かると思う。 問題はforeachの方だろう。 まず、Listは幾つかのインターフェースを継承しているのだが、以下の行でその中の一つであるIEnumerable内の唯一のメソッドであるGetEnumerator()というメソッドにアクセスしている。 List<int>.Enumerator enumerator = list.GetEnumerator(); GetEnumerator()が一体何をするメソッドなのか。 では、GetEnumerator()とはなんなのか。 簡単にまとめるとGetEnumerator()というメソッドは、List内にあるEnumeratorという構造体に自身の情報を設定して返すだけのメソッドだ。 もう少し詳しく言うならば、GetEnumerator()を呼び出した際に、Enumeratorのコンストラクタにアクセスし、渡されたList、初期index、渡された時点での_version、currentを設定し、設定したEnumeratorを返すメソッドだ。 IEnumerator内のメソッド 次に以下の処理に注目してもらいたい。 while (enumerator.MoveNext()) { int current = enumerator.Current; sum += current; } 次に疑問に思うのは、MoveNext()とは何者なのかという点だろう。 MoveNext()はEnumeratorが継承するインターフェースの一つであるIEnumerator内のbool型のメソッドだ。 こいつは簡単に言うと、List内の要素数が超えたらループを抜けて、要素を超えていない場合はループを続けるという処理が内部的に行われている。 もう少し深掘りしてみよう。 MoveNext()を覗いてみると、以下のようになっている。 public bool MoveNext() { List<T> localList = list; if (version == localList._version && ((uint)index < (uint)localList._size)) { current = localList._items[index]; index++; return true; } return MoveNextRare(); } private bool MoveNextRare() { if (version != list._version) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); } index = list._size + 1; current = default(T); return false; } これだけでは、訳が分からないかもしれないので、順番に見ていこう。 まず、以下の部分で、Listのバージョンが合っているか。現在探索している要素がList内の要素数を超えていないか。を調べている。 もし、条件が合っていた場合、current変数に現在の要素を設定し、探索位置をずらし、ループは続くのでtrueを返している。 if (version == localList._version && ((uint)index < (uint)localList._size)) { current = localList._items[index]; index++; return true; } では、条件が満たされなかった場合はどうなるのか。 その場合は、MoveNextRare()に入る。 MoveNextRare()の処理は、まずversionが合っているか確認し、合っていなかった場合はエラーを吐く。 もしあっていた場合は、currentを初期化してfalseを返すことでループを終了します。 private bool MoveNextRare() { if (version != list._version) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); } index = list._size + 1; current = default(T); return false; } これがだいたいのforeachのList探索の概要です。 では、結局どっちをつかうのがいいのか? 今一度、コンパイル結果を見てみよう。 List<int> list = new List<int>(); int sum = 0; //forの処理 int num = 0; while (num < list.Count) { sum += list[num]; num++; } //foreachの処理 List<int>.Enumerator enumerator = list.GetEnumerator(); try { while (enumerator.MoveNext()) { int current = enumerator.Current; sum += current; } } finally { ((IDisposable)enumerator).Dispose(); }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

forとforeach、どちらのループを使用するのがいいのか考えてみた!

正直プログラム経験がまだまだ浅いから間違いはあるかも。ご指摘下さればありがたいです。 Qiitaに不慣れなため、見にくかったらごめんなさい! きっかけ https://akinow.livedoor.blog/archives/52474053.html そもそものきっかけは、上記の記事でforeachの使用を控えるような内容を見たからだった。 (後々分かったが、これはUnity5.5で解決済みだった) 軽量化に力を入れていきたい身としては、重いとかメモリの効率が~なんて話は聞き捨てならない。 たとえ解決済みだとしても、気になる。一度気になりだしたら止まらない。 ・・・まあ、結果。ハマりましたよ。 見事にループ処理の沼にハマったので、同じような疑問を感じた人と、未来の自分用のメモとして、結局何がどういいのかまとめてみる。 コンパイル結果は、sharplabにお世話になりました。 内部分のコードなんかはactual implementationを参照させていただきました。 配列編 コンパイル用に以下のようなコードを作成したので、コンパイルしてみる。 sumに配列の中身を足していく処理だ。 コンパイルしたいだけなので、sumがリセットされてないとか、配列に何も入っていないとか。 その辺は見逃してほしい・・・。 int[] TestArray = new int[100]; int sum = 0; //forの処理 for (int i = 0; i < TestArray.Length; i++) { sum += TestArray[i]; } //foreachの処理 foreach (int i in TestArray) { sum += i; } ↓ コンパイル int[] TestArray = new int[100]; int sum = 0; //forの処理 int num = 0; while (num < TestArray.Length) { sum += array[num]; num++; } //foreachの処理 int[] array2 = TestArray; int num2 = 0; while (num2 < array2.Length) { int num3 = array2[num2]; sum += num3; num2++; } 上記のような結果が出力された。 ・・・おんなじやん! 配列におけるループの内部処理は、単純にint型の変数を一つ用意して、ループ毎に足し、配列の大きさより大きくなったら、whileから抜けるだけのようだ。 まあ、強いて言えばここで配列が参照されているぐらいか。 int[] array2 = TestArray; ちなみに配列は参照型なので、コピーはされない。上記のように書いても、渡されるのは配列のアドレス情報のみ。この程度なら問題はない。 もしも知らない!気になった!って方はその辺は論点がずれてしまうので、以下サイト様がわかりやすいと思う。 https://ufcpp.net/study/csharp/oo_reference.html#type-category さて、結果としては、やってることが同じならforを使うメリットは無い。 そもそも、foreachは糖衣構文( 要は読み書きのしやすさを重要視されている処理 )だから、優先してforeachを使いたい。 List編 Listを回す際に、どちらを使用すべきか。 配列に対してのコンパイル結果は、分かりやすく非常に簡単な結果であったが、ListはEnumeratorというものが関わってくるため、少々厄介だったりする。 まずはコンパイルしてみる。内容はさっきと同じ。 List<int> TestList = new List<int>(); int sum = 0; //forの処理 for (int i = 0; i < TestList.Count; i++) { sum += TestList[i]; } //foreachの処理 foreach (int i in TestList) { sum += i; } ↓ コンパイル List<int> list = new List<int>(); int sum = 0; //forの処理 int num = 0; while (num < list.Count) { sum += list[num]; num++; } //foreachの処理 List<int>.Enumerator enumerator = list.GetEnumerator(); try { while (enumerator.MoveNext()) { int current = enumerator.Current; sum += current; } } finally { ((IDisposable)enumerator).Dispose(); } ちょっとごちゃごちゃしてきたので、混乱するかもしれないが順番に紐解いていこう。 まず、forの部分は先ほどの配列とやり方は変わっていないので、言わなくても分かると思う。 問題はforeachの方だろう。 まず、Listは幾つかのインターフェースを継承しているのだが、以下の行でその中の一つであるIEnumerable内の唯一のメソッドであるGetEnumerator()というメソッドにアクセスしている。 List<int>.Enumerator enumerator = list.GetEnumerator(); GetEnumerator()が一体何をするメソッドなのか。 では、GetEnumerator()とはなんなのか。 簡単にまとめるとGetEnumerator()というメソッドは、List内にあるEnumeratorという構造体に自身の情報を設定して返すだけのメソッドだ。 もう少し詳しく言うならば、GetEnumerator()を呼び出した際に、Enumeratorのコンストラクタにアクセスし、渡されたList、初期index、渡された時点での_version、currentを設定し、設定したEnumeratorを返すメソッドだ。 IEnumerator内のメソッド 次に以下の処理に注目してもらいたい。 while (enumerator.MoveNext()) { int current = enumerator.Current; sum += current; } 次に疑問に思うのは、MoveNext()とは何者なのかという点だろう。 MoveNext()はEnumeratorが継承するインターフェースの一つであるIEnumerator内のbool型のメソッドだ。 こいつは簡単に言うと、List内の要素数が超えたらループを抜けて、要素を超えていない場合はループを続けるという処理が内部的に行われている。 もう少し深掘りしてみよう。 MoveNext()を覗いてみると、以下のようになっている。 public bool MoveNext() { List<T> localList = list; if (version == localList._version && ((uint)index < (uint)localList._size)) { current = localList._items[index]; index++; return true; } return MoveNextRare(); } private bool MoveNextRare() { if (version != list._version) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); } index = list._size + 1; current = default(T); return false; } これだけでは、訳が分からないかもしれないので、順番に見ていこう。 まず、以下の部分で、Listのバージョンが合っているか。現在探索している要素がList内の要素数を超えていないか。を調べている。 もし、条件が合っていた場合、current変数に現在の要素を設定し、探索位置をずらし、ループは続くのでtrueを返している。 if (version == localList._version && ((uint)index < (uint)localList._size)) { current = localList._items[index]; index++; return true; } では、条件が満たされなかった場合はどうなるのか。 その場合は、MoveNextRare()に入る。 MoveNextRare()の処理は、まずversionが合っているか確認し、合っていなかった場合はエラーを吐く。 もしあっていた場合は、currentを初期化してfalseを返すことでループを終了します。 private bool MoveNextRare() { if (version != list._version) { ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion); } index = list._size + 1; current = default(T); return false; } これがだいたいのforeachのList探索の概要です。 では、結局どっちをつかうのがいいのか? 以下のサイト様を見た所、速度面ではforの方がforeachよりも早い。 https://takap-tech.com/entry/2020/10/20/234610 メモリ面から見ても、GetEnumerator()でEnumeratorを生成している分、forの方がいいように感じられるが、私個人の見解としては、そこまで気にするほどではないように感じた。 よっぽど、速度を重要視する場合や、メモリ効率を考えたい!といった場合でない限りは、糖衣構文であるforeachの使用を避ける程ではないように思う。 まあ、この程度であれば好みの範疇かな? 結果 2021/04/21修正:配列の結果がforになってました!!!ごめんんささい!! 配列:foreach List:基本的にforeachでいい。よっぽど気にするようであればfor 以上! ご指摘、ご意見おまちしておりまs・・・ めtyくちゃ参考にさせていただきましたサイト様 https://qiita.com/NCT48/items/d8394d587e9fee969ca9
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エディタ拡張でUdonBehaviourを編集しよう

はじめに UdonがVRChatに現れてから早いもので1年が経過しました。 近頃はUdonを書く人も大分増えて、日本界隈でも市民権を得てきているなぁ、と思う今日この頃です。 本格的にUdonを触るようになって、エディタ拡張で自動化したいシチュエーションが増えてきたのですが、VRCTriggerと違ってUdonBehaviourはserializedObjectからUdonの変数に触る事は出来ないようです。 そんなわけでエディタ拡張からUdonの変数を設定する方法を調べてみました。 Udonの構造 UdonはUdonAssemblyProgramAssetを継承したクラス(Udonコンパイラ)によってコンパイルされます。 その際、シリアライズされた変数は型と変数名を合わせたSymbolという形で保存されます。 このSymbolに値は含まれておらず、値はPublicVariablesという、変数名と紐づけされた形でUdonBehaviourに保存されます。 Symbolの取得 SymbolはUdonコンパイラによって生成されるSerializedUdonProgramAssetが持っています。UdonBehaviourからは下記の経路でSymbolにアクセスできます。 using VRC.Udon; using VRC.Udon.Common.Interfaces; public class SymbolTable { public UdonBehaviour udonBehaviour; public IUdonSymbolTable GetSymbolTable() { IUdonSymbolTable symbolTable = udonBehaviour.programSource.SerializedProgramAsset.RetrieveProgram().SymbolTable; return symbolTable; } } SymbolTableから下記関数を使用してSymbolの情報を取得できます。 ImmutableArray<string> GetExportedSymbols() 変数名の配列を返す。 bool HasExportedSymbole(string publicVariableSymbol) 指定した変数名がテーブルに含まれるならTrueを返す。 Type GetSymbolType(string exportedSymbol) 変数名からSymbolの型を返す。 PublicVariablesの編集 PublicVariablesはUdonBehaviourのメンバ変数から取得できます。 IUdonVariableTable publicVariables = udonBehaviour.publicVariables; PublicVariablesが持つ下記関数から変数の値にアクセスできます。 bool TryGetVariableValue(string exportedSymbol, out object variableValue) 変数名からSymbolの値をobject型で取得する。失敗したらFalseを返す。 bool TrySetVariableValue(string exportedSymbol, object variableValue) 変数名からSymbolの値をSetする。失敗したらFalseを返す。 サンプルスクリプト Scene内の全てのAudioSourceを取得してUdonBehaviourに設定するエディタ拡張です。 using UnityEngine; using UnityEditor; using VRC.Udon; using VRC.Udon.Common.Interfaces; public class UdonAudioSetup : MonoBehaviour { public UdonBehaviour udonBehaviour; public string symbolName; } [CustomEditor(typeof(UdonAudioSetup))] public class UdonAudioSetupEditor : Editor { UdonAudioSetup udonAudioSetup; private void OnEnable() { udonAudioSetup = (UdonAudioSetup)target; } public override void OnInspectorGUI() { base.OnInspectorGUI(); EditorGUILayout.Space(); EditorGUILayout.Space(); // Symbolを取得出来なければボタンを無効化 using (new EditorGUI.DisabledScope(!IsSetupReady(udonAudioSetup))) { if (GUILayout.Button("Setup")) { SetupUdonBehaviour(udonAudioSetup.udonBehaviour, udonAudioSetup.symbolName); } } } private void SetupUdonBehaviour(UdonBehaviour udonBehaviour, string exportedSymbol) { // PublicVariables取得 IUdonVariableTable publicVariables = udonBehaviour.publicVariables; var audioSources = GameObject.FindObjectsOfType<AudioSource>(); // Undo用意 Undo.RecordObject(udonBehaviour, "Modify Public Variable"); // 値をPublicVariablesに設定 if (!publicVariables.TrySetVariableValue(exportedSymbol, audioSources)) { Debug.Log("Error! Failed Setting Public Variables."); return; } Debug.Log("Setup Completed."); } private bool IsSetupReady(UdonAudioSetup udonAudioSetup) { // Symbolテーブル取得 IUdonSymbolTable symbolTable = udonAudioSetup.udonBehaviour?.programSource?.SerializedProgramAsset?.RetrieveProgram()?.SymbolTable; if (symbolTable == null) return false; // Symbolが存在しなければFalse if (!symbolTable.HasExportedSymbol(udonAudioSetup.symbolName)) return false; // Symbolの型がAudioSource[]でなければFalse if (symbolTable.GetSymbolType(udonAudioSetup.symbolName) != typeof(AudioSource[])) return false; return true; } } 使用バージョン VRCSDK3-WORLD-2021.03.22.18.27_Public UdonSharp_v0.19.8
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む