見出し画像

Unityプログラミング現場に蔓延る罪

■前説

今回の記事は、Unityを用いてアプリを制作するにあたって、
うっかり使うのはよくない系メソッドとその代案などを解説しています。
あとなんか最終的には設計の話にも触れてます。

完全に(初学者)Unityプログラマー向けの内容となります。
前回と比べてコンセプトがニッチ? しらん。

尚、今回の記事はUGDGの皆様の監修を受けています。
この場を借りて感謝申し上げます。

◆ぶっちゃけまずいとされるコードは何がまずいのか

ここで取り上げているアンチパターンとその代案は、制約がより多い方を推奨しているものであり、更に捻くれた言い方をすれば、「あえて"不便"な方を用いる」ものです。

なぜ制約が多い方を用いるのか。例えば、なぜpublic変数よりprivate変数にした方がいいのか。実はこれ、的確に初学者に対して説明しようとすると、そこそこ難しい気がします。外部からの不用意なアクセスを防げるとかよくいわれますが、「じゃあ注意して不用意なアクセスをしなければいいのでは」って話になりますし……。

個人的な回答としては、プロの現場でチーム開発するうえで、
「おまえ自身はそのプログラムで困らないかもしれないが、他の人がおまえのプログラムを読むとき困るし、他の人のそういうプログラムを読まなければならなくなった時におまえはたぶんキレる
ではないかと思います。とどのつまり一言で言えば可読性なんですが。

逆に言えば個人で趣味で開発するなら別に好きなやり方でやっていいとも思います。今回の記事は「プロのプログラマーを目指す人」を対象としています。

補足ですが、「一カ月前の自分は他人、自分が過去に書いたコードも読みづらくなる」ともよく言われますが、これ結構個人差があると思っているので、これも必ずしも万人に通用する話ではないと思っています。過去に書いた自分のコードを忘れないタイプの人もいます。

◆なぜこの記事を書こうと思ったか

困ったことに、Unityの初心者本や、ネット上の講座系のサイトで、これらを使用しているものが多いからです。(あるいは純粋に執筆された当時はメジャーな方法だったものが、時代の変遷とともにレガシーな手法になっているケースも無くはないです)。確かに最も分かりやすく手っ取り早い方法ですし、前述のように、趣味の範疇なら問題ないと思ってはいますが、困ったことにこれらを使うスタンスのままチーム開発する立場になってしまう人もいるようで……(UGDG内ヒアリングの結果、多数の現場での被害報告を確認)

◆先に補足

今回の記事は、「最低限未満のレベルのコード」を、
「最低限以上のレベルにはする」事を目的とした記事です。
よりレベルの高い設計は勿論存在します。それについては
当記事の最後の方で触れています。


■本編



■銅の弾丸「[SerializeField]とInstantiateの返り値」を頼る

これは、以下に紹介するパターンに対する多くの解決案となるので
先に提示します。

「この手法を使えば全ての設計は解決できる!!」 という銀の弾丸は存在しません。当たり前の話です。しかし、「この手法を使えばだいたいのケースは何とかなる」くらいのつまり銅くらいの弾丸は存在します。

[SerializeField]及び、Instantiate(ないしは生成系メソッド)の返り値です。

[SerializeField]は……一応公式の説明を引用すると、各フィールドを強制的にシリアライズするアトリビュートです。なんのこっちゃだ。

//本来は別ファイルに宣言するコンポーネント
public class Foo : MonoBehaviour { } 

[SerializeField] private Foo bar;
[SerializeField] private int hoge;

平たく説明すると、[SerializeField]をスクリプト内のフィールド定義の前に記述することで、コンポーネントにフィールドが表示され、そこに値を入力することで、プログラム実行前に予め既定のパラメータを代入しておくことができます。 この方法を用いることで、別コンポーネントを参照する事が出来るようになります。 広義のDependency Injection(DI)と言えます。

いわゆる、
「とあるコンポーネントAから別のコンポーネントBを参照したい」
ケースはだいたいこれで解決します。

実行前からシーンに存在するオブジェクトに対しては、[SerializeField]が有効です。一方で、実行中に生成するゲームオブジェクトに関しては、その生成するメソッドの返り値の参照を保持するのが有効手です。

// Instantiate用なのでMonoBehaviour継承
//本来は別ファイルに宣言するものです
public class Foo : MonoBehaviour { } 

[SerializeField] private Foo bar;

void Fuga() {
    Foo current = Instantiate<Foo>(bar);
}

Instantiateメソッドは、生成時に、Instantiate元の型に応じた値を返します。
生成したオブジェクトを何かしら管理したい場合は基本的に生成時の返り値により参照を保持しましょう。Instantiateより使用頻度は低いですが、new GameObject()もAddComponentもその時点での返り値を保持するのが基本です。間違ってもあとからFindで探したりしない。

だいたいこの2つでどうにかなります。 いや本当に。

【大罪】GameObject.Find() その他Findがつく関数

大罪。 

いわずもしれた諸悪の根源。GameObject.Findが代表格ですが、Findが名前にある関数はだいたいまずいです。 

◆なぜまずいのか

・GameObject.Findは名前検索。これは可読性が最低のもの。
・それ以外でもシーン内のヒエラルキーの状態に依存するものが多い
・シーン内を全探査するため、パフォーマンスが悪い

と、とにかくどこをとってもデメリットの嵐です。

◆どうすればいいか

上述の[SerializeField]かInstantiate返り値で参照を獲得しましょう。

【大罪】string型による制御構文

例えばif文でゲームオブジェクトの名前で比較する。
あるいはStartCoroutineなどでメソッド名をstringで指定する。

後者に関しては、C#のリフレクションという機能により行える実装ですが、やはり大量の問題点を抱えている為、安易に使うべきではありません。

◆なぜまずいのか

GameObject.Findと同じですが、オブジェクト名=シーン内の設定に
スクリプトの処理が依存してしまうのは可読性に大きな難があります。

また、IDEが文字列には対応できない という欠点も大きいです。

◆どうすればいいか

IEnumerator TestCoroutine() {
    yield break;
}
void Fuga() {

    // 呼び出し元にジャンプできる
    StartCoroutine(TestCoroutine());

    // 呼び出し元にジャンプできず、
    // 更に、関数名が変わってもコンパイルエラーで検知できない
    StartCoroutine("TestCoroutine");

}

コルーチンなどに関しては、
文字列指定ではなく直接メソッドを指定すればよいです。

少し解決が難しいのが
OnTriggerEnter()などの、コリジョン絡みのイベント関数です。 
おそらく文字列比較もここでやってしまうことが多いのだと思います。

なんかもうUnityがそういう仕組みで作っている為、スマートに解決する方法はありません。あんまり綺麗じゃないなーと思いつつ、TryGetComponent() で対象のオブジェクトに該当のインターフェイスの有無を確認して、そのインターフェイスの特定の関数を叩くケースが多いかも。

【注意】Tagによる比較

コリジョン絡みのイベント関数で見かけることがある処理。
やはりスクリプトの処理がシーンに大きく依存してしまうのがまずい。

◆なぜまずいのか

可読性と保守性を大きく損ねるため。
毎回同じこと言ってるこの項目もう要らんな。

◆どうすればいいか

コリジョン絡みのイベント関数に関しては前の項目に準じる。
そもそも特定のオブジェクトに対して接触処理をしたくない場合は、
Layerを使いましょう。Project Settings->PhysicsないしPhysics 2Dでレイヤーごとに接触するかの設定が行えます。

下のチェックマーク部分で設定可能

【注意】public static変数(時と場合による)

最初に断っておくと、static自体は全く悪くないし、
public static変数自体も(あまり推奨はされないものの)
状況次第では使った方が楽なことは多くあります。

ただ、やばいのは単純に他クラスの変数を触りたい、その用途の為だけにpublic static変数を定義しているケースがある事です。
なのに、そういう解説をしているネットの記事がちらほらと……

◆どうすればいいか

上記のように、他クラスの変数を触りたいなら、
そのクラスの参照をDIすればいいだけです。

多少難しいケースとして、「シーンを跨いで値を保持したい変数」というケースがあります。この場合は、「シーンを跨いでも消えないオブジェクト(とその中のコンポーネント)を作り、その中に保持する」というのがひとまずの正解になりますが、これはちょびっと難易度高めの実装の為、手っ取り早く実装したい場合はpublic staticの方が安牌になることはあります。

【注意】publicメンバ変数(時と場合による)

これはUnity上だけでなく、プログラミング全般で言われている話です。

◆どうすればいいか

とりあえず最低限外部から直接変数を弄れる構造は辞めましょう。
どうしても外部から干渉したい場合は、直接変数に対して代入するのではなく、そのような関数を用意するのがセオリーです。あるいはプロパティ。

【古の罪】goto文

古来の言語より受け継がれし伝統ある命令であり、
そして殆どの言語で仕様が非推奨とされる、 
由緒正しき悪しき命令です。

Unityは(プログラミングの歴史全体からすると)
トレンディすぎてgoto文は少しマイナーな立ち位置な気もしますが、
使ってはいけない命令であることに変わりはないです。 

◆どうすればいいか

基本的な制御構造でだいたいどうにかなります。
for文、while文、if文の組み合わせ。

多重ネストを抜け出すときのみ有用、とされることもありますが、それもその部分を関数化して、returnすればいいと(個人的には)思います。

【負の遺産】Resourcesフォルダ

Unity古来より伝わる、リソース管理方法の一つです。
Resourcesフォルダに含めたリソースは、Resources.Loadにて
スクリプト中で読み込むことができます。

分かりやすい手法であり、太古のUnityでは積極的に使われていた手法です。
しかし、現代のUnityでは完全にレガシーな手法であり、いまやUnity公式に非推奨とされている手法です。 

が、困ったことに、アセットストアのアセットとかでは
平然と画像リソースがResourcesフォルダに格納されていたりします。
(あるいはそのアセットが滅茶苦茶古い可能性もありますが)

◆なぜまずいのか

すでに再三言われてる通り、「いつでもどこでもロードできる」が
プロジェクト保守の観点で問題だからです。

また、Resourcesフォルダに含まれるリソースは問答無用でアプリに組み込まれるため、うかつにResourcesフォルダに大量のリソースを格納しているアセットをプロジェクトに導入すると知らない間にアプリの容量が肥大化する、といった事故も起こります。まあこれはそんな構造のアセットを作った人が悪いんですけど…… 

◆どうすればいいか

特定のシーン、コンポーネントでのみ必要になるならば、そのコンポーネントにDIしてしまえばいい話です。銅の弾丸、[SerializeField]を崇めよ。 

また、リソース管理の代替の手法として、AssetBundle、Addressablesといった機能が存在します。

ただ、AssetBundleはある程度運用に多くの知識がいるので今回は除外します。そもそもunityroom単体ではクロスドメインの都合でAssetBundleは使いにくいです。一応NCMBがクロスドメインを許容しているのでNCMBのファイルストアを無理やり使えなくはないですが……まああまりオススメする方法ではないです……

Addressablesは扱えればとても有用な手法ですが、割と最近(とってももう数年前ですが)登場したために情報が幾分少なく、初学者にはすこしハードルが高いかもしれません。

■補足1 [SerializeField]の致命的な欠点

[SerializeField]、ここまで推してきましたが、大きな欠点を抱えています。

インターフェイスをシリアライズすることはできません。 

この1点だけが、Unityにおける設計の話を難儀にしています。
ある程度設計を意識すると、インターフェイスは必ず使用する事になりますが、この[SerializeField]をベースとしたUnity上の設計に対してものすごく相性が悪いです。

ちなみにGetComponent系列はインターフェイスを問題なく取得できます。
なので[SerializeField]よりGetComponentを推奨している所もあります。

この「インターフェイスをシリアライズできない」問題には
多くのUnityエンジニアが悩まされているようで、
その部分を疑似的に解決するSerializableInterfaceというプラグインが
有志により開発されています。

個人的には滅茶苦茶便利だと思い自分のプロジェクトにも導入しているのですが、何分このプラグインを導入している記事が少ないので
本当の所このプラグインを用いた設計がベストなのは判断できません。
有志の情報求ム。

また、ゴリッゴリの設計になりますが、
これらの問題はDIコンテナを用いると解決します。

2023年現在のUnityではVContainerが最有力のようです。

ただまあ……その……下の項目にも書きましたが、
だいぶ高度な次元の話になるので、「そんなのがあるんだなー」程度で
流してもいいと思います。階段は一歩ずつ登ろう。

■補足2 より上位の設計の話

というわけで、この記事の間銅の弾丸こと[SerializeField]と、生成命令の返り値の2つを推していましたが、この2つが設計における最善手かというと、もちろんそんなことはありません 。

・[SerializeField]も結局手作業で注入するなら
 どこかしら人為的ミスは発生するんじゃないの

とか

・どういった方法で参照するか以前に
 そもそも依存関係とかを意識して設計しないとダメでしょ

とか

・そもそもUnityと関係ないロジックまで全てコンポーネント化
 (MonoBehaviour継承)するのどうなの

とか

突き詰めようとすると、設計というものはキリがありません。
そう、設計というのは突き詰めるほどに深淵が広がる底なし沼なのです。

Unityにおける設計レベルは
とりすーぷ氏のQiitaの記事にわかりやすくまとめられています。

このQiita記事の観点から当記事を見ると、
-1レベルから1レベル程度に持っていく、
その程度の話しか書いていません。ほら、底が深い沼。

■おわりに

毎回なに書くか困るおわりに。

まあとりあえず……プロのプログラマー目指すなら、
人が読んでキレないくらいのコードを書きましょう!!

ちなみに銅の弾丸を用いた設計ならまあOKですっていうのは
UGDG内でとったアンケートで保証されています。たぶん大丈夫。

◆定型文句

この記事がいいとおもったら
いいねとかシェアとかフォローとかお願いします!!

現在このnoteの方向性を検討中です。
ただ基本的にはUnity&プログラム方面の記事が主体になると思っています。
前回の記事は妙に多方面に波及しましたが……
(なんかノベルゲー界隈とかツクール界隈まで波及してた

◆謝辞

前説でも述べましたが、
当記事はUGDGの皆さんの協力によって執筆されました。
改めて、この場を借りて感謝申し上げます。

↑UGDGについての記事はこちら。
UGDGよいとこいちどはおいで。


この記事が気に入ったらサポートをしてみませんか?