Unity初心者の俺が調べたことをメモするスレ
ちなスペックは コード作成は生成AI頼り・基礎的なコンポーネントの使い方すら理解してない・C#の経験皆無 こういうレベル >>48 Dictionaryは個別の値へのアクセスは早いけど高性能な分メモリ使用量は多いよ 配列やリストはメモリ使用量は少なめでIndexでのアクセスは早いけど、個別の値の検索や削除が要素数に比例して遅くなるから多数のアイテムを管理するシステムには不向きな印象がある 明日は久しぶりにコレクションについてまとめてみるか 今日は抽象クラスの整理はひとまず終わったので次に作るUIの設計を考えた クラス間の結合を弱めるにはインターフェースやZenject(外部ライブラリ)が有効だそうだが、導入には一手間かかりそうだなという印象 インターフェースはUnityの標準機能じゃインスペクターから設定できない(オーディンインスペクターという有料アセットや外部ライブラリを利用すれば一応可能)のが残念 それとインスペクターで参照を設定するのだと結局ゲームオブジェクト(MonoBehaviour)同士の結合は緩められてない気がする 「クラスAに機能を追加・修正・削除したから、クラスBの該当部分も直して~」っていう作業から抜けたいなあ 因みに今のパソコンでそのディクショナリのはやさとかメモリとかどれだけ影響あるのでしょうか? 最後の文章見ると個人開発じゃなくてグループ? このメモは一体、、、 個人開発だよ 「直して〜」の「〜」は呼びかけじゃなくて以下ループとかそういうニュアンスのつもりだった 大量のデータを扱うコレクション(配列とかリストとかディクショナリ)の場合はどのコレクションを選択するかで処理速度は顕著な差が出るね 前に実験したことあるけど、1万個の所持アイテム用インスタンスを格納した@List<自作クラス>とADictionary<int,自作クラス>で一番最初に入手したアイテム(ListではIndex0、Dictionaryではint型キーを連番になるように制御して格納しているのでこれもキー0になる)を削除する処理を行った場合 @Listだと500msぐらいかかる一方で、ADictionaryだと0.1msで終わる これはListでは内部的には配列に特殊な処理を加えて自由に挿入・削除のできるコレクションに仕立てているから、削除関数であるRemoveAt()を実行すると削除した要素Indexの後ろに並んでいる要素たちを全部1つずつコピーして前に詰める作業が内部的に行われると全体の要素数が増えるにしたがってクッソ重くなる 一方でDictionaryは内部ではハッシュテーブルを利用していて、与えられたキーをハッシュに変換する作業がある代わりに基本的に要求された要素だけを参照しに行くから、全体の要素数が増えても処理速度に対して影響は出ない メモリ使用量についてはモバイル端末想定じゃなきゃまあ誤差だとは思うけど可能な限りは軽くしたいね コレクションについてまとめようと思ったけど別のことして忘れてた とりあえず今日はメニュー画面の作成を進めた 今日から作成を始めたメインメニューはクラス間のシリアライズによる参照は使わずに、イベントとリスナー登録で情報のやり取りをしようと考えている 一般的にイベントでやり取りをする方法は、クラス間でお互いの内部実装に無関心で済むため(イベントの発火がされなければ待機しているだけで、引数を受け取ってどう使うかも自由)、疎結合に分類されるらしい まあシリアライズの代わりにデリゲートへの関数のキャッシュとリスナーの登録をコードで行う必要があって、更に複数のリスナーが存在する場合は実行順序の制御も別所で必要になるので、インスペクターからシリアライズで参照設定をしてグチャっと処理コードを書いた方が短期的には楽に見えるのはナイショ 基本的に実装したい処理が思い浮かんだ時に必要そうな処理を基礎からその都度調べているだけよ 地道にUnityやっていけば必ず到達できるレベルでしかないからガンバレ 本日の作業もメニュー作り 静的イベントを利用してクラス間のやり取りをするうちに気になったのが、イベントとゲームオブジェクトのライフサイクルとの関係性 メインメニュー → 「装備」ボタンを押すと静的イベントを発火 → リッスンした装備メニューのマネージャークラスが装備メニューをSetActive といった流れを実装したかったが、テストプレイすると「装備」ボタンを押しても装備メニューが起動しない 原因は、装備メニューのマネージャークラスがアタッチされたゲームオブジェクトが無効化されていたためっぽい 無効化されたゲームオブジェクトの関数を外部からコールすることは可能だが、無効化中にはイベントのリスナーとしてコールバックを行うことはできないようだ 確かにゲームオブジェクトにアタッチされたクラスはUpdateなどのマジックメソッドが呼び出されないが、厳密にはこれらもランタイムから呼び出されている訳で、外部からコールされている状態に近いように思える イベントのリスナーとしてのコールバックもEventHandler?が呼び出しているらしいので、「ゲームオブジェクト」のライフサイクルにはこれらの仕様を変更する(というか、制限された仕様のインスタンスをUnityが提供している?)ようだ まあ面倒な理屈はともかく、イベントでやり取りするクラスは発火前にゲームオブジェクトを有効化しておくか、MonoBehaviourを利用しないクラスとして設計しましょう、というのが今日得た結論となる それセットアクティブじゃなくて Enableでは? いやイベントのリスナーになっているクラス(コンポーネント)がアタッチされているゲームオブジェクトが無効化されているとコールバックが行われないっぽのよね コンポーネントじゃなくてゲームオブジェクトだからSetActiveの話 l_i_t_e(邪魔という方は左記をNGお願いします) 更にご家族に教えて加えて¥4000×人数をGETできます! https://i.imgur.com/l61BDyt.jpg 普通なら削除依頼出しとけよって定型文貼られるようなスレだけど 技術者多い板の性質と真面目な内容が組み合わさって技術メモブログのように存在を許されている 絵描き板とかである個スレみたいなもんでしょ 過疎板にはよくある アウトプットも兼ねて覚えたこと書き殴りたくなる機会がたまにあるんだけど、Unityの制作面中心の雑談スレが見当たらなかったからノリで立てちゃったのよねすまん 代わりに毎日何かしらは作業する・調べるように努力してます 今日気になったこと ・ジェネリックの扱い方 ジェネリックと抽象クラスを利用してUI関係の整理を続けていたが、独自セレクタブルたちが引数の型の異なるRefresh関数(描画情報を更新するために管理クラス側からコールされるpublicな関数)をそれぞれ持っており、これをどうやって統一するか悩む 統一すること自体は抽象クラスにRefresh関数自体をジェネリック<T>で抽象メソッドとして宣言すれば簡単にできるのだが、そうすると同時セレクタブルたちの型が抽象クラスとしても異なってしまうため、管理クラス側から独自セレクタブルたちの配列を扱う時にその型(内部実装)を知る必要性が出てきてしまうように思える インターフェースでよくないか?とも思ったのだが、インターフェースはUnityの標準機能ではシリアライズ化できないし、キャストしたりGetComponentで具体的な型を指定しないと多分Transform等にもアクセスができないしどうしたものか 抽象クラスの基底に更に抽象クラスを宣言すれば何とかなるのかな…明日以降検証してみる ちなみにコレクションの復習はまだ出来ていない というかUIのクラスとレイアウト周りの整理が中心で、検索システムとか内部面は一段落したので暫くコード書くときにコレクションに触れてない アイテム合成とか敵からのドロップテーブル等を実装する時にまた考えるかな(10月以降) 今日の作業 ・独自の派生セレクタブルの処理を抽象化するためにRefresh関数の引数をint型に統一することに決定する。int型以外を引数とするクラスはコード全体を見直す必要が出てきたので数か月前の設計のガバさのツケが回ってきた形。 ・MonoBehaviourを継承しないクラスに[Serializable]を付けて、このクラスをフィールドとして保有するMonoBehaviour継承クラスのインスペクターから値を弄れるようにしてみた 基本的な使い方だけどスクリプタブルオブジェクト以外で[Serializable]を利用してこなかったのでちょっと感動 ・ノーコード系ゲーム制作アセットの戦闘システムに自作の拡張機能を追加した。攻撃時に攻撃者と被害者をそれぞれ独自戦闘システムインターフェース型でGetComponentしてダメージ計算など色々な処理をスクリプトから行う 有料アセットのテンプレートは便利だけど、個人的にはノーコード系のノードやリスト形式の命令処理は何となく苦手 VisualStudioで素直にC#書いた方が楽だし処理も追いやすい気がしてならない ・「今日の疑問」について調べている時にDictionaryのTryGetValueを使えばキーの存在確認と値のチェックを両方できることを知った というか前に調べて覚えたつもりだったけど忘れていた 嫌な予感がして自分のコード見てみたらContansKeyとDictionary[]を両方使って二重にDictionaryを走査している箇所が案の定見つかった out系の使い方がよく分からなくて昔は見かけてもスルーしていた記憶がある(RayCastとかTryGetComponentでもoutはしばしば出てくる。何なら3D空間上の計算をするゲームでは頻繁に見かけそう) ちゃんとやり方覚えなきゃなあと思いました 今日の疑問 ・GetComponentはしばしば重いと言われるが実際問題どの程度使うとボトルネックになるのだろうか?1フレームに30回程度じゃプロファイラーを見る限りでは全く問題にならない程度の負担しかない まあキャッシュしておけばいいものを毎度取得しに行くコードの存在自体が不適切な印象は否めないが ゲットコンポーネントは重くないでしょ それは公式でも書いてると思うけど Updateでやっちやあかんって 「Unityでよくある失敗」はコードを書く習慣がある層ならUnity初学者でも回避するよう内容が多いな GetComponentとFindを周期処理のなかに書くなんぞ最たるもんだな >>63-64 必要ならキャッシュしておいた方がいい、との記述はあるものの https://learn.microsoft.com/en-us/windows/mixed-reality/develop/unity/performance-recommendations-for-unity?tabs=openxr 色々な人の検証結果を見る限りはGetComponentを1フレーム内で数万回繰り返しても、処理時間はそれ自体ではぶっちゃけ殆ど差が出ないみたいだね そう考えると実はUpdate内で毎度呼び出しても現実的なボトルネックにはならないんだろうね ある意味「GetComponentは重いぞ神話」が独り歩きしている状況かもしれない 自分は基本的にその場限りで利用する場合以外はキャッシュするけども 代わってGameObject.Find系は本当に看過できないほど重いようだ ゲームオブジェクト全体を走査する系の処理は重いのは感覚的にもよく分かる 同じくゲームオブジェクト全体にメッセージを送信するSendMassageも滅茶苦茶重いらしくてMicrosoftのDocには 「SendMessage() と BroadcastMessage() は、どんな犠牲を払っても排除されるべきです。これらの関数は、直接関数呼び出しよりも約1000倍遅くなる可能性があります。」(google翻訳) って書いてあって草 素直にUnityEventsかC#Eventを使うのが安心だな おそらくはまだCPUがショボい頃の事が今でも尾を引いてらのかもね >>67 あと可能性があるとしたら、TransformへのアクセスはUnityのバージョンアップで最適化がされたことがあるそうで、GetComponentもそうなのかもしれん(調べてない) ・今日の作業 今日の作業は昨日と変わらず、重い処理関係以外で特に何も調べたりはしなかった 引き続き設計思想の話としては、インスペクターから値・参照を設定・確認できるのは便利である一方で、Unityに慣れるにつれてインスペクターから設定しなくてはならないのはコードとインスペクターの双方を行き来する必要があって面倒だとも感じてきた UnityにはEventTriggerなど便利なコンポーネントが存在しているが、一部は自分でスクリプトを組めば代替できるものもある GameObject.Find系が推奨されないもう一つの理由として「制作途中の仕様変更に弱い」という指摘があるが、これにはコードとインスペクターだけではなくヒエラルキーも動作確認の際にチェック対象になってしまう煩雑さを回避したいという願いが暗に含まれていそうだ 今日は自作のインベントリシステムに並び替え機能の実装を始めた List.Sortで自作クラスをソートするにはIComparableインターフェイスを継承してCompareTo関数を実装するか、又はラムダ式で直接ソート順を指定することでも可能となる ラムダ式は匿名関数を作成するもので、内部的には最初に実行される時のみnewされて関数が生成されてそれ以降はコンパイラが自動的にそれを使い回してくれるそうなのでアロケーション面でも優しい Unityでお世話になる時は大体ソート関係な気がする Linp使えるからSQL的なSelect,Whereなんかも使ってもいいね すまんLinqだスマホだから打ち間違えた そしてpもqもパッと見見分けつかんからそのまま送信してしまった >>70 LINQは便利そうな機能が一通り揃ってるのね ただ前に使ってみた時に(どの関数かは忘れた)素直にForやForeachでやるのと比べて凄い量のアロケーションが出てたから使うのやめちゃってたわ インベントリ内アイテムの並び替えみたいな、そう頻繁に行われない処理にはLINQの使用を再検討してみようかな 直接並び替え用の式を書くと後で見た時に大抵「何だっけこれ…」ってなるから…(まずコード上の注釈を再読む所から始まる) 今日も並び替え機能の実装を進めた そういえばクラス間の情報のやり取りで静的イベント(public static event Action<>〜)はよく使っているけど、Funcはまだどこにも利用してないと思った がChatGPTによるとFuncはリスナーが複数いる場合に戻り値にどのリスナーからの値を使用すべきが不明瞭になるから使っちゃいけないらしい よく意味が分からなかったので明日以降検証したい そもそもこの疑問が浮かんだのが作業が終了してからなのでVisualStudio上でコード自体が適切にコンパイルできるのかすら試してない 適切にコンパイルされたか否かって何かToolとかあるんです? >>72 たしかにLinqは便利な分最適化には程遠いかも >>73 いや分からない(知らない) コンパイルすら通らないのかコンパイルは通るけど使い方間違えると実行時エラーでるのかが分からないからそう書いただけ >>74 便利だけどゲームだと使い道が難しそうな印象があるなあ 今日の作業で久しぶりにLINQ使ってみたけど、FindAllしたら新しいList<T>インスタンスが戻り値として生成されるみたいで結構な量のアロケーションが発生しちゃった Forで要素0からループで走査して条件一致する度にグローバルな使い回し用リストにAddするとアロケーション無しになる ただインベントリのアイテム内を並び替えるだけの処理で戦闘中とかフレームレートが必要な場面で呼び出されることは想定してないから、まあ実際にプレイに影響は出ないとは思うけどね そういえばFuncでリスナー複数いるとどうなるの?問題についてちょっと検証してみたそういえばFuncでリスナー複数いるとどうなるの?問題についてちょっと検証してみた using System; using UnityEngine; public class TestClass : MonoBehaviour { // 静的アクション public static event Func<int,int> testFuncEvents; // シリアライズフィールド [Header("Funcテスト")] public bool startFuncTest; public int FuncTestArg; public int Returnvalue; public int? Result { get => Returnvalue; set { Debug.Log("Resultプロパティが呼ばれました 戻り値は"+value); if(value.HasValue) Returnvalue = value.Value; } } 改行と文字数の限界でコード貼るのは無理か 結論としては @ 変数に代入される戻り値は一番最後に登録されたリスナーのものになる A 最後のリスナーのみがプロパティをコールする。リスナーは3人いるのにプロパティは1回しかコールされなかった(リスナーは3人ともデバックログが出たが、プロパティは1回しか出なかった) B nullはnullじゃない文字列と連結しようとすると空白になるが、単体又はnullと連結しようとすると文字列はNullになる まあリスナーの管理が面倒だからたしかにChatGPTの言う通りFuncはイベントで扱うべきじゃないのかも? 今日の作業はクラスの設計を考えた 「カプセル化」をすることでクラスを他の処理にも再利用できるので、静的イベントやインターフェースを駆使して様々な「選択」が必要なUI画面で利用できる独自セレクタブルを作りたい 今日はグラフィック関係をやっていてコードにあまり触れてないので調べ物はナシ! enumのSwitch式だかSwitch文だかをもう少し書きやすくする方法ないかなとは思ったり 制作が軌道に乗ってくるとメモすることがなくなるんだよな 既に調べた知識で作り続けるだけだから だいぶ先になるけど有料アセットのうちFinalIKとEasySaveは詳しく使い方を覚えたいし覚えたら書くかねえ 特にFinalIKはUnity標準のメカニムとどのくらい共用できるのか知りたい >>81 それはアリだねただ一応秘密で作ってるんでどこまで内容書くかは悩みどころだが 今やってるのはアイテム合成システムの設計 合成時に素材の特殊能力を引き継ぐのだが、異なる特殊能力同士が合体して新たな特殊能力に変わるシステムをどうやって実装するか考えている 一番手っ取り早そうなのは、 ①素材アイテムの特殊能力をコレクションに格納する→②コガネブログからお借りした組み合わせ列挙拡張メソッドを使って事前に指定した合体組み合わせに該当するか調べる→③合体組み合わせが不存在になるまで繰り返す かなと思っている 俺もちょうど合成システム作ってるとこでビビった 俺は2つのarrayをソートして、比較する単純なものだけど… >>83 合成が合成システムを実行したその1回しか行われないならそのやり方の方が楽そう 自分のは合成して特殊能力が合体した際に次レベルの合体条件も満たしていたら順次合体を繰り返していく機能を想定しているから、配列やリストを何回も走査すると重いかもなあという懸念がある ただコガネブログのも合体成立時に何回も呼び出すと重そうなので、素材アイテムの持つ特殊能力の管理番号全てをHashSetに入れて、差集合とAddを使って合体組み合わせを探していくのもアリかも こればっかりは自分の設計に合わせて複数のコードを組んで検証してみるしかないね おや?停滞? 良スレかと思ってたが、中途半端に投げ出す感じな 今は既に調べておいた知識でゆっくり作っている状況だから特に書くことが無いのよね グラフィックス関係の勉強もしてるんでそっちに多く時間を割いてるのもある 一応調べたことの備忘のスレのつもりなんで、何も調べてないのに毎日作業内容を報告するのもなんか違うと思うし あと単純に最近安定して5chが見られなくないか? 基本的にメモは作業用ではないPCのEdgeで書いてレスしてるけど、今もまたスレが開けなくて内容ドライブ経由でコピーしてスマホからレスした 今やってる作業はアイテム合成システムのクラス設計、メニュー画面・装備画面UIの作成あたりかな ゲームシステムについての最近考えている事は ・アイテムの購入・売却の実装 既にエクセルデータをスクリプタブルオブジェクト化して売値・買値等のデータを取り込んである & プレイヤー以外もアイテムを所持できるようにプレイヤー・NPCの共通基底キャラクタークラスに所持品クラスのフィールドと取得用インターフェースを実装済み、装備画面UIの作成時に抽象クラスとインターフェースを利用して割と再利用性を高めてある(つもり)なので、作成自体はそこまで難しくなさそう。 アイテム売る商人の持つアイテムデータは、アイテムインスタンスクラスに[Serializable]を付けてキャラクタークラス→所持品クラス→アイテムインスタンスで開いてインスペクターから直接設定するか、又は何らかのコレクションで値だけインスペクターから設定してStart関数でアイテムクラスのインスタンスをnewしてコンストラクタにその値を渡してやればよさそう ただそもそもゲーム内に商人を実装するかは未定 ・3Dのゲーム性を採用するか 購入済みのノーコード系アセットがパフォーマンスの向上目指して改修されるそうなんでやっぱり使ってみたい感がある(気持ちの問題)。 あとこのアセットがasync/await使ってる所為でアセットの機能にアクセスするとそれなりにGC発生するんだけど、将来的にはUnityにも.NET5のAsyncValueTaskが導入されたら更にパフォーマンス向上するかも(非同期処理のこと全然分かってないので自分でアセットのコアコードをUniTaskに置き換えるのはまだ怖い) C#・Unityの勉強としては ・非同期処理 上でも書いたがよく分かってない。今作っている範囲だと用途は少なそうだけど、唯一「アイテムを大量に捨てる場合」に役立ちそうな気がしている。 というのもアイテムを捨てる際には自作のアイテム管理クラスのアイテム破棄の関数をコールするのだが、検索システムに備えて入手時にコレクションへの登録を沢山しているので破棄時には逆にコレクションからの削除を行う必要がある アイテム捨てる処理は入手処理と違って新たなインスタンスはほぼ生成しないのでGCの問題はないが、単純にコレクションへのアクセスが多いのと、プレイヤーが何百・何千個ものアイテムを一気に捨てることができるUIを組んでしまったので大量にアイテム破棄を行うと余裕で60fps割ってしまう。また所持品管理ディクショナリーの参照が切れるのでアイテムインスタンス自体もガベコレの対象になるのでガベコレによるスパイク発生の危険性も高くなる。 そこで非同期処理を用いてフレームを分散してアイテムの破棄を行えば、捨てる処理自体のスパイクは軽減できるかなと思った プレイヤーは1f単位で処理を認識している訳ではないので、内部的には数fに分離しても多分気が付かない(あまりに重い場合は「アイテムを捨てています」等の進捗を画面に表示した方がよさそうだけど) 非同期処理はコルーチン、async/await、UniTaskあたりが有名なのかな?性能でいえばUniTask一択なんだろうけどまずはコルーチンから試してみようと思っている(GCは知らない) 自分はインボーク、インボークリピートをもっぱら使うなぁ >>88 InvokeやInvoke.RepeatingはMonoBehaviourの機能だから個人的にあまり使いたくないなあと思ったり(他のクラスに移植したり仕様変更する際に面倒そうなので…) 引数にstring型使うからアロケーションも心配 ただアタッチ先ゲームオブジェクトだけで完結する処理ならそれが手っ取り早そうだね でも引数はメソッドの名称だからアロケーション発生しないんじゃね? >>90 エディタ上でテストプレイしてみたけど。確かにMonoJIT以外でのアロケーションは発生しないね 原因について雑に調べたけど、もしかしたら文字列リテラルだからコンパイル時に生成・メモリに格納されて一致する限りは使い回しがされるからっぽい >>90 エディタ上でテストプレイしてみたけど。確かにMonoJIT以外でのアロケーションは発生しないね 原因について雑に調べたけど、もしかしたら文字列リテラルだからコンパイル時に生成・メモリに格納されて一致する限りは使い回しがされるからっぽい >>93 GCallocは削れば削るだけ良いからね 自分はECSを使うのは現状では無理そうだから削減可能な場所は全て対応するようにしている >>94 MonoBehaviourで非同期処理を扱うならコルーチンが入門な感じがあるね 返し値を扱うのが面倒そうなのが難点だけどそれでもUniTaskよりはとっつきやすそう(偏見) 今のパソコンというか環境でも やはりけづれる物は削る方がいい? 初心者とかそこまで考える必要はあるのかな? >>96 明らかなボトルネックや安定した後の最適化ならともかく、開発中はできるだけ素直でメンテナンスしやすいコードの方が良いよ read.cgi ver 07.5.1 2024/04/28 Walang Kapalit ★ | Donguri System Team 5ちゃんねる