Skip to content
cccskills
FreePublished on GitHub

unity-performance-and-gc

Use when touching Unity per-frame code, spawning, physics queries, UI updates or rendering setup - profiling with numbers, zero-GC hot paths (caching, NonAlloc queries, no LINQ/closures/boxing, TMP SetText), ObjectPool, allocation tests, Burst/Jobs for bulk math, and rendering/physics settings that cost frames.

The English summary is being prepared. Showing the original description.

Works withUnity
See how to install

Files included (1)

  • SKILL.md7.0 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Unity Performance and GC

Overview

Unity's managed heap is garbage-collected; every allocation in Update is a future GC spike, and the incremental collector only spreads the pain. Most frame drops in Unity projects come from a short list: allocations per frame, per-frame lookups, unpooled spawning, physics queries that allocate, UI rebuilds, and draw calls. Find which one with the Profiler, fix it, and prove the fix with numbers (game-performance-budget).

Core principle: Zero managed allocations per frame in gameplay, measured — not assumed — with the Profiler's GC Alloc column or an allocation test.

Profile first

  • Profiler: CPU Usage (Hierarchy, sort by Time ms and GC Alloc), Rendering, Physics, Memory modules. Profile a Development Build with Autoconnect; the Editor adds overhead.
  • Deep Profile only to locate a call, never for timing.
  • Profile Analyzer compares a before and an after capture; Memory Profiler finds leaks across scene loads.
  • Mark your own code: static readonly ProfilerMarker s_Marker = new("Enemy.Think"); using (s_Marker.Auto()) { ... }.

The allocation list

// ❌ allocates or searches every frame
void Update() {
    var enemies = FindObjectsByType<Enemy>(FindObjectsSortMode.None);       // array + scene search
    var close = enemies.Where(e => (e.transform.position - transform.position).sqrMagnitude < r2).ToList(); // LINQ + closure
    hpText.text = "HP: " + hp;                                              // new string every frame
    if (other.tag == "Enemy") { }                                           // allocates the tag string
    var hits = Physics.OverlapSphere(transform.position, 5f);              // new array per call
    StartCoroutine(Flash(new WaitForSeconds(0.1f)));                        // allocation per call
}

// ✅ cached, reused, non-allocating
readonly Collider[] _hits = new Collider[32];
static readonly WaitForSeconds FlashDelay = new(0.1f);
int _shownHp = -1;

void Update() {
    int count = Physics.OverlapSphereNonAlloc(transform.position, 5f, _hits, enemyMask);
    for (int i = 0; i < count; i++) { /* use _hits[i] */ }
    if (hp != _shownHp) { _shownHp = hp; hpText.SetText("HP: {0}", hp); }   // TextMeshPro, no string alloc
    if (other.CompareTag("Enemy")) { }
}

More of the same:

  • 3D queries: RaycastNonAlloc, SphereCastNonAlloc, OverlapBoxNonAlloc. 2D: the overloads taking a ContactFilter2D and a reused List<T>/array (the 2D *NonAlloc methods are obsolete).
  • GetComponentsInChildren(list) overloads that fill a reused list.
  • Animator.StringToHash, Shader.PropertyToID, cached WaitForSeconds/WaitForFixedUpdate.
  • foreach over List<T> and arrays is fine; over an IEnumerable<T> interface it boxes the enumerator.
  • No capturing lambdas, string.Format/interpolation, params arrays or boxing (struct → object/interface) in hot paths.
  • Debug.Log in a hot path allocates and stalls — strip it from builds ([Conditional("UNITY_EDITOR")] wrapper) or remove it.
  • Camera.main is cheaper than it was but is still a lookup — cache it.

Pool what you spawn

using UnityEngine.Pool;

public sealed class BulletSpawner : MonoBehaviour {
    [SerializeField] Bullet prefab;
    ObjectPool<Bullet> _pool;

    void Awake() => _pool = new ObjectPool<Bullet>(
        createFunc: () => { var b = Instantiate(prefab); b.Init(release: x => _pool.Release(x)); return b; },
        actionOnGet: b => b.gameObject.SetActive(true),
        actionOnRelease: b => b.gameObject.SetActive(false),
        actionOnDestroy: b => Destroy(b.gameObject),
        collectionCheck: false, defaultCapacity: 64, maxSize: 256);

    public void Fire(Vector3 pos, Vector3 dir) { var b = _pool.Get(); b.Launch(pos, dir); }
}

Pre-warm at load (get and release defaultCapacity instances) so the first fight does not instantiate. A pooled object resets all its state in actionOnGet/Launch, not in Start.

Prove zero allocations

using Is = UnityEngine.TestTools.Constraints.Is;

[Test] public void Tick_SteadyState_DoesNotAllocate() {
    var sim = TestSim.WithEnemies(100);
    sim.Tick(0.016f);                                    // warm-up: first call may JIT/initialise
    Assert.That(() => sim.Tick(0.016f), Is.Not.AllocatingGCMemory());
}

Burst and Jobs for bulk work

For the same math over hundreds or thousands of items (flocking, projectiles, terrain), a Burst-compiled job on native containers:

[BurstCompile]
struct MoveJob : IJobParallelFor {
    public NativeArray<float3> Positions;
    [ReadOnly] public NativeArray<float3> Velocities;
    public float Dt;
    public void Execute(int i) => Positions[i] += Velocities[i] * Dt;
}
// schedule: var h = new MoveJob { ... }.Schedule(count, 64); later h.Complete();
  • Native containers are disposed (Allocator.Persistent in OnDestroy; TempJob within four frames; Temp within the frame).
  • Complete handles before reading results; schedule early, complete late.
  • At entity scale, consider DOTS (unity-dots-ecs) — only if the project already uses it or the task decides it.

Rendering and physics settings that cost frames

  • URP/HDRP: keep the SRP Batcher compatible (shaders with CBUFFERs; avoid per-renderer MaterialPropertyBlock where it breaks batching); GPU instancing for repeated meshes; static batching for static scenery; Sprite Atlases for 2D.
  • UI: split static and frequently changing elements into separate Canvases; turn off Raycast Target on non-interactive graphics; never animate layout groups every frame.
  • Overdraw: transparent particles and full-screen effects, especially on mobile; LODs and occlusion culling for 3D.
  • Physics: the layer collision matrix trims pair checks; primitive colliders over mesh colliders on moving bodies; Physics.autoSyncTransforms off; tune Time.fixedDeltaTime deliberately.
  • Mobile: set Application.targetFrameRate explicitly; watch thermal throttling over minutes, not seconds.

Common Mistakes

  • Optimising from intuition without a Profiler capture.
  • Instantiate/Destroy for bullets, hit effects and damage numbers.
  • Updating TextMeshPro text every frame with string concatenation.
  • Allocation test without a warm-up call, failing on first-call initialisation.
  • Leaking NativeArrays (the leak detection warning in the console is a real bug).

Red Flags

  • GC.Alloc above zero in a gameplay Update in the Profiler.
  • LINQ, FindObjectsByType, GetComponent or new inside Update/FixedUpdate.
  • A performance claim in the closing message without before/after numbers.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper117Updated Oct 18, 2026

See all skills by makifbaysal

Report a problem with this skill