C#
Unity
疎結合
UniRx

Unity初心者がC#できれいなコードを書くには

スマホゲームの盛り上がりにより、学生をはじめ他分野からUnityのプログラマになる人が多くおられます。
入門書で基礎を覚えたらさっそくゲーム作り! と行きたいところですが、案外と綺麗なコードが書けず、再利用もできないものが出来上がってしまうものです。

個人的に覚えて欲しいことをリストアップしました。これを覚えてから開発に着手しましょう。

追記:
各項目に少し理由を付け足しました。
Unityにはデファクトスタンダードがなく、Unity公式サンプルを見ても以下の項目に当てはまらないものがあります。
会社やプロジェクト毎でも大きく違うので、参考程度にみるとよいでしょう。

意識してほしい

  • Privateは省略せずにちゃんとprivate修飾子を書く事 (美しさが+1上がります)
  • ファイル、クラス、メソッド、プロパティ、定数、protectedは必ず大文字から始める。(命名規則はAsp.netとRiderを信仰)
  • 文字の区切りに_を使うな(web系の人要注意)
  • メンバ変数は名前の先頭に_を付ける事(社内でmが使われていたら、そちらを優先。そもそも付けない方もいるので、既存のコードに合わせるが吉です。_は補完の時に分かりやすいのです)
  • ネームスペースは細かく分けておく(フォルダ単位がデフォルト)
  • " "や謎の数値が出てきたら、すぐに定数化(マジックナンバーは特に嫌われます。外部SDKがマジックナンバーだらけで推理ゲームかと思いました)
  • 1ファイルの行数は40行を目安に。(難しいと思ったあなた。密結合の地獄に落ちます)
  • ifやwhile、switch1つにつき、1バグ増えると思え(上場企業のソースを修正するときに痛感しました。カオスと闇しかありません)
  • 制御文の深さは2まで。if(true){if(true){}}(制御文の代わりに状態を表すプロパティを作ったほうが良いのです)
  • Switch文が出たらStateパターンに置き換えが出来ないか検討すること(Swich便利ですが、保守が大変なのです)
  • Start文とUpdate文は、使わなかったら削除すること(使わなくても実行対象として登録されてしまう)
  • 「今はいらないけどいつか使うかも。」というコードは書くな。その時まで保守されず、レガシーコードに化けてしまう。(今、痛感してます)
  • 「今使わないから、とりあえずコメントアウトしておこう。」はコミットを汚してしまう。メモ帳にでも避難させておく(誰かが有効にしてしまったら大変です)
  • boolを返す場合はプロパティ名にIsを付けろ(これは大事)
  • ExceptionやInterfaceは1クラス1ファイル毎(埋もれて発見に苦労します)
  • partialは使わない(これは自動生成クラスの為に作られたのであって、沢山書くためではないから)
  • AwakeでGetComponetを使うな(初期化順序が保証されてないのでnullになります)
  • 三項演算子を多用するな。特に2重はダメ( A == B? C == D ?E : F;) (4重を見てから嫌いになってしまいました)
  • 慣れないうちはシングルトンを使わない。慣れても使わない、使わせないを目標に。(自分は使っていますが、やっぱり怖いです)
  • 使い回せそうと感じたら直ぐにクラス化。(何回やっても終わらないですね)
  • メソッドや変数の前後には必ず1行以上空けること。ただし、意味合い的にまとめたい場合は改行しなくても良い。(詰める派と空ける派はあると思います。気付いたら良く改行消されてます...)
  • ソースの整頓は手書きで直すな。IDEにやらせろ(ショートカットを頻繁に叩くだけOK)
  • 略名は使用するな。(em => enemy, mov => move, tmp => _, trans => transform)(これは難しい所です。objectは型と被るのでobjの方が良いですし)
  • コピペをやりたくなったら共通化出来るサイン。コピペ厳禁。(これは間違いないです)
  • 出来る所はすべてvarで定義すること。(習慣化は大事です)

命名

  • なにかしらの実行クラスはServiceクラス(Web系の人にはなじみがあるかも)
  • なにかを管理するクラスはManagerクラス(Controllerでも可です)
  • なにかデータを示すものはModelクラス(プロパティだけが吉)
  • キャラクターはActorクラス(クロノトリガーの開発言語ではキャラクターをアクターって言ってたっぽいです)
  • 状態を示すにはStateクラス(そのままです)

ゲームマネージャー編

  • Managerはシングルトンにするな(これは半分間違いです。解放処理が適切に出来れば正しいのです)
  • ManagerのManagerは作るな(苦い思い出があります。GameManagerにDataManagerがあって、その中のStageManagerがPlayerManagerに...)
  • Managerに全ての機能を持たせるな(バケモノみたいにでかくなっていくのです。数千行になってくると解読に1週間とか掛かるのですよ)
  • Managerにvoidなメソッドを実装するな(これは機能を持たせない為です。あくまでクラスを返して欲しい)
  • Managerには必ずManager自身の状態管理を持て(シングルトンなら特に必要ですね)

危険サイン

  • 大きいクラスにとりあえず機能を詰め込んでいる(動きますが、その場しのぎのコードです。悔い改めましょう)
  • 1カ所を直すのに、何カ所も直さないといけない(密結合が発生しています)
  • Managerで直接実行している(便利なのは分かりますが、保守性がありません。必ず実行クラスを返して、そちらが処理をするようにしましょう)
  • 1クラスが沢山の機能へ参照を持っている(仲介クラスを作りましょう。疎結合にしていれば、そもそも参照数は減るはずです)
  • Interfaceがほとんどない(色んな書籍やデザインパターンをみて、必要性を感じましょう)
  • コンストラクタが使われていない(MonoBehaviorには使えませんが、それ以外では多用しましょう)
  • 1クラスに複数の機能がある(1クラス1機能を厳守)(難しいですが、志すようにしています)
  • メソッドやプロパティ名にAndなどの複数機能がついている(1メソッド1機能)
  • ネームスペースが殆ど変わらない(メンテナンス困難。外部ライブラリとコンフリクトした時は面倒くさいです...)
  • ManagerにGameManager、ApiManagerなどざっくりとした名前しかついていない(GameSequenceManager, ServerApiManagerなど具体的にする)
  • プロパティがすくない(状態が把握しにくいです)
  • とりあえずDebug.Logを書いてしまう(IDEのステップ実行を使えば激減するはず)
  • Baseクラスを多用している(密結合のサイン。なるべく継承はしないこと)
  • 1クラスのコードが100行を越えている(必ず複数のクラスに分けられます)
  • ほとんどのクラスがMonoBehaviorを継承している(機能の分離化が出来ていない)

あなたを救う?

  • UniRxを使うこと(Unityをマスターする近道)
  • 非同期に興味を持ったらすかさずUniRx(神様です)
  • 楽したいならUniRx(本当に楽になります)
  • UniRxのコードを読み解いてみる(さっぱり分かりませんが、得れるものは多いと思います)
  • 大きいプロジェクトになりそうならZenjectを使うこと(自分はお手製DIで対処してますが...)
  • 本気でやるならRiderかReSharperを入れる事(補完が優秀です)
  • 手続き型からイベント駆動型へスタイルを変える事(UniRxが適役)
  • PlayerPrefは使うな(これは保存場所が指定できないから。インメモリなので的確に保存指示を出さないと消えます。消えても良いような一時データなら良いでしょう)
  • セーブデータはMessagePackに変更(これは上級者向けです。ごめんなさい)
  • ソースこそが最新の仕様。コメントは最小限に留めること(仕様を書いた所で読んでくれないんです。悲しいね)
  • APIとの連携する時は、必ずInterfaceを利用したラッパークラスを利用する事。間違っても直接ゲーム内クラスをいじらない(仕様に合わせるためModelクラスを書き換えたらバグの嵐だった...)
  • UIはPush,Popを導入すると良い(実装が大変だけれど、何かと都合が良い)
  • Utilityフォルダをつくって、ガンガン拡張メソッドを書いていく(Transform周り、パース系は必須)
  • Scriptsフォルダがネームスペースに入らないようにIDEで設定しておく(Game.Scripts.Manager -> Game.Managerになる)

面倒くさいけれど、綺麗に書く手順

1.まずはInterfaceを書く(必須ではいですが、習慣化としての意味合いが強いです)
2.次にNullパターンを作る(実行しても何もしないクラス)
3.実行側を実装する(この時点で何も起きない)
4.Interfaceを実装した本体を書く(Nullクラスは消さない)
5.テスト

Componentパターンを信頼する

UnityはComponentパターンで出来ています(完全ではないけどね)。コードもそれに合わせた書き方にしましょう。

  • MonoBehaviorを継承したクラスは「機能のまとまり」と考える。
  • MonoBehaviorが破棄されたら、必ず機能は破棄される。(内部クラスが外部のクラスから参照されてると上手く解放されないかも)
  • MonoBehaviorはシンプルなクラスの寄せ集めにする(MonoBehaviorがMonoBehaviorを参照しない)
  • DontDestroyは使い方を誤ると危険。出来れば使わない(ゴミデータが溜まっていったりします)
  • OnDestroyで外部のMonoBehaviorを参照しない。非同期の解放処理は実装しない(イベント通知にしよう)
  • MonoBehaviorに対してnull合体演算子は決して使わない(Unityが裏でごにょごにょしてる)

ゲーム的思考

  • キャラクターに直接ダメージ処理を持たすな(継承が絡むと修正が大変...)
  • ダメージ判定は全て、ダメージ判定クラスに任せる(通知する)
  • ダメージ判定クラスはキャラクターに持たせない。通知してくれる仲介者を持たせよう。
  • バトル終了後はダメージ判定クラスをOFFにすればバグになりにくい(クリアしたのに死亡...が発生しにくい)
  • ダメージが通ったかどうかはコールバックで取得(メソッド経由だとアニメーションとの非同期がしんどい)

UniRxって便利なの?

便利ですがUniRxで全てを片づけるのは混乱を招くのでNOです。(UniRxが出てる時点で初心者なんて関係なくなってますね。ごめんなさい)

  • UniRxを使うなら必ず名前にAsObservableを付ける(Subscribe忘れ防止です)
  • AddToを使いましょう(メモリリーク対策)

UniRxのメリットとして、拡張すると色々便利なのです。

Observable.Using(() => guard.OverlayGuard.Guard("Loading"),
                _ => Observable.Timer(TimeSpan.FromSeconds(5))).Subscribe();

これは実際に使っているコードです。
UniRxを知らない人からみたら意味不明ですが、慣れるとすぐに分かるようになります。
このコードを実行すると、5秒間の間はローディング画面が出てタップや入力が出来なくなります。(DeNAさんのブログに載ってたものを改造しました)
そして5秒経つと自動的に解除されます。
もともとUsingというメソッドはUniRxにありません。でもちょっと作るだけでこんな不思議なコードが書けるのです。

一から組むならUniRxを導入してみたら良いと思いますし、既存のコードであれば下手に組み込まない方が良いかと思います。とくにチーム作業なら尚更ですね。
Usingはこれです。参考のQiita記事

public static partial class Observable
    {
        public static IObservable<TSource> Using<TSource, TResource>(
            Func<TResource> resourceFactory,
            Func<TResource, IObservable<TSource>> observableFactory)
            where TResource : IDisposable
        {
            return Observable.Create<TSource> (observer =>
            {
                var source = default(IObservable<TSource>);
                var disposable = Disposable.Empty;
                try
                {
                    var resource = resourceFactory();
                    if (resource != null)
                    {
                        disposable = resource;
                    }
                    source = observableFactory(resource);
                }
                catch (Exception exception)
                {
                    return new CompositeDisposable(Throw<TSource>(exception).Subscribe(observer), disposable);
                }

                return new CompositeDisposable(source.Subscribe(observer), disposable);
            });
        }
    }