Unity Architecture について
保守しやすい Unity プロジェクト向けのアーキテクチャパターンを扱うスキルです。Unity ゲーム開発者がシステム、データ、通信を整理し、テストしやすく疎結合なコンポーネントを設計できます。
主な機能
- 01マネージャーとサービスロケーターパターン
- 02ScriptableObject によるデータ設計
- 03イベント駆動型の通信パターン
- 04コンポーネント構成とステートマシン
- 05MVC/MVP による分離
活用シーン
- 01ゲーム全体のシステム構成
- 02シーン間でのゲームデータ共有
- 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 プロジェクトを実現するために、これらのアーキテクチャパターンを適用してください。