Unityにおける継承と合成の使い分け:技術資料

2024年11月26日

概要

Unityはコンポーネントベースの設計思想を採用しており、オブジェクトに機能を付与する方法として「継承」と「合成」の両方が利用可能です。本資料では、それぞれの利点と欠点を解説し、適材適所で使い分けるための指針を示します。


継承と合成の違い

要素継承合成
関係性is-a(基本クラスの一種)has-a(機能の組み合わせ)
柔軟性階層が深くなると低下必要なコンポーネントだけで柔軟
再利用性再利用には派生クラス作成が必要そのまま使い回しが可能
保守性基本クラス変更の影響が大きい各コンポーネントが独立
適用場面共通ロジックの継承やポリモーフィズムに最適機能単位で構成する柔軟な設計に最適

継承の利点と適用場面

利点

「is-a」関係の表現

  • 派生クラスが基本クラスの一種である場合、継承を使用すると直感的な設計が可能。
  • 例: 「犬」は「動物」の一種 (Dog : Animal)。

コードの共通化

  • 基本クラスに共通のロジック(メンバー)を定義することで、コードを再利用できます。

ポリモーフィズムの活用

  • 基本クラスを通じて派生クラスの異なる振る舞いを共通化できます。
  • 例: Animal.Speak() を派生クラスごとに異なる実装で上書き (Dog.Speak()Cat.Speak())。

安定したドメインに有効

  • 基本クラスの仕様が変更されにくい場合、継承による設計がシンプルで効果的です。

適用場面

  • クラスの内容が確定されている場合
    仕様変更が少なく、基本クラスがしっかり設計されているケース。
  • ポリモーフィズムを活用する場合
    同じインターフェースを持つ複数の派生クラスを操作する場面。
  • 「is-a」関係が明確な場合
    派生クラスが基本クラスの一種であることが自明な場合。

継承を使った例

注意:Updateメソッドを省略しているため、このままのコードでは実行できません

BaseCharacter.cs

using UnityEngine;

public class BaseCharacter : MonoBehaviour
{
    public float speed = 5f;

    public virtual void Move()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        transform.Translate(new Vector3(h, 0, v) * speed * Time.deltaTime);
    }
}

PlayerCharacter.cs

using UnityEngine;

public class PlayerCharacter : BaseCharacter
{
    public float jumpForce = 5f;

    public override void Move()
    {
        base.Move(); // 基底クラスの移動処理を呼び出す

        if (Input.GetButtonDown("Jump"))
        {
            Jump();
        }
    }

    private void Jump()
    {
        GetComponent<Rigidbody>().AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
    }
}

問題点

  • 基本クラスの仕様変更が派生クラスに影響を与えやすい。
  • 階層が深くなるとコードの理解や保守が難しくなる。

合成の利点と適用場面

利点

柔軟性

  • 機能を独立したコンポーネントとして実装することで、必要な機能を自由に組み合わせられます。

再利用性

  • 各コンポーネントを他のゲームオブジェクトにも簡単に使い回せます。

単一責任の原則(SRP)

  • 各コンポーネントが1つの責任(例: 移動、ジャンプ)にのみ集中しているため、コードがシンプルで理解しやすくなります。

保守性

  • コンポーネントが独立しているため、他のスクリプトに影響を与えずに変更や追加が可能です。

適用場面

  • 多様な機能を持つオブジェクトを設計する場合
    必要な機能だけを柔軟に組み合わせたい場面。
  • プロジェクトが大規模化する場合
    各機能を独立させて管理した方が保守が容易になるケース。
  • ドメインが頻繁に変化する場合
    仕様が変わりやすい機能を独立して実装することで、変更の影響を最小限に抑えます。

合成を使った例

MoveComponent.cs

using UnityEngine;

public class MoveComponent : MonoBehaviour
{
    public float speed = 5f;

    void Update()
    {
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        transform.Translate(new Vector3(h, 0, v) * speed * Time.deltaTime);
    }
}

JumpComponent.cs

using UnityEngine;

[RequireComponent(typeof(Rigidbody))]
public class JumpComponent : MonoBehaviour
{
    public float jumpForce = 5f;

    void Update()
    {
        if (Input.GetButtonDown("Jump"))
        {
            Jump();
        }
    }

    private void Jump()
    {
        GetComponent<Rigidbody>().AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
    }
}

使い方

  1. プレイヤーオブジェクトに MoveComponentJumpComponent をアタッチします。
  2. 必要に応じて他のオブジェクトにも追加可能です(例: 敵キャラクターが移動だけを持つなど)。

結論

「継承」と「合成」はどちらも強力な設計手法ですが、それぞれ得意な場面が異なります。Unityのコンポーネントベース設計では、特に合成が推奨されるケースが多いですが、適切な場面では継承も十分に活用できます。

設計の指針

  • 明確な「is-a」関係がある場合は継承
  • 柔軟性や拡張性が必要な場合は合成
  • プロジェクトの要件に応じて、両者を併用することで最適な設計を実現します。