Unity Performance について
Unity Performanceは、Unityゲームのパフォーマンス最適化を案内するスキルです。Unity開発者がプロファイリングでボトルネックを特定し、CPU、メモリ、レンダリング、物理処理を改善できます。
主な機能
- 01コンポーネントとオブジェクト参照のキャッシュ
- 02オブジェクトプールによる再利用
- 03Updateループの処理削減
- 04ガベージコレクション割り当ての抑制
- 05物理判定の最適化
- 06ボトルネックの計測と比較
活用シーン
- 01ゲーム中のフレーム時間の急増を抑制
- 02頻繁に呼ばれるコードの割り当てを削減
- 03レンダリングや物理処理のボトルネックを調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cryptorabea/ claude_unity_dev_plugin --skill unity-performanceインストール後、Claude Code で/unity-performanceを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/memory-optimization.mdrendering-optimization.mdprofiling-guide.mdphysics-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 による割り当て
- テクスチャメモリ
- メッシュメモリ
プロファイリングのワークフロー
- ボトルネックを特定する:Profiler を実行し、負荷の高いフレームを見つけます
- 詳細を調べる:スパイクをクリックし、呼び出し階層を確認します
- ベースラインを測定する:現在のパフォーマンスを記録します
- 最適化を適用する:対象を絞って変更します
- 改善を測定する:変更前と変更後を比較します
- 繰り返す:次のボトルネックを見つけます
詳細プロファイル
詳細なコールスタックを表示するには、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- プロファイリングのワークフロー、ツール、分析の全容
クイックリファレンス
キャッシュの優先順位:
- Transform の参照
- GetComponent の結果
- Find の結果
- Material インスタンス
- コルーチン内の WaitForSeconds
Update 内で避けること:
- GetComponent
- Find メソッド
- 文字列の連結
- 新しいメモリ割り当て
- 物理演算のレイキャスト(使用は控えめに)
最適化の前には必ずプロファイリングする:
- ベースラインを測定する
- ボトルネックを特定する
- 対象を絞って修正する
- 改善を測定する
これらのパフォーマンス最適化手法を一貫して適用し、すべてのプラットフォームで滑らかで応答性の高い Unity ゲームを実現しましょう。