UnityProject知見のドキュメント化の取り組みに関して
こんにちは、情熱開発部プログラマ課の青柳です。
すっかり夏本番という感じで溶けてしまいそうです。
今回はUnityプロジェクトを業務で行う上で知っておいて欲しい事を色々なプロジェクトで展開する目的で書いたドキュメントを公開します。
何故この文章を作ったか
弊社内でもUnityを使うプロジェクトを行ってきた経験によってUnityに関する知見が溜まってきてはいるのですが、それを中々プロジェクト外に展開出来ないままでした。
それを解消する為に、Unityプロジェクトを始める前に知っておいて欲しい事をまとめたのが今回の文章です。
ドキュメントはGitで管理し、プルリクエスト・レビューで変更履歴と理由を残していく形で運用しています。
注意点
注意点として、社内で軽いレビューは受けていますがほぼ書いたのが私、という事と、出来立てほやほやなのでこのドキュメントに沿って社内のプロジェクトを運用した事は無いという事でしょうか。
それでも何故公開するかというと暖かいフィードバックがあるかもというのを期待しての事です。
一つ一つにお返事は出来ませんが、はい、マサカリ大歓迎です、血の涙を流して喜びます。
長くなりましたが早速中身を以下に公開します、そこそこの長さになっていますがよければお付き合いください。
目次
- 共通
- Unityの仕組み編
- UnityProject構成編
- PureC#編
- C#メモリリーク対策
- UnityC#Script編
- UnityC#メモリリーク対策
- パフォーマンス対策編
- UI設定編
- UIパフォーマンス対策編
- Misc編
知っておきたいUnityProject運営Topic集
ここはUnityProjectを業務で進めるにあたって知っておきたい知識をまとめる目的で作成されたドキュメントです。
ここでさす業務で行うとは多人数で多量のアセット、ソースを取り扱う、という事です。
その為に知っておきたい事柄、推奨されるプログラムの書き方、Unityプロジェクト構成、設定を書き周知する事が目的です。
その為、初心者向けというわけではありません。
重要度毎に
- 必須
- 強く推奨
- 推奨
の三段階に分かれています
早速Topicを見ていきましょう。
Topic
共通
具体的な事柄に入る前に心構えのようなものを。
- 【必須】最新のUnityの情報をキャッチアップしよう
Unity公式やUnityJapanの動画、Blogなどで最新の機能を確認しましょう、それとなくでかまいません。
- 【必須】最新のC#の情報をキャッチアップしよう
まだまだC#は活発に開発されています。
パフォーマンスにもフォーカスしSpanなどが追加されました。
最新のC#を知ってクールにプログラムを書きましょう。
公式 - 【必須】このドキュメントを更新する事を厭わないようにしよう
業務でUnityを使ったりしてこのドキュメントを更新したい時が来ると思います。
そんな時はGitから更新のプルリクエストをドンドン作ってください。 - 【必須】このドキュメントに沿っていない事に気づいたらプルリクエストのコードレビューの際などに指摘しよう、修正のプルリクエストを作ろう
Unityには沢山の落とし穴があります。
それを回避、修正できるのは気づいたあなただけです。
負債は後になるほど大きくなるので気づいたその瞬間に他の人にお知らせしましょう。
発見の一番最初の機会はコードレビューです。
Unityの仕組み編
- 【必須】Unityがどうやってファイルを管理しているか知ろう
UnityはGUIDでそれぞれファイルを管理しています。
このGUIDはインポート時に割り振られ、ファイルの間の参照はこれで解決されます。
公式 - 【必須】.metaファイルに何が書かれているかを知ろう
.metaファイルはUnityにファイルをインポートしたりフォルダを作ったりすると自動で作成されます。
このファイルには先のGUIDが書かれています。
下図のように .metaファイルはテキスト形式のファイルです。ソース管理ツールなどでも差分がわかりやすく確認できます。 - 【必須】metaファイルを新たに生成する操作には気を付けよう
Material,ScriptなどよくMissingになるのを見かけます。
これは参照していたGUIDが見つからない為に起こります。
GUIDが見つからない原因は、まずファイルが削除されていたら見つかりません。
もう一つ、元々GUIDが指していたファイルはあるが、違うGUIDが割り振られていた場合は同じくMissingになります。
そうなってしまう原因は様々ですがMetaファイルを新たに生成するような操作が行われたことが原因です。
ex: .meta削除、.meta上げ忘れ、explorlerで直接ファイル移動し.meta移動し忘れ…etc
そのような操作には気を付けましょう。 -
【推奨】UnityのMemory管理のイメージを持っておこう
UnityはVMで管理するManargedMemoryとUnityが管理しているNativeMemoryがあります。
この世界が分かれている為、基本的には世界を超えるにはメモリコピーが発生します。
メモリも余計に消費しますし、データの行き来が入る分重いです。
またUnityは参照カウンタでメモリを管理しています。
基本的にはどちらのメモリもオブジェクトの参照がなくなり、かつ所属するシーンがUnloadされた場合に解放予定のメモリに積まれるはずです。
ただしPlatformによっては積まれるだけ積まれて解放されない場合があるようです。
そんな時は下にもあるResources.UnloadUnusedAssetsを呼んでやる必要が出てきます。 - 【強く推奨】Addressable(AssetBundle)の仕組みを知ってメモリに対する影響を知ろう
Addressableがどのようにファイルをパックしてそのファイルがどうメモリに展開されるかを知っておき、メモリ消費を抑えメモリリークをしないようにしよう。
公式BestPractice 公式FAQ - 【必須】Resources.UnloadUnusedAssetsを呼べるタイミングで呼ぼう
AdditiveでSceneをLoadしている場合 Platformによっては不必要なメモリが解放されないようです。
Asyncなメソッドではありますが重い処理なのでLoadシーン終わりなどで呼びましょう。 - 【必須】Resources.UnloadUnusedAssets実行中は何もしないでおこう
プラットフォームによってはアプリ落ちが発生する場合があるようです。
UnityProject構成編
- 【必須】スケール出来るプロジェクト構成にしよう
リソースや人員が増えても大丈夫な理由を持ってプロジェクトを綺麗に保とう。
- 【強く推奨】プロジェクトのフォルダをまず作ってからその中にファイルを入れよう
あまり行儀のよくないライブラリなどあるかもしれません。
念のためにプロジェクトの名前を付けたフォルダを最上位に持ってきてそこから下に作っていきましょう。 - 【強く推奨】Resourcesフォルダは使わないようにしよう
公式で非推奨です。
Resourcesフォルダの中身は強制的にRomに含まれます。
またResourcesクラスのLoadは負債になりやすい為、使用するのはやめましょう。 - 【強く推奨】極大量のModelAssetがある場合はAssetBundle用のプロジェクトを別途作ってそこからAssetBundleを作成しリソースを参照しするかキャッシュサーバーを設定しよう
リソースが大量にあると予想される場合、普段の開発イテレーションが急激に落ちていきます。
重い要素が切り離し出来るか検討してください。
ModelならAssetBundleにビルドして配布するという形がとれるはずです。
キャッシュサーバーを建てるのも有効です。 - 【強く推奨】リソースを入れるフォルダは使用する単位でまとめよう
Addressableを設定するのが楽になるようにしよう。
例えばあるテクスチャがどこから参照されているか分かりずらい、という構成は避けた方がいいのはAddressableからの要求となります。
意図せず複数のAssetBundleに含まれやすくなる構成は避けましょう。 - 【推奨】ScriptのAssemblyDefineは使用する単位でまとめよう
コンパイルは速くなります。
またプログラムの構成が大概は綺麗になります。
以上から構成の1案を以下にあげます
- シーン毎にScript、リソースを分けて少なくともどのシーンで使っているかを明瞭にする。
AssetBundleは最大でシーン単位にまとまる。
AssemblyDefinitiaonの名前はPrefixにプロジェクト名で統一。
Assets
├─LogicalBeat_PRJXXXX
| ├─Common
| | ├─PRJXXXX_CommonAssemblyDef
| │ ├─Prefabs
| │ ├─Scripts
| │ ├─Shaders
| │ └─Textures
| ├─DependOnAll
| | ├─PRJXXXX_DependOnAllAssemblyDef
| | └─Scripts
| ├─Title
| | ├─Materials
| | ├─Models
| │ ├─Prefabs
| │ ├─Scences
| │ ├─Scripts
| │ ├─Textures
| │ └─PRJXXXX_TitleAssemblyDef
| ├─InGame//大きいシーンならこの中でもっと細かく分けてもいい
| | ├─PRJXXXX_InGameAssemblyDef
| | ├─Materials
| │ ├─Models
| │ ├─Prefabs
| │ ├─Scences
| │ ├─Scripts
| │ ├─Shaders
| │ └─Textures
PureC#編
-
- 【必須】参照型(Class)と値型(Struct)の違いを知ろう
C++では両者にほぼ違いはありませんでしたがC#では明確に違います。
値型は基本的にはStackに積まれ、コピーで値を更新します。
参照型はHeapに領域を確保されます。
MicrosoftLearn - 【必須】値型(Struct)はコピーで更新されるという事を意識しよう
コピーで更新されるという事の実例は以下です。
transform.position.x += 5.0f;// transformのpositionは更新されない Vector3 position = transform.position; position.x += 5.0f; transform.position = position;// これで更新される
transformのpositionをコピーして新たなVector3型変数の値は更新していますが。
それをtransform.positionに入れなければtransformのpositionは更新されません。 - 【必須】Boxingについて知ろう
値型を参照型にキャストする事がBoxingです。
上記のような違いがあるためにキャストの際、参照型の基底クラスobject型がnewされます。
MicrosoftLearn
有名な例を上げれば以下のようなものがあります。IEnumerable Hoge{ yeild return 0;//値型なのでBoxing }
string.Format("{0}", 1);//引数にobjectしかとらないので「1」がBoxing
- 【必須】場合によって使うコレクション、メソッドを選ぼう
検索が多いならDictionary(HashSet)、要素の走査が多く要素の追加があるならList、ないならArray。
Listでも要素のInsertが多ければLinkedListを検討、最後に要素を追加する場合が多ければCapacityの確認。
ListやArrayの検索はBinarySearchが使えないかきちんと選びましょう。
※Arrayの走査はとびぬけて早いです。 - 【強く推奨】なるべくStringは使わないようにしよう
C++でもそうで現代になってもそうですが文字列操作はパフォーマンス上問題になりやすい処理です。
特にC#はGCAllocも加わってつらさが増します。
DictionaryのKeyにするには手軽ですがもう一度使わなくて良くないか考えましょう。 - 【推奨】ラムダ式よりローカル関数を使えないか検討しよう
キャプチャが必要ないラムダ式でもGCAllocは起こりませんが、使える場面ではローカル関数を使った方が無難です。
※C#9.0以降からstatic lamdaが追加されました。
MicrosoftLearn
Styleの設定について - 【推奨】パフォーマンス ルールスを確認しよう
パフォーマンス ルール
Anyを使おう、ただしコレクション固有のLength、Count、IsEmptyがある場合はそれを使おう。
TryGetValueを使おう、1つのTaskでWhenAllを使うのはやめよう、などなど一通り目を通しておく事をおすすめします。
結構厳しめな部分もありますね。 - 【推奨】解放処理が必要ならIDisposeを継承しよう
解放はDisposeというルールにしましょう。
- 【推奨】Taskに慣れよう
慣れるために普段から書きましょう。
公式
重要な情報とアドバイス
こちらから一部抜粋します。 - 【強く推奨】Asyncメソッドは名前でAsyncと分かるようにしよう
Asyncはまた特別なので分かるようにしましょう。
- 【強く推奨】AsyncメソッドはなるべくTaskを返そう
投げっぱなしでいいかはユーザーに任せましょう。
- 【必須】Asyncメソッドは少なくともCancellationTokenを引数に持とう
Cancelを伝搬出来ない作りはやめましょう、後から変えるのは大変なコストになります。
Cancel出来ないTaskは無駄な処理だけでなく意図しない動作の元になります。
Cancelの事を常に頭に入れておきましょう、もちろん必要なければかまいません。
※CancelはOperationCanceledExceptionをThrowする事になっています。
公式 - 【推奨】ArrayPool、Spanに慣れよう
慣れるために普段から書きましょう。
公式
公式Memory Spanベストプラクティス
- 【必須】参照型(Class)と値型(Struct)の違いを知ろう
C#メモリリーク対策
C#はGCがありますが下手をすればメモリリークします。
そうならない為に注意しましょう。
-
-
- 【必須】循環参照には気を付けよう
循環参照していると一生解放されません、プログラムを綺麗に保って循環参照を起こさないようにしましょう。
┌J─I─H─G─F┐ └A─B─C─D─E┘
-
【必須】イベントを使う際は購読解除をセットにしよう
GCはイベントハンドラに参照がある限りその対象を解放しません。
きちんと「-=」で購読破棄しましょう。
MicrosoftLearnclass ListenerCalss : Monobehaviour { [SeriazlizeFeild] private SomethingBroadcaster broadcaster; public void Start() { loadResource(); setSubscribe(); } public void OnDestroy() { unsetSubscribe(); } private void setSubscribe() { if(broadcaster != null) { broadcaster.onClick += onClick; } } private void unsetSubscribe() { if(broadcaster != null) { broadcaster.onClick -= onClick; broadcaster = null; } } private void onClick() { } private void loadResource() { ... } private string hugeText; }
※上のプログラムでNullチェックしてる箇所がありますが、下で書いた通りUnityObjectのNullは実は生存チェックを兼ねています。
例えばbroadcasterがMonobehaviourであり、SceneUnloadにDestroyを任せている場合、先にbroadcasterがDestroyされてしまうと、NullでないにもかかわらずNull扱いとなります。
購読破棄されずにListenerCalssはリークしてしまいます。
コード自体が悪いわけでは無く、破棄順番を管理すべきクラスで管理されていないのが問題です。
詳しくはUnityC#Script編へ。 - 【必須】Actionは使い終わったらNullを入れよう
別のクラスの参照を持っているのと同様なので、こちらもきちんとNullを入れておきましょう。
- 【必須】変数をキャプチャしたラムダ式には気を付けよう
ラムダが解放されるタイミングにならないとキャプチャされた変数はGC対象になりません。
分かったうえでご利用ください。
MicrosoftLearn
特にメンバ変数をキャプチャしたラムダを自身をメンバ変数で参照していると循環参照になります。class LamdaHolder : Monobehaviour { OtherClass otherClass = new(); func<int> lamda; private void Start() { lamda = () => { return otherClass.value; } } }
- 【推奨】ReactiveExtensionに慣れよう
便利ですがスパゲッティになったり、AddTo、Disposeし忘れたりします、使ってみるというのはしておきましょう。
- 【必須】循環参照には気を付けよう
-
UnityC#Script編
-
-
- 【必須】ソースコードはUtf-8にしよう
強制する為に.editorconfigを作ろう。
Unityが作成するSlnと同階層に置けば勝手に追加されます。
VisualStudio公式 - 【必須】GCAllocが生まれる状況を意識しよう
Update内で行っている場合、結構馬鹿にならない事になります。
- 【必須】UnityEngine.Objectの!=でのNullチェックは生存チェックも含まれている事を知っておこう
賛否ありますがUnityEngine.ObjectのNullチェックはDestroyされたかもしれないもチェックします。
ただし!=演算子, ==演算子が対象です。
上のような解放し忘れ、下のような対応していない演算子の使用に注意しましょう。 - 【必須】UnityEngine.Objectには?, ??演算子を使わないようにしよう
Null条件演算子、Null合体演算子をUnityEnging.ObjectはOverloadしていない為、生存チェックは効かず意図しない動作になります。
- 【必須】ReloadDomainを無効に出来る作り方をしよう
staticな変数やstaticなイベントハンドラを初期化出来るようにしよう。
つまり、この手のものは増やさずきちんと管理できるようにしよう。
作る必要がある場合は[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] Attribute
を付けたメソッド内で初期化しよう。
ReloadDomainについてはこちら。
※サードパーティーのコードで対応していないものもあります。 - 【強く推奨】Monobehaviourを管理するためのマネージャーを作ろう
MonobehaviourのUpdateを個々に呼ぶと重いのは有名ですが。
そのほかにAwake、Start、onDestroyなども個々のMonobehaviourには頼らないようにしましょう。
初期化順、解放順など管理したくなることが多いはずです。
また、管理する事でゲーム一時停止への対応などもしやすくなります。
- 【必須】ソースコードはUtf-8にしよう
-
UnityC#メモリリーク対策
Unityは知らないとメモリリークしやすい部分があります、対策しましょう。
Game Industry Conference講演動画
Repairing your game’s memory in Unity – Zbigniew Andrzejewski
-
-
- 【必須】UnityEnging.Objectをnewした際にはDestroyのコードも追加しよう
NativeMemoryと結びついたObjectを手動でNewした場合は手動でDestroyするようにしましょう。
Mesh,Texture,RenderTexture,,,etc
newしたらDestroyしましょう。 - 【必須】アクセスしただけでUnityEnging.Objectを生成するプロパティを知ろう
Mesh@materialなどはアクセスしたらMaterialの複製が作られます。
しかしそれが問題かというとまた別で。
一回作ればそれがキャッシュされますし、SceneをUnloadすれば参照が無くなり廃棄予定に積まれるはずです。
通常ならば、、というのは1シーン構成であったりそのMaterialを参照したままのクラスがいれば当然リークします。 - 【強く推奨】外部から注入されたメンバ変数のClassにはNullを入れておこう
循環参照には気を付けよう、とはいうもののチェーンが長くなっていると気づかないものです。
消極的ではありますが、外部から貰ったClassの参照を切るためにNullを入れておきましょう。 - 【必須】public feildやSerializeFeildは濫用しないようにしよう
循環参照の元です。
必要以上のMonobehaviour、GameObjectの参照は控えましょう。 - 【強く推奨】Subject, Task, DoTweenはGameObjectがDestroyした場合の事も念頭に置こう
GameObjectがなくなっても動き続けようとするタイプはCancelしたりGameObjectの寿命と紐づけるAddTo、SetLinkを呼びましょう。
もうDestroyしてるMonobehaviorにアクセスしようとする事故はよくあります。
- 【必須】UnityEnging.Objectをnewした際にはDestroyのコードも追加しよう
-
パフォーマンス対策編
パフォーマンスチューニングバイブルという素晴らしい資料がありますので目を通して下さい。
軽く雑多な部分を上げていきます。
-
-
- 【推奨】GameObjectの数は最小限にしよう
GameObjectあるだけで重いです。
- 【推奨】ScriptableObjectを使おう
CSV, JSONもいいのですが最速はScriptableObjectです。
-
【推奨】なるべくInstantiateは避けよう
強烈に重いです。
ほぼ確実にStopTheWorldします。 -
【強く推奨】重い処理をUpdateで書かないようにしよう
GetComponent, FindObject無駄に呼ぶのはやめましょう。
-
【推奨】GameObject.SetActiveには副作用がある事を知ろう
案外重いですし、非アクティブからActiveにして元通りにならなず初期化されてしまうものもあります。
AnimatorControllerが代表的です。
参考 - 【推奨】NativeArrayを使おう
- 【推奨】Spanを引数にとるメソッドを使おう
- 【推奨】NoAlloc版のメソッドを使おう
- 【推奨】便利なライブラリは導入を検討しよう
UniTaskはもとよりZString, ZLoggerは良さそうです。
ただそのままで全てのプラットフォームで動くかはやってみないと分かりません。
- 【推奨】GameObjectの数は最小限にしよう
-
UI設定編
-
-
- 【必須】 マルチ解像度対応ならアンカーを設定しよう
解像度を変えた際にもUIを画面の相対位置で同じ場所にしたいならアンカーを設定しましょう。
- 【強く推奨】 対象プラットフォームがコンシューマー機のみでもマウスに対応しよう
必要なくてもボタンなどはマウスに対応すべきです。
デバッグ時に役に立ちますし、急なPC移植にも耐えられます。
- 【必須】 マルチ解像度対応ならアンカーを設定しよう
-
UIパフォーマンス対策編
-
-
- 【必須】UIはパフォーマンスネックになることを知ろう
特にuGUIはすぐに重く出来る作りになります。
昔の記事ですが基本は今も変わっていません。
TECHxGAME COLLEGE #10
公式・旧
公式・新
公式・日本語この中からピックアップする形で上げていきます。
- 【必須】Canvasにまとめる単位は考えよう
uGUIのリビルドはCanvas単位で行われます。
リビルドで何が行われるかというと頂点の再構築が行われます。
リビルドが行われるのは以下の条件があります。- UIのEnable, Disable
- マテリアルの変化
- RectTransformの変化
- Transformの親が変化
この変化がCanvasの子供全員に関わる処理になる為、子供が多ければ多いほど重い処理になります。
ただし、Canvasにまとめれば描画処理だけ見るといい事が沢山あります。
描画バッチが効いて一度に沢山描画してもらえたりします。
つまりCanvasにまとめる単位を十分に吟味する必要があります。 - 【強く推奨】アニメーションするUIは独自のCanvasにまとめよう
上述の通りCanvasにまとめる単位は考える必要があります。
それぞれトレードオフがありますが、まずアニメーションするUIはそれだけでまとめてしまった方が良い結果になると思います。 - 【強く推奨】アニメーションはなるべくTweenライブラリで行おう
Animatorで行うとAnimator自体の負かもさることながら動かしていない値も変化したとみなされてしまうようです。
DoTweenなど使えるようになりましょう。 - 【必須】GraphicRayCasterが必要ないなら取り除こう
クリックなどしないUIのCanvasなら必要ありません。
取り除いてしまいましょう。 - 【必須】RayCastTargetが必要ないならOffにしよう
こちらも必要なければOffにしましょう。
- 【必須】LayoutGroupは使わないようにしよう
結構しつこく言われているので使わないようにしましょう。
- 【必須】TextureAtlasをきちんと使おう
使っていきましょう。
- 【推奨】Spriteとして画像をImportするさいMeshTypeを確認しよう
FullRect、Tightが選べます
頂点数が少ない代わりにDrawされる領域が多い、頂点数が多い代わりにDrawされる領域が少ない
どちらもトレードオフがあるので考えて設定しましょう。
- 【必須】UIはパフォーマンスネックになることを知ろう
-
Misc編
-
-
-
- 【強く推奨】TextureAssetのReadWriteのチェックが必要か吟味しよう
ReadWriteのチェックが入っているとVMからデータをアクセスできるようになります。
その代わりメモリが2倍になります。
公式 - 【強く推奨】MeshAssetのReadWriteのチェックが必要か吟味しよう
ReadWriteのチェックが入っているとVMからデータをアクセスできるようになります。
その代わりメモリが2倍になります。
その他、NaviMeshをランタイムでベイクしたいなどの時に必要です。
公式 - 【推奨】HumanoidでImportする場合かつBoneにアクセスする必要がないなら消してしまおう
HumanoidでImportする場合、ModelのImportの設定
Rigタブの「Optimize Game Object」で消せます。
公式
- 【強く推奨】TextureAssetのReadWriteのチェックが必要か吟味しよう
-
-
終わりです
長い文章でしたがお付き合い頂きありがとうございました。
間違い、過不足、拙い部分あるかと思います、お教え頂ければ幸いです。
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。