Chapter 03

一般的なコーディングについて (General Coding)

sh1ch
sh1ch
2020.09.23に更新

一般的なコーディングについて (General Coding)

9. すべてのコードを名前空間に入れること

これにより、独自のライブラリーとサードパーティのコートとの衝突を避けることができます。しかし、重要なクラスとの衝突を避ける目的で名前空間を頼らないこと。異なる名前空間を使用する場合であっても、クラスの名前を「Object」、「Action」、「Event」のようにしないでください。

後ろはリーダブルコードのような内容の指摘だと思います。

10. Assert を使うこと

アサーションは、コードの実行結果が変わらないことをテストしたり、ロジックのバグを洗い出したりするのに便利な機能です。

Unity では「Unity.Assertions.Assert」を利用できます。これらはなんらかの条件のテストをして、条件を満たさなかった場合、コンソールにエラーメッセージを表示します。

アサーション(というよりも、テスト)がどのように役に立つのかをよく知らない場合は、「The Benefits of programming with assertions(リンク切れ)」を参照してください。

11. 表示されるテキスト以外の文字列は使用しない

特に、オブジェクトや Prefab の識別子としてテキストをそのまま使わないこと。例外もあります。(Unity では名前でしかアクセスできないものが、まだいくつかあります)そのような場合は、 AnimationNames や AudioModuleNames のようにファイルで文字列を定数として定義してください。これらのクラスが管理しきれなくなった場合は、入れ子になったクラスを使用して、AnimationNames.Player.Run のように定義します。

旧 Tips 34 と同じ内容だと思います。

12. Invoke と SendMessage を使わないこと

MonoBehaviour のこれらのメソッドは、名前をつけて他のメソッドを呼び出します。テキストの名前で呼び出されるメソッドはコードで追跡するのが難しいです。(Usages を見つけることができないし、SendMessage は範囲が広いので、さらに追跡できない)

コルーチンや C# のアクションを使って、独自の Invoke を簡単に用意できます:

public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time)
{
   return monoBehaviour.StartCoroutine(InvokeImpl(action, time));
}

private static IEnumerator InvokeImpl(Action action, float time)
{
   yield return new WaitForSeconds(time);
   
   action();
}

MonoBehavior から、次のように使うことができます:

this.Invoke(ShootEnemy); //ShootEnemy メソッドの引数はなし (void)

独自に MonoBehaviour を継承する基底クラスを実装する場合は、そこに独自の Invoke メソッドを追加することができます。

旧 Tips 21 と同じ内容です。

より安全な SendMessage の代替を実装するのは、難しいです。その代わりとして、通常は GetComponent を使って、対象のコンポーネントを取得し、直接呼び出すようにします。

補足として、Unity の「ExecuteEvent」についての提案が挙がっています。今のところ、よく調べられていませんが調査する価値がありそうです。

13. ゲーム実行中に生成したオブジェクトがヒエラルキーをごちゃごちゃにしないこと

親オブジェクトをシーンオブジェクトに設定することで、ゲームの実行中にオブジェクトを見つけやすくします。

コードからのアクセスをしやすくするために、空の Game Object や、Behaviour を持たない Singleton クラスを使用することもできます。

こうしたオブジェクトのことを DynamicObjects と呼びます。

旧 Tips 28 と同じような内容です。

14. 正しい値として null を使用するときは具体的にして、可能な限りそれを避けること

null は不正なコードを検出するための役にたちます。しかし、もし、null を暗黙的に渡すことを習慣にしてしまうと、不正なコードは楽々実行されるようになり、また、そのバグに気づくのはかなり後になります。

さらに、それぞれのレイヤーが null の値を渡すことで、コードの深い部分でそれがはっきりすることもあります。私は、null を正しい値として使用することを完全に避けるようにしています。

私の好ましいイディオムは null の値のチェックを一切行わず、問題のあるところではコードを失敗させることです。より深いレベルのインターフェースとして機能するメソッドでは、値が null であるかどうかをチェックし、それが失敗する可能性のある他のメソッドに渡す代わりに例外を投げます。

場合によっては、正しい値が null になることがあり、別のやり方で対処する必要があります。このような場合は、コメントを追加して、いつ、なぜ、値が null になっているのかを説明するコメントを追加してください。

よくあるシナリオ(ケース)では、インスペクターが設定した値を使用することです。ユーザーは値を指定することができますが、ユーザーが指定しない場合はデフォルト値が使用されます。

これより好ましい方法は、T の値をラップする Optional<T> クラスを使用することです。(Nullable<T> とすこし似てる)

特別なプロパティのレンダラーを使って、チェックボックスをレンダリングして、チェックが入っている場合にのみ、値を編集するボックスを表示するようにします。(残念ながら、ジェネリッククラスを直接使うことはできないので、T を特定する型にクラスを拡張しないとダメです)

[Serializable]
public class Optional<T>
{
   public bool useCustomValue;
   public T value;
}

コードでは次のように使用することができます:

health = healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;

補足として、これは多くのコメントで struct を使ったほうがよいという指摘がありました。ただし、これだと、非ジェネリッククラスのベースクラスとしては使えないということなので、実際にインスペクターで使えるフィールドに適します。

15. コルーチンを使うなら効率的な使い方を学ぶ

コルーチンは多くの問題を解決する強力な手段になりえます。しかし、デバッグが難しく、(自分を含めた)誰もが理解できないよくわからないコードを簡単につくることができます。

知っておくべきことは:

  • コルーチンを並列に実行する方法
  • コルーチンを連続して実行する方法
  • 既存のコルーチンから新しいコルーチンを作成する方法
  • CustomYieldInstruction を使ったカスタムコルーチンの作成方法
Enumerator RunInSequence()
{
   yield return StartCoroutine(Coroutine1());
   yield return StartCoroutine(Coroutine2());
}

public void RunInParallel()
{
   StartCoroutine(Coroutine1());
   StartCoroutine(Coroutine1());
}

Coroutine WaitASecond()
{
   return new WaitForSeconds(1);
} 

16. インターフェースを共有するコンポーネントを操作するときは拡張メソッドを利用する

今は、GetComponent はインターフェースも動作するようになったので、この Tips は冗長になっています。

特定のインターフェースを実装したコンポーネントを取得したり、そんなコンポーネントを持つオブジェクトを見つけるときに便利です。

以下の実装では、これの汎用的な実装に typeof を使用しています。ジェネリック版だと、インターフェースは動作しないけど、typeof は動作します。以下のメソッドは、これを通常のメソッドでラップしています。

public static TInterface GetInterfaceComponent<TInterface>(this Component thisComponent)
   where TInterface : class
{
   return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}

17. 拡張メソッドを使用して構文をより簡便にする

たとえば、つぎのようなもの:

public static class TransformExtensions 
{
   public static void SetX(this Transform transform, float x)
   {
      Vector3 newPosition = 
         new Vector3(x, transform.position.y, transform.position.z);
 
      transform.position = newPosition;
   }
   ...
}

旧 Tips 24 と同じような内容です。

18. 防御的な GetComponent の代替メソッドを使うこと

RequireComponent を使用してコンポーネントの依存関係を強制しても、他のクラスで GetComponent を呼び出す場合は、常に取得可能であるとは限らないし、望ましいことでもないです。RequireComponent を使用する場合であっても、コンポーネントを取得するコードの中で、コンポーネントが存在することを期待しているので、存在しない場合はエラーであることを示すとよいです。

エラーメッセージを表示するか、見つからなかった場合に役立つ例外をスローする拡張メソッドを用意します。

public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour
{
   T component = obj.GetComponent();
 
   if(component == null)
   {
      Debug.LogError("Expected to find component of type " 
         + typeof(T) + " but found none", obj);
   }
 
   return component;
}

旧 Tips 25 と同じような内容です。

19. 同じことをするのに異なるイディオムを使うのは避ける

多くの場合、ひとつのものに複数のイディオム(慣用句)があります。そんな場合、プロジェクト全体で使う慣用句をひとつ選びましょう。その理由は:

  • イディオム同時はうまく働かない。ひとつのイディオムを使うと別のイディオムには適していない方向にデザインを強制されます。
  • 全体をとおして同じイディオムを使うことで、チームメンバーは、そこでなにをしているのか理解しやすくなります。構造・コードが理解しやすくなって、ミスをしづらくなります。

同じ意味のイディオムになるケースを挙げると:

  • Coroutine と State Machine
  • Nested Prefab と Lined Prefab と God Prefab
  • データを分離するための手法
  • 2D ゲームのスプライトの使用方法
  • Prefab の構造
  • スポーンするときのやり方
  • オブジェクト見つけるための方法:type, name, tag, layer, reference などいろんなやり方がある
  • オブジェクトをグループ化する方法:type, name, tag, layer, reference などいろんなやり方がある
  • 他のコンポーネントからメソッドを呼び出す方法(Tips 12 みたいなこと)
  • オブジェクトのグループを見つける方法とグループに登録する方法(Tips 13 みたいなこと)
  • 実行順序の制御
  • ゲーム内でのマウスによるオブジェクト・位置・ターゲットの選択
  • シーン変更の間にデータを保持する方法:PlayerPrefab を介して、または、新しいシーンがロードされたときに Destroy されないオブジェクト
  • アニメーションを組み合わせる方法
  • 入力のやり方

これだけ聞くと、半端に UniRx を使うのって微妙な気持ちになる気も。学習不足でコードをあまり追えない状態で利用すると、いくつかの Tips に反してしまうけど、前書きのリファクタリングに関する説明がそれを肯定する関係に思った。

20. ポーズを簡単にするために独自の時間クラスを準備する

ポーズとタイムスケール(遅くしたりする)をするために Time.DeltaTimeTime.TimeSinceLevelLoad をラップしておきます。これは厳格さを必要とするけど、特に異なるタイマーを管理している場合は物事を簡単にします。(インターフェースのアニメーションとゲームのアニメーション速度が別々になるときなど)

補足として、Unity は unscaledTimeunscaledDeltaTime をサポートしており、多くの状況で独自の時間クラスを持つことは冗長になります。Tips 21 のようなケースの場合はまだ便利です。

旧 Tips 20 と同じ内容だと思います。

21. 更新を必要とするカスタムクラスはグローバルな静的時間にアクセスしないこと

更新を必要とするカスタムクラスはグローバルな静的時間にアクセスするべきではないです。

その代わりに、Update メソッドのパラメーターに delta time をとる必要があります。上の Tips 20 で説明したように、ポーズの機能を実装する場合や、カスタムクラスの動作を高速化したり遅くしたりする場合に、このクラスを使用することができる。

22. WWW を使うときは、共通のやり方にする

サーバー通信の多いゲームでは、数十もの WWW のコールがあるものです。

Unity の用意した WWW クラスを使用する場合でも、プラグインを使用する場合でも、その上にひな形 (boiler plate) になるレイヤーを入れることで便利になります。

通常だと、Call メソッド(Get と Post をひとつずつ)、CallImpl コルーチン、MakeHandler を定義します。基本的に、Call メソッドは Make Handler メソッドを使用して、パーサー・成功時と失敗時のハンドラを構築します。また、CallImpl コルーチンを呼び出し、URL を入れて、呼び出し、完了するまで待機してからスーパーハンドラを呼び出します。

大まかにはこんな感じ:

public void Call<T>(string call, Func<string, T> parser, Action<T> onSuccess, Action<string> onFailure)
{
	var handler = MakeHandler(parser, onSuccess, onFailure);
	StartCoroutine(CallImpl(call, handler));
} 

public IEnumerator CallImpl<T>(string call, Action<T> handler)
{
	var www = new WWW(call);
	yield return www;
	handler(www);
}

public Action<WWW> MakeHandler<T>(Func<string, T> parser, Action<T> onSuccess, Action<string> onFailure)
{
   return (WWW www) =>
   {
      if(NoError(www)) 
      {
         var parsedResult = parser(www.text);
         onSuccess(parsedResult);
      }
      else
      {
         onFailure("error text");
      }
   }
}

これには、いくつかのメリットがあります。

  • 定型的なコードを書く必要がなくなります。
  • 特定のこと(読み込み中の UI コンポーネントの表示や、特定の一般的なエラー処理など)をまとめて処理することができます。

23. 文字数の多いテキストは、ファイルにする

インスペクターで編集するフィールドに入れないでください。

Unity エディターを開かなくても、シーンを保存しなくても、簡単に変更できるようにしておきましょう。

旧 Tips 38 と同じ内容だと思います。

24. ローカライズを計画しているなら、すべての文字列をひとつのところに配置します

このやりかたはたくさんあります。ひとつの方法は、各文字列のpublic string のフィールドを持つ Text クラスを定義して、デフォルトを英語に設定しておくことです。他の言語は、これを基底としたサブクラスにして、対応する言語でフィールドを再設定します。

より洗練されたテクニック(テキストの本文が長い or 多くの言語に対応する)は、スプレッドシートを読み込んで、選択した言語に基づいて正しい文字列を選択するロジックを提供することです。

旧 Tips 39 と同じ内容だと思います。