mcpField
U
Unity Performance

Unity Performance

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

Unity Performance について

Unity Performanceは、Unityゲームのパフォーマンス最適化を案内するスキルです。Unity開発者がプロファイリングでボトルネックを特定し、CPU、メモリ、レンダリング、物理処理を改善できます。

主な機能

  1. 01コンポーネントとオブジェクト参照のキャッシュ
  2. 02オブジェクトプールによる再利用
  3. 03Updateループの処理削減
  4. 04ガベージコレクション割り当ての抑制
  5. 05物理判定の最適化
  6. 06ボトルネックの計測と比較

活用シーン

  1. 01ゲーム中のフレーム時間の急増を抑制
  2. 02頻繁に呼ばれるコードの割り当てを削減
  3. 03レンダリングや物理処理のボトルネックを調査

インストール

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

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

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
memory-optimization.md
rendering-optimization.md
profiling-guide.md
physics-optimization.md

詳細

Unity のパフォーマンス最適化

Unity ゲーム向けの必須パフォーマンス最適化テクニックです。メモリ管理、CPU 最適化、レンダリング、プロファイリング戦略を扱います。

概要

あらゆるプラットフォームの Unity ゲームにおいて、パフォーマンスは非常に重要です。パフォーマンスが低いと、フレームレートの低下、カクつき、ロード時間の増加、クラッシュが発生します。このスキルでは、あらゆる Unity プロジェクトに適用できる、実証済みの最適化テクニックを紹介します。

主な最適化領域:

  • CPU の最適化(Update ループ、キャッシュ、プーリング)
  • メモリ管理(GC の削減、アロケーションのパターン)
  • レンダリングの最適化(バッチング、カリング、LOD)
  • プロファイリングと測定(ボトルネックの特定)

参照のキャッシュ

Unity で最もよくあるパフォーマンス上のミスは、コストの高い検索を繰り返すことです。重複した処理を避けるため、すべての参照をキャッシュしてください。

GetComponent のキャッシュ

GetComponent を繰り返し呼び出してはいけません。結果を Awake でキャッシュします。

// ❌ BAD - GetComponent every frame
private void Update()
{
    GetComponent<Rigidbody>().velocity = Vector3.forward;  // SLOW!
}

// ✅ GOOD - Cache once
private Rigidbody rb;

private void Awake()
{
    rb = GetComponent<Rigidbody>();
}

private void Update()
{
    rb.velocity = Vector3.forward;  // FAST
}

パフォーマンスへの影響:GetComponent は、キャッシュした参照よりも 10~100 倍遅くなります。

Transform のキャッシュ

特に頻繁にアクセスする GameObject では、transform へのアクセスをキャッシュしてください。

// ❌ BAD - Property access overhead
private void Update()
{
    transform.position += Vector3.forward * Time.deltaTime;
    transform.rotation = Quaternion.identity;
}

// ✅ GOOD - Cache transform reference
private Transform myTransform;

private void Awake()
{
    myTransform = transform;
}

private void Update()
{
    myTransform.position += Vector3.forward * Time.deltaTime;
    myTransform.rotation = Quaternion.identity;
}

理由:transform プロパティにはオーバーヘッドがあります。参照をキャッシュすれば、検索の繰り返しをなくせます。

Find メソッドのキャッシュ

Update で Find メソッドを使用してはいけません。結果をキャッシュしてください。

// ❌ BAD - Find every frame (EXTREMELY SLOW)
private void Update()
{
    GameObject player = GameObject.Find("Player");
    Transform target = GameObject.FindWithTag("Enemy").transform;
}

// ✅ GOOD - Cache in Start
private GameObject player;
private Transform target;

private void Start()
{
    player = GameObject.Find("Player");
    target = GameObject.FindWithTag("Enemy")?.transform;
}

private void Update()
{
    // Use cached references
}

パフォーマンスへの影響:Find メソッドはシーン階層全体を検索します。キャッシュした参照よりも 100~1000 倍遅くなります。

Material のキャッシュ

renderer.material にアクセスすると新しい Material インスタンスが作成されます。メモリリークを避けるため、キャッシュしてください。

// ❌ BAD - Creates new Material every frame (MEMORY LEAK)
private void Update()
{
    GetComponent<Renderer>().material.color = Color.red;  // Creates new Material!
}

// ✅ GOOD - Cache material reference
private Material material;

private void Awake()
{
    material = GetComponent<Renderer>().material;
}

private void Update()
{
    material.color = Color.red;  // Modifies cached Material
}

private void OnDestroy()
{
    // Clean up instantiated material
    if (material != null)
        Destroy(material);
}

重要:.material にアクセスすると新しいインスタンスが作成されます。インスタンス化を避けて読み取り専用でアクセスするには、.sharedMaterial を使用してください。

オブジェクトプーリング

Instantiate と Destroy はコストが高い処理です。オブジェクトを繰り返し作成・破棄するのではなく、再利用してください。

基本的なプールの実装
public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int initialSize = 10;

    private Queue<GameObject> pool = new Queue<GameObject>();

    private void Awake()
    {
        // Pre-instantiate objects
        for (int i = 0; i < initialSize; i++)
        {
            GameObject obj = Instantiate(prefab);
            obj.SetActive(false);
            pool.Enqueue(obj);
        }
    }

    public GameObject Get()
    {
        if (pool.Count > 0)
        {
            GameObject obj = pool.Dequeue();
            obj.SetActive(true);
            return obj;
        }

        // Pool exhausted - create new
        return Instantiate(prefab);
    }

    public void Return(GameObject obj)
    {
        obj.SetActive(false);
        pool.Enqueue(obj);
    }
}

用途:

  • 弾丸、飛翔体
  • パーティクルエフェクト
  • UI 要素(ツールチップ、ダメージ表示)
  • ウェーブ制のゲームの敵
  • オーディオソース

パフォーマンスの向上:Instantiate/Destroy より 10~50 倍高速になり、GC の急増をなくせます。

プールパターンの使用例
public class BulletSpawner : MonoBehaviour
{
    [SerializeField] private ObjectPool bulletPool;
    [SerializeField] private Transform firePoint;

    private void Fire()
    {
        // Get from pool
        GameObject bullet = bulletPool.Get();
        bullet.transform.position = firePoint.position;
        bullet.transform.rotation = firePoint.rotation;

        // Return after 3 seconds
        StartCoroutine(ReturnToPoolAfterDelay(bullet, 3f));
    }

    private IEnumerator ReturnToPoolAfterDelay(GameObject obj, float delay)
    {
        yield return new WaitForSeconds(delay);
        bulletPool.Return(obj);
    }
}

Update ループの最適化

Update、FixedUpdate、LateUpdate は頻繁に呼び出されます。これらのメソッド内で実行する処理を最小限に抑えてください。

空の Update メソッドを削除する
// ❌ BAD - Empty methods still have overhead
private void Update() { }
private void FixedUpdate() { }

// ✅ GOOD - Remove unused methods
// (No Update/FixedUpdate if not needed)

パフォーマンス:Unity は空の Update メソッドもすべて呼び出します。オーバーヘッドを減らすため、削除してください。

Update の頻度を減らす

すべてのロジックを毎フレーム実行する必要はありません。

// ❌ BAD - Expensive check every frame
private void Update()
{
    CheckForNearbyEnemies();  // Expensive raycast/distance checks
}

// ✅ GOOD - Check every N frames
private int frameCounter = 0;
private const int checkInterval = 10;

private void Update()
{
    frameCounter++;
    if (frameCounter >= checkInterval)
    {
        frameCounter = 0;
        CheckForNearbyEnemies();
    }
}

// ✅ BETTER - Use InvokeRepeating or Coroutine
private void Start()
{
    InvokeRepeating(nameof(CheckForNearbyEnemies), 0f, 0.2f);  // Every 0.2 seconds
}

代替方法:コルーチン

private void Start()
{
    StartCoroutine(CheckEnemiesRoutine());
}

private IEnumerator CheckEnemiesRoutine()
{
    while (true)
    {
        CheckForNearbyEnemies();
        yield return new WaitForSeconds(0.2f);
    }
}
イベント駆動型アーキテクチャ

ポーリングをイベントに置き換えます。

// ❌ BAD - Poll for state change every frame
private bool wasGrounded;

private void Update()
{
    bool grounded = IsGrounded();
    if (grounded != wasGrounded)
    {
        OnGroundedChanged(grounded);
    }
    wasGrounded = grounded;
}

// ✅ GOOD - Event-driven
public event Action<bool> OnGroundedChanged;

private bool isGrounded;

private void SetGrounded(bool grounded)
{
    if (isGrounded != grounded)
    {
        isGrounded = grounded;
        OnGroundedChanged?.Invoke(grounded);
    }
}

ガベージコレクションの削減

GC の急増を防ぐため、頻繁に呼び出されるメソッド内でのアロケーションを避けてください。

文字列の連結
// ❌ BAD - Allocates strings every frame
private void Update()
{
    string message = "Health: " + health;  // String allocation
    scoreText.text = "Score: " + score;    // String allocation
}

// ✅ GOOD - Use StringBuilder or string interpolation
private StringBuilder sb = new StringBuilder();

private void UpdateUI()
{
    sb.Clear();
    sb.Append("Health: ").Append(health);
    healthText.text = sb.ToString();
}

// ✅ ALTERNATIVE - Cache formatted strings
private void UpdateHealth(int newHealth)
{
    health = newHealth;
    healthText.text = health.ToString();  // Less allocation than concatenation
}
コレクションのアロケーション
// ❌ BAD - Allocates new list every frame
private void Update()
{
    List<Enemy> nearbyEnemies = new List<Enemy>();  // GC allocation!
    FindNearbyEnemies(nearbyEnemies);
}

// ✅ GOOD - Reuse list
private List<Enemy> nearbyEnemies = new List<Enemy>();

private void Update()
{
    nearbyEnemies.Clear();  // Reuse existing list
    FindNearbyEnemies(nearbyEnemies);
}
配列/リストのベストプラクティス
// ❌ BAD - ToArray allocates
private void Update()
{
    GameObject[] enemies = enemyList.ToArray();  // GC allocation!
}

// ✅ GOOD - Iterate list directly
private void Update()
{
    for (int i = 0; i < enemyList.Count; i++)
    {
        Enemy enemy = enemyList[i];
        // Process enemy
    }
}

// ✅ GOOD - Use foreach (no allocation for List)
private void Update()
{
    foreach (var enemy in enemyList)
    {
        // Process enemy
    }
}
コルーチンのアロケーション
// ❌ BAD - Allocates WaitForSeconds every call
private IEnumerator DelayedAction()
{
    yield return new WaitForSeconds(1f);  // New allocation each time
}

// ✅ GOOD - Cache WaitForSeconds
private WaitForSeconds oneSecondWait = new WaitForSeconds(1f);

private IEnumerator DelayedAction()
{
    yield return oneSecondWait;  // Reuse cached wait
}

コンポーネントへのアクセスパターン

コンポーネントの検索を最小限にする
// ❌ BAD - Multiple GetComponent calls
private void OnTriggerEnter(Collider other)
{
    if (other.GetComponent<Enemy>() != null)
    {
        other.GetComponent<Enemy>().TakeDamage(10);  // Called twice!
    }
}

// ✅ GOOD - Single GetComponent with pattern matching
private void OnTriggerEnter(Collider other)
{
    if (other.TryGetComponent<Enemy>(out var enemy))
    {
        enemy.TakeDamage(10);  // Called once
    }
}
衝突時のコンポーネントのキャッシュ
// ❌ BAD - GetComponent on every collision
private void OnTriggerEnter(Collider other)
{
    var damageable = other.GetComponent<IDamageable>();
    if (damageable != null)
        damageable.TakeDamage(10);
}

// ✅ GOOD - Cache component on trigger enter
private Dictionary<Collider, IDamageable> damageableCache = new Dictionary<Collider, IDamageable>();

private void OnTriggerEnter(Collider other)
{
    if (!damageableCache.TryGetValue(other, out var damageable))
    {
        damageable = other.GetComponent<IDamageable>();
        damageableCache[other] = damageable;  // Cache for future collisions
    }

    damageable?.TakeDamage(10);
}

private void OnTriggerExit(Collider other)
{
    damageableCache.Remove(other);  // Clean up cache
}

物理演算の最適化

レイヤーベースの衝突判定

不要な衝突判定を防ぐため、Physics Layer Collision Matrix を設定します。

Edit > Project Settings > Physics > Layer Collision Matrix

// Setup layers
Layer 8: Player
Layer 9: Enemies
Layer 10: Projectiles
Layer 11: Environment

// Disable unnecessary collisions:
- Player vs Player (disabled)
- Enemies vs Enemies (disabled)
- Projectiles vs Projectiles (disabled)

パフォーマンスの向上:物理演算のオーバーヘッドを 30~50%削減できます。

Raycast の最適化
// ❌ BAD - Raycast checks everything
bool hit = Physics.Raycast(origin, direction, out RaycastHit hitInfo);

// ✅ GOOD - Layer mask limits checks
int layerMask = 1 << LayerMask.NameToLayer("Enemy");
bool hit = Physics.Raycast(origin, direction, out RaycastHit hitInfo, maxDistance, layerMask);

// ✅ BETTER - Cache layer mask
private int enemyLayerMask;

private void Awake()
{
    enemyLayerMask = 1 << LayerMask.NameToLayer("Enemy");
}

private void Fire()
{
    bool hit = Physics.Raycast(origin, direction, out RaycastHit hitInfo, maxDistance, enemyLayerMask);
}
Rigidbody のスリープ

動いていないときは、Rigidbody をスリープさせます。

// Rigidbody automatically sleeps when velocity < threshold
// Configure in Edit > Project Settings > Physics

// Wake up manually when needed
private void ApplyForce()
{
    if (rb.IsSleeping())
        rb.WakeUp();

    rb.AddForce(force);
}

プロファイリング

最適化の前に測定してください。Unity Profiler を使用して、実際のボトルネックを特定します。

Profiler を開く:Window > Analysis > Profiler

主な Profiler の指標

CPU 使用率:

  • レンダリング(DrawCalls、SetPass 呼び出し)
  • スクリプト(Update、FixedUpdate、コルーチン)
  • 物理演算(FixedUpdate.PhysicsFixedUpdate)
  • GC.Alloc(ガベージコレクションによるアロケーション)

メモリ:

  • 割り当て済みメモリの合計
  • GC による割り当て
  • テクスチャメモリ
  • メッシュメモリ
プロファイリングのワークフロー
  1. ボトルネックを特定する:Profiler を実行し、負荷の高いフレームを見つけます
  2. 詳細を調べる:スパイクをクリックし、呼び出し階層を確認します
  3. ベースラインを測定する:現在のパフォーマンスを記録します
  4. 最適化を適用する:対象を絞って変更します
  5. 改善を測定する:変更前と変更後を比較します
  6. 繰り返す:次のボトルネックを見つけます
詳細プロファイル

詳細なコールスタックを表示するには、Deep Profile を有効にします(パフォーマンスに影響します)。

警告:Deep Profile を有効にすると、ゲームの動作が大幅に遅くなります。小さなシーンや対象を絞ったプロファイリングで使用してください。

カスタムプロファイラーマーカー

特定のコードセクションを測定します。

using Unity.Profiling;

public class AIController : MonoBehaviour
{
    private static readonly ProfilerMarker s_PathfindingMarker = new ProfilerMarker("AI.Pathfinding");
    private static readonly ProfilerMarker s_DecisionMarker = new ProfilerMarker("AI.DecisionMaking");

    private void Update()
    {
        s_PathfindingMarker.Begin();
        CalculatePath();
        s_PathfindingMarker.End();

        s_DecisionMarker.Begin();
        MakeDecision();
        s_DecisionMarker.End();
    }

    private void CalculatePath() { }
    private void MakeDecision() { }
}

Profiler にカスタムマーカーが表示され、正確に測定できます。

パフォーマンス予算

各システムのパフォーマンス目標を設定します。

目標:60 FPS(1 フレームあたり 16.67ms)

  • レンダリング:6ms
  • スクリプト:4ms
  • 物理演算:2ms
  • UI:1ms
  • オーディオ:0.5ms
  • その他:3ms

Profiler で監視し、予算を超過するシステムを最適化します。

プラットフォーム固有の最適化

モバイルの最適化

主な懸念事項:

  • CPU/GPU の性能が低い
  • メモリの制約
  • バッテリー駆動時間
  • タッチ入力のオーバーヘッド

モバイル固有の最適化:

  • ドローコールを削減する(モバイルでは 100 未満)
  • テクスチャの解像度を下げる
  • 影を無効にするか、簡易的な影を使用する
  • パーティクル数を減らす
  • オクルージョンカリングを使用する
  • UI を最適化する(Canvas のバッチ処理)
PC/コンソールの最適化

処理能力に余裕があっても最適化は必要です:

  • 最低 60 FPS を目標にする
  • より高品質な設定を許容する
  • VRAM 使用量を監視する
  • 最低スペックのハードウェアでプロファイリングする

その他のリソース

参考ファイル

詳しいパフォーマンス最適化手法については、次のファイルを参照してください。

  • references/memory-optimization.md - 高度な GC 削減手法、メモリ割り当てパターン
  • references/rendering-optimization.md - ドローコールのバッチ処理、GPU の最適化、シェーダー
  • references/physics-optimization.md - 衝突判定の最適化、Rigidbody のベストプラクティス
  • references/profiling-guide.md - プロファイリングのワークフロー、ツール、分析の全容

クイックリファレンス

キャッシュの優先順位:

  1. Transform の参照
  2. GetComponent の結果
  3. Find の結果
  4. Material インスタンス
  5. コルーチン内の WaitForSeconds

Update 内で避けること:

  • GetComponent
  • Find メソッド
  • 文字列の連結
  • 新しいメモリ割り当て
  • 物理演算のレイキャスト(使用は控えめに)

最適化の前には必ずプロファイリングする:

  • ベースラインを測定する
  • ボトルネックを特定する
  • 対象を絞って修正する
  • 改善を測定する

これらのパフォーマンス最適化手法を一貫して適用し、すべてのプラットフォームで滑らかで応答性の高い Unity ゲームを実現しましょう。

コメント