🫥

(初学者向け)Unityで汚いコードを減らすための一歩目の知識

2023/12/08に公開

今回は、初心者向けに、Unityで汚いコードを減らすためにまず知っておきたい知識について。

SerializeField

UnityのInspector上で値やGameObject等を編集・参照したいことはよくあると思います。
そんな時、以下のようなコードが考えられます。

using UnityEngine;

public class Player : MonoBehaviour
{
    public int hp;
}

このとき、hpは、実際にInspector上で編集できます。

しかし、この値はpublicとなっているため、このクラスを参照している全てのクラスがこの変数(以下フィールド)にアクセスできてしまいます。

「別に良くね?」となっている方は特に大丈夫ですが、一度でもpublicに不用意にアクセスして値を改ざんし、とんでもないスパゲッティコード状態になった経験がありましたら、この状況を避けたいと思うことでしょう。

その1つのアプローチとして、SerializeFieldがあります。

 using UnityEngine;

 public class Player : MonoBehaviour
 {
-    public int hp;
+    [SerializeField] private int hp; // privateの記述は任意
 }

上記のように書くと、アクセス修飾子はprivateにしながら、同様にInspectorで編集できるようになります!

これで他クラスから不要にアクセスされる危険性を減らすことが出来ました!

基本的に、不用意なアクセスを防ぐため、フィールドはprivateで定義すべきです

では、どうしてもアクセスしたい場合はどうするべきでしょうか。その答えは以下に。

フィールドに聞くな、メソッドに聞け

他クラスから先ほどのhpの加減を操作したいとき、どのように実装するのが安全でしょうか?

最も簡単なのは、publicのまま使う手法です。

using UnityEngine;

public class Player : MonoBehaviour
{
    public int hp = 15;
}

public class Enemy : MonoBehaviour
{
    private void OnCollisionEnter(Collision other)
    {
        // もしPlayerと衝突したら
        if (other.gameObject.TryGetComponent(out Player player))
        {
            player.hp -= 10;
        }
    }
}

ですが、これは面倒な事態を引き起こします。例えば、hpが0以下ならPlayerをDestroy()したいとしましょう。すると、コードは以下のようになります。

 using UnityEngine;

 public class Enemy : MonoBehaviour
 {
     private void OnCollisionEnter(Collision other)
     {
         // もしPlayerと衝突したら
         if (other.gameObject.TryGetComponent(out Player player))
         {
             player.hp -= 10;
+ 	     if (player.hp <= 0)
+ 	     {
+ 	         Destroy(player.gameObject);
+ 	     }
         }
     }
 }

他にもPlayerにダメージを与えるオブジェクトを実装したくなった場合、またこのコードを書かないといけなくなります。一個でも書き漏れがあるとバグになります。

また、hpが0以下のときゲームオーバー画面に移りたかったり、エフェクトを出したりしたくなったら、またこういう処理を書いたコード全てに加筆する必要が出てきます。

最大hpの概念を表現したくなったら、またEnemy等のコードが肥大化します…

つらい
using UnityEngine;

public class Player : MonoBehaviour
{
    public int hp = 15;
    // 最大体力
    public int maxHp = 15;
}

public class Enemy : MonoBehaviour
{
    private void OnCollisionEnter(Collision other)
    {
        // もしPlayerと衝突したら
        if (other.gameObject.TryGetComponent(out Player player))
        {
            player.hp -= 10;
            if (player.hp <= 0)
            {
                player.hp = 0;
                Destroy(player.gameObject);
                Debug.Log("game over");
            }
        }
    }
}

public class HealthPotion : MonoBehaviour
{
    private void OnCollisionEnter(Collision other)
    {
        // もしPlayerと衝突したら
        if (other.gameObject.TryGetComponent(out Player player))
        {
            player.hp += 10;
	    // hpの最大値をmaxHp以下に調整する
            if (player.hp > player.maxHp)
            {
                player.hp = player.maxHp;
            }
            Destroy(gameObject);
        }
    }
}

こうなってくると、バグだらけになりがちです。

この問題を解決するために、「フィールドに聞くな、メソッドに聞け」を実行してみましょう。

まずは、hpとmaxHpをprivateにし、hpを取得するためのメソッドを用意します。これによって、外部のクラスはフィールドに聞かず、メソッドに聞くことを強制されます。

using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField] private int hp = 15;
    [SerializeField] private int maxHp = 15;
    
    /// <summary>
    /// Hpを取得します。
    /// </summary>
    public int GetHp()
    {
        return hp;
    }
}

GetHp()メソッドを利用することで、hpの中身を取得できるようになりました。
この要領で、AddHp(int)も実装してみます。hpの加減を実行するメソッドです。

 using UnityEngine;

 public class Player : MonoBehaviour
 {
     [SerializeField] private int hp = 15;
    
     /// <summary>
     /// Hpを取得します。
     /// </summary>
     public int GetHp()
     {
         return hp;
     }

+    /// <summary>
+    /// Hpを設定します。
+    /// </summary>
+    public void AddHp(int value)
+    {
+        hp += value;
+    }
 }

さて、ここからがメソッドの本領発揮です。メソッドを使えば、この内部で色んな処理を記入できます!

先ほどの条件を全部追加してみましょう!

  • hpが0以下ならDestroy()、"game over"と表示
  • hpはmaxHpを超えない
 using UnityEngine;

 public class Player : MonoBehaviour
 {
     [SerializeField] private int hp = 15;
     private int maxHp = 15;

     /// <summary>
     /// Hpを取得します。
     /// </summary>
     public int GetHp()
     {
         return hp;
     }

     /// <summary>
     /// Hpを設定します。
     /// </summary>
     public void AddHp(int value)
     {
         hp += value;
+        if (hp <= 0)
+        {
+            hp = 0;
+            Destroy(gameObject);
+            Debug.Log("game over");
+        }
+        else if (hp > maxHp)
+        {
+            hp = maxHp;
+        }
     }
 }

こうすれば、外部はhpの内部的な処理を気にすることなく、メソッドを活用することができます!

 using UnityEngine;
 
 public class Enemy : MonoBehaviour
 {
     private void OnCollisionEnter(Collision other)
     {
         // もしPlayerと衝突したら
         if (other.gameObject.TryGetComponent(out Player player))
         {
-            player.hp -= 10;
-            if (player.hp <= 0)
-            {
-                player.hp = 0;
-                Destroy(player.gameObject);
-                Debug.Log("game over");
-            }
 
+            player.AddHp(-10);
         }
     }
 }

 public class HealthPotion : MonoBehaviour
 {
     private void OnCollisionEnter(Collision other)
     {
         // もしPlayerと衝突したら
         if (other.gameObject.TryGetComponent(out Player player))
         {
-            player.hp += 10;
-            if (player.hp > player.maxHp)
-            {
-                player.hp = player.maxHp;
-            }
 
+            player.AddHp(10);
             Destroy(gameObject);
         }
     }
 }

これで同じ処理を何度も繰り返さず、気軽にhpを参照できるようになりました!

なお、もう1ランク上の記法として、プロパティというものが存在します。これはまた機会があったら紹介します。

他クラスの取得は[SerializeField]かGetComponent()

他クラスを参照する方法が分からなくて、以下のようなコードを書いていませんか?

public class StageManager : MonoBehaviour
{
    private void Start()
    {
        GameObject player = GameObject.Find("Player");
	Instantiate(player);
    }
}

GameObject.Find()は、処理速度も遅いですし、Playerの名前を変更するだけでバグってしまう、かなり不安定なメソッドです。

stringで参照する系のメソッドには、大抵もっといい方法があります。

今回は、先ほど紹介した[serializeField]で他クラスを参照してみましょう。これだけで、playerのGameObjectを参照できました。

 public class StageManager : MonoBehaviour
 {
+    [SerilaizeField] private GameObject player;   
+ 
     private void Start()
     {
-        GameObject player = GameObject.Find("Player");
         Instantiate(player);
     }
 }

同じGameObjectにアタッチされているクラス(コンポーネント)であれば、GetComponent<T>()を使う方法もあります。Tにクラス名を入れて使いましょう。

 public class StageManager : MonoBehaviour
 {
     private void Start()
     {
-        GameObject player = GameObject.Find("Player");
+        GameObject player = GetComponent<Player>();
         Instantiate(player);
     }
 }

なお、GetComponent<T>()はちょっと重たいコードなので、できるだけUpdate()等で使用しないようにしましょう!

Awake()Start()にて変数に格納しておいて、それを使うのが良い実装例です。

public class StageManager : MonoBehaviour
{
    private GameObject player;

    private void Start()
    {
        player = GetComponent<Player>(); // Start()でキャストしておく
        Instantiate(player);
    }
    
    private void Update()
    {
      GetComponent<Player>().何かしらのメソッド() // これは重いのでやめた方がいいかも!
    
        player.何かしらのメソッド() // これは比較的重くない(何かしらのメソッドが重かったら重いけど…)
    }
    
}

GameObjectで参照しない

そもそも、GameObjectで参照すること自体が良くない場合も多いです。参照をGameObjectにしてしまうと、クラスが制限されていなさすぎて、何でも入れることが出来てしまうため、思わぬエラーに繋がる可能性が上がります。

 public class StageManager : MonoBehaviour
 {
-    [SerilaizeField] private GameObject player;   
+    [SerializeField] private Player player; // ここにはPlayerクラスのついたコンポーネントやGameObjectしか付けられない!

     private void Start()
     {
         Instantiate(player);
     }
 }
 
+public class Player : MonoBehaviour
+{
+    // 別に急いで何か書かなくてもいい。クラスを用途別に区別できるようにすることが大事。
+}

Discussion