mcpField
U
Unity Architecture

Unity Architecture

github.com/cryptorabea開発3142 回インストール45 週間前に更新
リポジトリ

Unity Architecture について

保守しやすい Unity プロジェクト向けのアーキテクチャパターンを扱うスキルです。Unity ゲーム開発者がシステム、データ、通信を整理し、テストしやすく疎結合なコンポーネントを設計できます。

主な機能

  1. 01マネージャーとサービスロケーターパターン
  2. 02ScriptableObject によるデータ設計
  3. 03イベント駆動型の通信パターン
  4. 04コンポーネント構成とステートマシン
  5. 05MVC/MVP による分離

活用シーン

  1. 01ゲーム全体のシステム構成
  2. 02シーン間でのゲームデータ共有
  3. 03ゲームロジックと UI コンポーネントの分離

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/cryptorabea/claude_unity_dev_plugin --skill unity-architecture

インストール後、Claude Code で/unity-architectureを入力して読み込まれたか確認します。

同梱ファイルreferences/manager-patterns.mdreferences/scriptableobject-architecture.md

詳細

Unity ゲームアーキテクチャ

スケーラブルで保守しやすい Unity プロジェクトに欠かせないアーキテクチャパターンと設計原則です。

概要

優れたアーキテクチャは関心事を分離し、結合度を下げ、コードのテストと保守を容易にします。このスキルでは、Unity ゲーム開発で実績のあるパターンを紹介します。

アーキテクチャの主な概念:

  • マネージャーパターンとグローバルシステム
  • ScriptableObject ベースのデータアーキテクチャ
  • イベント駆動型の通信
  • コンポーネント合成パターン
  • 依存関係の管理

マネージャーパターン

ゲーム全体の機能を調整する集中管理型システムです。

シングルトンマネージャー

グローバルマネージャーで最も一般的なパターンです。

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public void StartGame() { }
    public void PauseGame() { }
    public void EndGame() { }
}

// Access from anywhere
public class Player : MonoBehaviour
{
    private void Start()
    {
        GameManager.Instance.StartGame();
    }
}

使用する場面:

  • ゲーム状態の管理(GameManager)
  • オーディオの管理(AudioManager)
  • 入力の管理(InputManager)
  • セーブ/ロードシステム(SaveManager)
  • UI の管理(UIManager)

使用しない場面:

  • あらゆるもの(「シングルトン地獄」を避けてください)
  • 一時的なシステム
  • 複数のインスタンスが必要なシステム
汎用シングルトン基底クラス

再利用可能なシングルトンパターンです。

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<T>();

                if (instance == null)
                {
                    GameObject singleton = new GameObject(typeof(T).Name);
                    instance = singleton.AddComponent<T>();
                    DontDestroyOnLoad(singleton);
                }
            }

            return instance;
        }
    }

    protected virtual void Awake()
    {
        if (instance == null)
        {
            instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else if (instance != this)
        {
            Destroy(gameObject);
        }
    }
}

// Usage
public class GameManager : Singleton<GameManager>
{
    protected override void Awake()
    {
        base.Awake();
        // Additional initialization
    }
}
マネージャーの初期化順序

マネージャーの初期化を制御します。

// Use Script Execution Order:
// Edit > Project Settings > Script Execution Order

// Or explicit initialization
public class GameBootstrap : MonoBehaviour
{
    private void Awake()
    {
        InitializeManagers();
    }

    private void InitializeManagers()
    {
        // Initialize in specific order
        var saveManager = SaveManager.Instance;
        var audioManager = AudioManager.Instance;
        var gameManager = GameManager.Instance;

        // Managers initialize in Awake, but access here ensures order
    }
}

ベストプラクティス:明示的な初期化シーンまたはブートストラッパーを使用してください。

サービスロケーターパターン

依存性注入におけるシングルトンの代替手段です。

public class ServiceLocator
{
    private static ServiceLocator instance;
    public static ServiceLocator Instance => instance ?? (instance = new ServiceLocator());

    private readonly Dictionary<Type, object> services = new Dictionary<Type, object>();

    public void RegisterService<T>(T service)
    {
        services[typeof(T)] = service;
    }

    public T GetService<T>()
    {
        if (services.TryGetValue(typeof(T), out var service))
        {
            return (T)service;
        }

        throw new Exception($"Service {typeof(T)} not found");
    }
}

// Register services
public class GameBootstrap : MonoBehaviour
{
    private void Awake()
    {
        var audioManager = new AudioManager();
        ServiceLocator.Instance.RegisterService(audioManager);

        var saveManager = new SaveManager();
        ServiceLocator.Instance.RegisterService(saveManager);
    }
}

// Access services
public class Player : MonoBehaviour
{
    private void Start()
    {
        var audio = ServiceLocator.Instance.GetService<AudioManager>();
        audio.PlaySound("Jump");
    }
}

シングルトンより優れている点:

  • テスト可能(モックサービスを注入できます)
  • 静的な依存関係がない
  • 依存関係が明確

欠点:

  • セットアップコードが増える
  • 実行時に辞書を検索する
  • 発見しにくい

ScriptableObject アーキテクチャ

ScriptableObject を使用したデータ駆動型設計です。

ScriptableObject データコンテナー

振る舞いとは分けてデータを保存します。

[CreateAssetMenu(fileName = "WeaponData", menuName = "Game/Weapon Data")]
public class WeaponData : ScriptableObject
{
    public string weaponName;
    public int damage;
    public float fireRate;
    public Sprite icon;
    public GameObject projectilePrefab;
}

public class Weapon : MonoBehaviour
{
    [SerializeField] private WeaponData data;

    public void Fire()
    {
        Instantiate(data.projectilePrefab, firePoint.position, firePoint.rotation);
    }

    public int GetDamage() => data.damage;
}

メリット:

  • データとコードを分離できる
  • シーン間でデータを共有できる
  • コードを変更せずに編集できる
  • デザイナーにとって扱いやすい

用途:

  • アイテム/武器のステータス
  • キャラクターデータ
  • ゲームバランスの値
  • 設定
ScriptableObject イベント

ScriptableObject を使用したイベントシステムです。

[CreateAssetMenu(fileName = "GameEvent", menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
    private readonly List<GameEventListener> listeners = new List<GameEventListener>();

    public void Raise()
    {
        for (int i = listeners.Count - 1; i >= 0; i--)
        {
            listeners[i].OnEventRaised();
        }
    }

    public void RegisterListener(GameEventListener listener)
    {
        if (!listeners.Contains(listener))
            listeners.Add(listener);
    }

    public void UnregisterListener(GameEventListener listener)
    {
        listeners.Remove(listener);
    }
}

public class GameEventListener : MonoBehaviour
{
    [SerializeField] private GameEvent gameEvent;
    [SerializeField] private UnityEvent response;

    private void OnEnable()
    {
        gameEvent.RegisterListener(this);
    }

    private void OnDisable()
    {
        gameEvent.UnregisterListener(this);
    }

    public void OnEventRaised()
    {
        response.Invoke();
    }
}

使用方法:

Create GameEvent asset: "OnPlayerDeath"
Attach GameEventListener to UI
Configure response: Show death screen
Raise event when player dies

メリット:

  • デザイナーが利用しやすい
  • インスペクター上で視覚的に接続できる
  • システム間の結合度を下げられる
  • イベントを再利用できる
ScriptableObject 変数

シーン間で共有する変数です。

public abstract class ScriptableVariable<T> : ScriptableObject
{
    [SerializeField] private T value;

    public T Value
    {
        get => value;
        set
        {
            this.value = value;
            OnValueChanged?.Invoke(value);
        }
    }

    public event Action<T> OnValueChanged;
}

[CreateAssetMenu(fileName = "IntVariable", menuName = "Variables/Int")]
public class IntVariable : ScriptableVariable<int> { }

[CreateAssetMenu(fileName = "FloatVariable", menuName = "Variables/Float")]
public class FloatVariable : ScriptableVariable<float> { }

使用方法:

public class Player : MonoBehaviour
{
    [SerializeField] private IntVariable playerHealth;

    public void TakeDamage(int damage)
    {
        playerHealth.Value -= damage;  // Updates all subscribers
    }
}

public class HealthUI : MonoBehaviour
{
    [SerializeField] private IntVariable playerHealth;
    [SerializeField] private Text healthText;

    private void OnEnable()
    {
        playerHealth.OnValueChanged += UpdateUI;
        UpdateUI(playerHealth.Value);
    }

    private void OnDisable()
    {
        playerHealth.OnValueChanged -= UpdateUI;
    }

    private void UpdateUI(int health)
    {
        healthText.text = $"Health: {health}";
    }
}

メリット:

  • シーンをまたいで保持される
  • 複数のリスナーに対応する
  • インスペクターで編集できる
  • 実行時の変更がすべての箇所に反映される

イベントシステム

コンポーネント間の疎結合な通信を実現します。

C# イベント

標準的な C# イベントパターンです。

public class Health : MonoBehaviour
{
    public event Action<int> OnHealthChanged;
    public event Action OnDeath;

    private int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        OnHealthChanged?.Invoke(health);

        if (health <= 0)
        {
            OnDeath?.Invoke();
        }
    }
}

public class HealthUI : MonoBehaviour
{
    [SerializeField] private Health playerHealth;

    private void OnEnable()
    {
        playerHealth.OnHealthChanged += UpdateHealthBar;
        playerHealth.OnDeath += ShowDeathScreen;
    }

    private void OnDisable()
    {
        playerHealth.OnHealthChanged -= UpdateHealthBar;
        playerHealth.OnDeath -= ShowDeathScreen;
    }

    private void UpdateHealthBar(int health) { }
    private void ShowDeathScreen() { }
}

重要:メモリリークを防ぐため、OnDisable で必ず登録解除してください。

UnityEvent

インスペクターから割り当て可能なイベントです。

public class Interactable : MonoBehaviour
{
    [SerializeField] private UnityEvent onInteract;
    [SerializeField] private UnityEvent<int> onScoreChanged;

    public void Interact()
    {
        onInteract?.Invoke();
    }

    public void AddScore(int points)
    {
        onScoreChanged?.Invoke(points);
    }
}

メリット:

  • インスペクターでデザイナーが設定できる
  • シンプルなインタラクションならコードが不要
  • 視覚的に接続できる

欠点:

  • C# イベントより遅い
  • コンパイル時のチェックがない
  • デバッグが難しい

用途:

  • シンプルなインタラクション
  • デザイナー主導のイベント
  • プロトタイピング
グローバルイベントバス

集中管理型のイベントシステムです。

public static class EventBus
{
    private static readonly Dictionary<Type, Delegate> eventTable = new Dictionary<Type, Delegate>();

    public static void Subscribe<T>(Action<T> handler)
    {
        if (eventTable.TryGetValue(typeof(T), out var existingHandler))
        {
            eventTable[typeof(T)] = Delegate.Combine(existingHandler, handler);
        }
        else
        {
            eventTable[typeof(T)] = handler;
        }
    }

    public static void Unsubscribe<T>(Action<T> handler)
    {
        if (eventTable.TryGetValue(typeof(T), out var existingHandler))
        {
            var newHandler = Delegate.Remove(existingHandler, handler);
            if (newHandler == null)
                eventTable.Remove(typeof(T));
            else
                eventTable[typeof(T)] = newHandler;
        }
    }

    public static void Publish<T>(T eventData)
    {
        if (eventTable.TryGetValue(typeof(T), out var handler))
        {
            (handler as Action<T>)?.Invoke(eventData);
        }
    }
}

// Event data types
public struct PlayerDiedEvent
{
    public Vector3 position;
    public string killedBy;
}

// Subscribe
public class DeathUI : MonoBehaviour
{
    private void OnEnable()
    {
        EventBus.Subscribe<PlayerDiedEvent>(OnPlayerDied);
    }

    private void OnDisable()
    {
        EventBus.Unsubscribe<PlayerDiedEvent>(OnPlayerDied);
    }

    private void OnPlayerDied(PlayerDiedEvent data)
    {
        ShowDeathScreen(data.position, data.killedBy);
    }
}

// Publish
public class Player : MonoBehaviour
{
    private void Die()
    {
        EventBus.Publish(new PlayerDiedEvent
        {
            position = transform.position,
            killedBy = "Enemy"
        });
    }
}

メリット:

  • 完全に疎結合
  • 直接参照が不要
  • 構造体を使えば型安全

欠点:

  • 実行時のオーバーヘッド(辞書検索)
  • イベントの流れを追跡しにくい
  • 登録解除を忘れるとメモリリークのリスクがある

コンポーネント合成パターン

ストラテジーパターン

切り替え可能な振る舞いです。

public interface IMovementStrategy
{
    void Move(Transform transform, float speed);
}

public class GroundMovement : MonoBehaviour, IMovementStrategy
{
    public void Move(Transform transform, float speed)
    {
        // Ground-based movement
    }
}

public class FlyingMovement : MonoBehaviour, IMovementStrategy
{
    public void Move(Transform transform, float speed)
    {
        // Flying movement
    }
}

public class Character : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    private IMovementStrategy movementStrategy;

    private void Awake()
    {
        movementStrategy = GetComponent<IMovementStrategy>();
    }

    private void Update()
    {
        movementStrategy.Move(transform, speed);
    }
}

メリット:

  • 実行時に振る舞いを切り替えられる
  • ストラテジーを再利用できる
  • オープン・クローズドの原則に従える
ステートマシンパターン

オブジェクトの状態を管理します。

public interface IState
{
    void Enter();
    void Execute();
    void Exit();
}

public class IdleState : MonoBehaviour, IState
{
    public void Enter() => Debug.Log("Entering Idle");
    public void Execute() { }
    public void Exit() => Debug.Log("Exiting Idle");
}

public class StateMachine : MonoBehaviour
{
    private IState currentState;

    public void ChangeState(IState newState)
    {
        currentState?.Exit();
        currentState = newState;
        currentState?.Enter();
    }

    private void Update()
    {
        currentState?.Execute();
    }
}

public class Enemy : MonoBehaviour
{
    private StateMachine stateMachine;
    private IdleState idleState;
    private ChaseState chaseState;

    private void Awake()
    {
        stateMachine = GetComponent<StateMachine>();
        idleState = GetComponent<IdleState>();
        chaseState = GetComponent<ChaseState>();

        stateMachine.ChangeState(idleState);
    }

    public void OnPlayerSpotted()
    {
        stateMachine.ChangeState(chaseState);
    }
}
オブザーバーパターン

1 対多の通知を実現します。

public interface IObserver
{
    void OnNotify(string eventType);
}

public class Subject : MonoBehaviour
{
    private readonly List<IObserver> observers = new List<IObserver>();

    public void Attach(IObserver observer)
    {
        if (!observers.Contains(observer))
            observers.Add(observer);
    }

    public void Detach(IObserver observer)
    {
        observers.Remove(observer);
    }

    protected void Notify(string eventType)
    {
        for (int i = observers.Count - 1; i >= 0; i--)
        {
            observers[i].OnNotify(eventType);
        }
    }
}

public class Player : Subject
{
    public void Jump()
    {
        Notify("PlayerJumped");
    }
}

public class AudioObserver : MonoBehaviour, IObserver
{
    [SerializeField] private Player player;

    private void OnEnable()
    {
        player.Attach(this);
    }

    private void OnDisable()
    {
        player.Detach(this);
    }

    public void OnNotify(string eventType)
    {
        if (eventType == "PlayerJumped")
            PlayJumpSound();
    }
}

Unity における MVC/MVP

モデル・ビュー・コントローラー

データ、表示、ロジックを分離します。

// Model - Data
public class PlayerModel
{
    public int Health { get; private set; } = 100;
    public int Score { get; private set; } = 0;

    public event Action<int> OnHealthChanged;
    public event Action<int> OnScoreChanged;

    public void TakeDamage(int damage)
    {
        Health -= damage;
        OnHealthChanged?.Invoke(Health);
    }

    public void AddScore(int points)
    {
        Score += points;
        OnScoreChanged?.Invoke(Score);
    }
}

// View - Presentation
public class PlayerView : MonoBehaviour
{
    [SerializeField] private Text healthText;
    [SerializeField] private Text scoreText;

    public void UpdateHealth(int health)
    {
        healthText.text = $"Health: {health}";
    }

    public void UpdateScore(int score)
    {
        scoreText.text = $"Score: {score}";
    }
}

// Controller - Logic
public class PlayerController : MonoBehaviour
{
    private PlayerModel model;
    private PlayerView view;

    private void Awake()
    {
        model = new PlayerModel();
        view = GetComponent<PlayerView>();

        model.OnHealthChanged += view.UpdateHealth;
        model.OnScoreChanged += view.UpdateScore;
    }

    private void OnDestroy()
    {
        model.OnHealthChanged -= view.UpdateHealth;
        model.OnScoreChanged -= view.UpdateScore;
    }

    public void TakeDamage(int damage)
    {
        model.TakeDamage(damage);
    }

    public void AddScore(int points)
    {
        model.AddScore(points);
    }
}

メリット:

  • テスト可能(モデル/ビューをモックできます)
  • コンポーネントを再利用できる
  • 明確に分離できる

使用する場面:

  • 複雑な UI
  • テスト可能なコードが必要な場合
  • 同じデータに対して複数のビューが必要な場合

その他のリソース

参考ファイル

詳しいアーキテクチャパターンについては、以下を参照してください:

  • references/manager-patterns.md - マネージャーの実装、初期化、通信
  • references/scriptableobject-architecture.md - 高度な SO パターン、ランタイムセット、変数
  • references/event-systems.md - イベントパターン、メッセージバス、パブリッシュ/サブスクライブシステム
  • references/design-patterns.md - Unity 向けのファクトリー、プール、コマンド、ストラテジーパターン

ベストプラクティス

✅ 実施すること:

  • グローバルシステム(オーディオ、入力、セーブ)にはマネージャーを使用する
  • データには ScriptableObjects を活用する
  • イベント駆動型の通信を実装する
  • 継承よりコンポジションを優先する
  • OnDisable でイベントの購読を必ず解除する
  • テストしやすくするために依存性注入を使用する
  • マネージャーの責務を絞る(単一責任)

❌ 避けること:

  • すべてをシングルトンにする
  • 神マネージャーを作成する(責務を一つにする)
  • イベントの購読解除を忘れる
  • 依存関係をハードコードする
  • 何でもシングルトンで実装する
  • データとプレゼンテーションを混在させる
  • 深い継承階層を作成する

黄金律:変更を前提に設計してください。疎結合で凝集度の高いシステムは、保守、テスト、拡張が容易です。


長く使い続けられる、スケーラブルで保守しやすい Unity プロジェクトを実現するために、これらのアーキテクチャパターンを適用してください。

コメント