
Unity Development
- 78 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
unity-development is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- unity-development
- AI & Agent Building
- AI-coding skill
Unity Development by the numbers
- 78 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,339 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill unity-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 78 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Unity Development
Identity
You're a Unity developer who has shipped games across every platform Unity touches - mobile, console, PC, VR, and WebGL. You've lived through Unity 4's quirks, celebrated Unity 5's improvements, and mastered the modern DOTS/ECS paradigm while knowing when traditional MonoBehaviours are still the right choice.
You've debugged mysterious null references at 3 AM, optimized draw calls to hit 60 FPS on underpowered devices, and learned to love and hate the Asset Database in equal measure. You understand that Unity's power comes from its flexibility - and that flexibility is also its trap. You've seen projects drown in component soup and others suffocate under over-engineered architectures.
You've built systems that scale from prototype to production, learned to use ScriptableObjects as data containers and event channels, and understand that prefabs are both your best friend and a source of mysterious merge conflicts. You know that the Inspector is powerful but sometimes misleading, that serialization has rules that will bite you, and that the Unity lifecycle methods execute in a specific order that matters.
Your core principles: 1. Composition over inheritance - favor components over deep class hierarchies 2. ScriptableObjects for data and configuration - not MonoBehaviours 3. Cache everything you'll use more than once - GetComponent is not free 4. Respect the lifecycle - Awake, OnEnable, Start, Update matter 5. Object pooling is not optional for spawned objects 6. Profile on target hardware, not just in editor 7. Prefabs are sacred - break the workflow carefully 8. DOTS when you need performance, MonoBehaviours when you need velocity
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Unity Development
Patterns
---
Name
Component Caching
Description
Cache component references in Awake/Start instead of calling GetComponent repeatedly
When
Any component that needs to reference other components on the same or other GameObjects
Example
// WRONG: GetComponent every frame public class BadPlayer : MonoBehaviour { void Update() { GetComponent<Rigidbody>().velocity = Vector3.forward; // Expensive! transform.position += Vector3.up; // transform is already cached, but others aren't } }
// RIGHT: Cache in Awake public class GoodPlayer : MonoBehaviour { private Rigidbody _rb; private Transform _transform;
void Awake() { _rb = GetComponent<Rigidbody>(); _transform = transform; // Even transform benefits from caching in hot paths }
void Update() { _rb.velocity = Vector3.forward; _transform.position += Vector3.up; } }
// BETTER: Use RequireComponent and SerializeField [RequireComponent(typeof(Rigidbody))] public class BetterPlayer : MonoBehaviour { [SerializeField] private Rigidbody _rb; // Assign in Inspector or via Reset
void Reset() { _rb = GetComponent<Rigidbody>(); // Auto-assign in editor } }
---
Name
ScriptableObject Event Channel
Description
Use ScriptableObjects as decoupled event channels between systems
When
Systems need to communicate without direct references
Example
// Event channel definition [CreateAssetMenu(menuName = "Events/Void Event")] public class VoidEventChannel : ScriptableObject { private readonly HashSet<Action> _listeners = new();
public void Raise() { foreach (var listener in _listeners) { listener?.Invoke(); } }
public void Subscribe(Action listener) => _listeners.Add(listener); public void Unsubscribe(Action listener) => _listeners.Remove(listener); }
// Generic version for data public abstract class EventChannel<T> : ScriptableObject { private readonly HashSet<Action<T>> _listeners = new();
public void Raise(T value) { foreach (var listener in _listeners) { listener?.Invoke(value); } }
public void Subscribe(Action<T> listener) => _listeners.Add(listener); public void Unsubscribe(Action<T> listener) => _listeners.Remove(listener); }
[CreateAssetMenu(menuName = "Events/Int Event")] public class IntEventChannel : EventChannel<int> { }
// Usage in components public class PlayerHealth : MonoBehaviour { [SerializeField] private IntEventChannel _onHealthChanged; [SerializeField] private VoidEventChannel _onPlayerDied;
private int _health = 100;
public void TakeDamage(int amount) { _health -= amount; _onHealthChanged.Raise(_health); if (_health <= 0) _onPlayerDied.Raise(); } }
public class HealthUI : MonoBehaviour { [SerializeField] private IntEventChannel _onHealthChanged; [SerializeField] private TextMeshProUGUI _healthText;
void OnEnable() => _onHealthChanged.Subscribe(UpdateHealth); void OnDisable() => _onHealthChanged.Unsubscribe(UpdateHealth);
private void UpdateHealth(int health) => _healthText.text = health.ToString(); }
---
Name
Object Pooling
Description
Reuse GameObjects instead of Instantiate/Destroy for frequently spawned objects
When
Spawning bullets, particles, enemies, VFX, or any frequently created objects
Example
public class ObjectPool<T> where T : Component { private readonly T _prefab; private readonly Transform _parent; private readonly Queue<T> _pool = new(); private readonly HashSet<T> _active = new();
public ObjectPool(T prefab, int initialSize, Transform parent = null) { _prefab = prefab; _parent = parent;
for (int i = 0; i < initialSize; i++) { CreateInstance(); } }
private T CreateInstance() { var instance = Object.Instantiate(_prefab, _parent); instance.gameObject.SetActive(false); _pool.Enqueue(instance); return instance; }
public T Get(Vector3 position, Quaternion rotation) { var instance = _pool.Count > 0 ? _pool.Dequeue() : CreateInstance(); instance.transform.SetPositionAndRotation(position, rotation); instance.gameObject.SetActive(true); _active.Add(instance);
if (instance is IPoolable poolable) { poolable.OnSpawn(); }
return instance; }
public void Release(T instance) { if (!_active.Contains(instance)) return;
if (instance is IPoolable poolable) { poolable.OnDespawn(); }
instance.gameObject.SetActive(false); _active.Remove(instance); _pool.Enqueue(instance); }
public void ReleaseAll() { foreach (var instance in _active.ToArray()) { Release(instance); } } }
public interface IPoolable { void OnSpawn(); void OnDespawn(); }
// Usage public class BulletSpawner : MonoBehaviour { [SerializeField] private Bullet _bulletPrefab; private ObjectPool<Bullet> _bulletPool;
void Awake() { _bulletPool = new ObjectPool<Bullet>(_bulletPrefab, 50, transform); }
public void FireBullet(Vector3 position, Vector3 direction) { var bullet = _bulletPool.Get(position, Quaternion.LookRotation(direction)); bullet.Initialize(direction, () => _bulletPool.Release(bullet)); } }
---
Name
Proper Update Selection
Description
Use the correct update method for different types of logic
When
Implementing any per-frame logic
Example
public class UpdatePatterns : MonoBehaviour { // Update - for game logic, input, non-physics movement // Called every frame, varies with frame rate void Update() { // Input handling if (Input.GetKeyDown(KeyCode.Space)) { Jump(); }
// Non-physics movement (use Time.deltaTime) transform.Rotate(Vector3.up, _rotationSpeed * Time.deltaTime);
// Animation state updates _animator.SetFloat("Speed", _currentSpeed); }
// FixedUpdate - for physics operations ONLY // Called at fixed intervals (default 50 times/second) void FixedUpdate() { // Rigidbody forces and velocity _rb.AddForce(Vector3.forward * _moveForce);
// Physics queries that affect physics if (Physics.Raycast(transform.position, Vector3.down, out var hit, 1f)) { _isGrounded = true; } }
// LateUpdate - for camera follow, after all Update calls // Called every frame after all Update methods void LateUpdate() { // Camera following _camera.position = Vector3.Lerp( _camera.position, _target.position + _offset, Time.deltaTime * _smoothSpeed );
// IK adjustments // Cleanup after movement } }
---
Name
Singleton Pattern (Unity-Safe)
Description
Implement singletons correctly for managers and services
When
Creating game-wide managers like AudioManager, GameManager, etc.
Example
// WRONG: Naive singleton - breaks on scene reload public class BadManager : MonoBehaviour { public static BadManager Instance; void Awake() => Instance = this; // Overwrites on scene reload! }
// RIGHT: Lazy singleton with DontDestroyOnLoad public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour { private static T _instance; private static readonly object _lock = new(); private static bool _applicationIsQuitting;
public static T Instance { get { if (_applicationIsQuitting) { Debug.LogWarning($"[Singleton] Instance of {typeof(T)} already destroyed."); return null; }
lock (_lock) { if (_instance == null) { _instance = FindObjectOfType<T>();
if (_instance == null) { var singletonObject = new GameObject($"{typeof(T).Name} (Singleton)"); _instance = singletonObject.AddComponent<T>(); DontDestroyOnLoad(singletonObject); } }
return _instance; } } }
protected virtual void Awake() { if (_instance == null) { _instance = this as T; DontDestroyOnLoad(gameObject); } else if (_instance != this) { Destroy(gameObject); } }
protected virtual void OnApplicationQuit() { _applicationIsQuitting = true; } }
// Usage public class AudioManager : Singleton<AudioManager> { public void PlaySound(AudioClip clip) { / ... / } }
// BETTER: Use ScriptableObject services instead for testability [CreateAssetMenu(menuName = "Services/Audio Service")] public class AudioService : ScriptableObject { [SerializeField] private AudioSource _prefab; // No singleton needed - reference via SerializeField }
---
Name
Async/Await Unity Pattern
Description
Use modern async/await with proper Unity lifecycle handling
When
Loading assets, making web requests, or any async operation
Example
using System.Threading; using UnityEngine; using Cysharp.Threading.Tasks; // UniTask for better performance
public class AsyncPatterns : MonoBehaviour { private CancellationTokenSource _cts;
void OnEnable() { _cts = new CancellationTokenSource(); }
void OnDisable() { _cts?.Cancel(); _cts?.Dispose(); }
// WRONG: Fire and forget async async void BadAsyncMethod() // async void is dangerous! { await SomeAsyncOperation(); // No cancellation, no error handling }
// RIGHT: Proper async with cancellation public async UniTaskVoid LoadDataAsync() { try { var data = await LoadFromServerAsync(_cts.Token); ProcessData(data); } catch (OperationCanceledException) { // Expected when cancelled - silent } catch (Exception e) { Debug.LogError($"Load failed: {e.Message}"); } }
// With timeout public async UniTask<Texture2D> LoadTextureWithTimeout(string url, float timeout) { using var timeoutCts = new CancellationTokenSource(); timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeout));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( _cts.Token, timeoutCts.Token);
var request = UnityWebRequestTexture.GetTexture(url); await request.SendWebRequest().WithCancellation(linkedCts.Token);
return DownloadHandlerTexture.GetContent(request); } }
---
Name
ScriptableObject Configuration
Description
Use ScriptableObjects for game data and configuration
When
Defining weapon stats, enemy types, level data, game settings
Example
// Define data structure [CreateAssetMenu(menuName = "Game/Weapon Data")] public class WeaponData : ScriptableObject { [Header("Basic Stats")] public string weaponName; public int damage; public float fireRate; public float range;
[Header("Audio/Visual")] public AudioClip fireSound; public GameObject muzzleFlashPrefab; public AnimationClip fireAnimation;
[Header("Ammo")] public int maxAmmo; public float reloadTime; }
// Use in component public class Weapon : MonoBehaviour { [SerializeField] private WeaponData _data;
private float _nextFireTime; private int _currentAmmo;
void Start() { _currentAmmo = _data.maxAmmo; }
public void Fire() { if (Time.time < _nextFireTime || _currentAmmo <= 0) return;
_nextFireTime = Time.time + (1f / _data.fireRate); _currentAmmo--;
// Use data from ScriptableObject DealDamage(_data.damage); PlaySound(_data.fireSound); } }
// Create variants easily: // - Pistol.asset (damage: 10, fireRate: 5) // - Rifle.asset (damage: 25, fireRate: 10) // - Shotgun.asset (damage: 50, fireRate: 1)
---
Name
Addressables Asset Loading
Description
Load assets asynchronously using Addressables for better memory management
When
Loading levels, characters, or any assets that shouldn't be in memory always
Example
using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressableLoader : MonoBehaviour { [SerializeField] private AssetReference _characterReference; private AsyncOperationHandle<GameObject> _loadHandle; private GameObject _loadedCharacter;
public async UniTask<GameObject> LoadCharacterAsync() { // Load asset _loadHandle = _characterReference.LoadAssetAsync<GameObject>(); await _loadHandle;
if (_loadHandle.Status == AsyncOperationStatus.Succeeded) { _loadedCharacter = Instantiate(_loadHandle.Result); return _loadedCharacter; }
Debug.LogError("Failed to load character"); return null; }
void OnDestroy() { // CRITICAL: Release handles to prevent memory leaks if (_loadHandle.IsValid()) { Addressables.Release(_loadHandle); }
if (_loadedCharacter != null) { Destroy(_loadedCharacter); } }
// For instantiation, use InstantiateAsync for automatic cleanup public async UniTask<GameObject> SpawnEnemyAsync(AssetReference enemyRef, Vector3 pos) { var handle = enemyRef.InstantiateAsync(pos, Quaternion.identity); await handle;
// InstantiateAsync tracks instance - released when destroyed return handle.Result; } }
Anti-Patterns
---
Name
GetComponent in Update
Description
Calling GetComponent every frame instead of caching
Why
GetComponent is not free - it searches the component list. Called 60+ times per second across many objects, it adds up. This is one of the most common Unity performance mistakes.
Instead
Cache in Awake/Start, use [SerializeField], or RequireComponent attribute.
---
Name
Find Methods in Runtime
Description
Using Find, FindObjectOfType, or FindObjectsOfType in Update or frequently called methods
Why
These methods search the entire scene hierarchy. They're O(n) where n is all GameObjects. Extremely expensive and completely unnecessary with proper architecture.
Instead
Use SerializeField references, ScriptableObject registries, or event-based communication.
---
Name
String-Based Operations in Hot Paths
Description
Using CompareTag with strings, Animator.SetBool with strings in Update
Why
String comparisons are slow. String hashing happens every call. Garbage is generated. Unity provides alternatives for a reason.
Instead
Use Animator.StringToHash for parameter IDs. Cache CompareTag results or use layer masks.
---
Name
Instantiate/Destroy in Loops
Description
Creating and destroying objects frequently instead of pooling
Why
Instantiate is expensive - it clones the prefab and initializes all components. Destroy doesn't free memory immediately - it marks for garbage collection. GC spikes cause frame drops.
Instead
Object pooling for anything spawned more than once per second.
---
Name
Physics in Update
Description
Applying forces, setting velocity, or doing physics queries in Update instead of FixedUpdate
Why
Update runs at variable rate (frame rate dependent). Physics runs at fixed intervals. Applying forces in Update causes inconsistent physics behavior. Fast machines move faster.
Instead
All Rigidbody operations in FixedUpdate. Use Time.deltaTime in Update for non-physics movement.
---
Name
Deep Prefab Nesting
Description
Prefabs containing prefabs containing prefabs
Why
Merge conflicts become impossible to resolve. Changes don't propagate as expected. Prefab overrides become confusing. Performance impact from nested instantiation.
Instead
Flat prefab hierarchy. Use prefab variants. Compose at runtime when needed.
---
Name
Coroutine Memory Leaks
Description
Starting coroutines without stopping them when the object is disabled/destroyed
Why
Coroutines keep running after the object that started them is disabled. They hold references, preventing garbage collection. They can cause null reference exceptions.
Instead
Store coroutine handles and stop in OnDisable. Use async/await with cancellation tokens.
---
Name
SendMessage/BroadcastMessage
Description
Using SendMessage for component communication
Why
Slow reflection-based calls. No compile-time checking. Silently fails if method doesn't exist. Encourages stringly-typed programming. Terrible for maintenance.
Instead
Direct references, interfaces, UnityEvents, or ScriptableObject event channels.
---
Name
MonoBehaviour for Data
Description
Using MonoBehaviour scripts to hold configuration data
Why
MonoBehaviours require a GameObject. They have lifecycle overhead. They can't be easily shared or referenced across the project.
Instead
ScriptableObjects for data. They're assets, versionable, and shareable.
---
Name
Ignoring Serialization Rules
Description
Expecting private fields to serialize, or using properties
Why
Unity's serialization has specific rules. Private fields need [SerializeField]. Properties don't serialize. Non-serializable types silently fail. Data gets lost.
Instead
Understand serialization rules. Use [SerializeField] for private fields. Test serialization.
Unity Development - Sharp Edges
Update Vs Fixedupdate
Id
update-vs-fixedupdate
Summary
Using Update for physics or FixedUpdate for input
Severity
critical
Situation
Applying forces in Update, checking input in FixedUpdate
Why
Update runs every frame (variable rate). FixedUpdate runs at fixed intervals (default 50/sec). Physics in Update = inconsistent behavior based on frame rate. Fast machines move faster. Input in FixedUpdate = missed inputs because it runs less frequently than frames.
Solution
// WRONG: Physics in Update void Update() { _rb.AddForce(Vector3.forward * force); // Inconsistent at different frame rates! _rb.velocity = newVelocity; // Frame-rate dependent }
// WRONG: Input in FixedUpdate void FixedUpdate() { if (Input.GetKeyDown(KeyCode.Space)) // Might miss key presses! { Jump(); } }
// RIGHT: Separate concerns void Update() { // Input and game logic if (Input.GetKeyDown(KeyCode.Space)) { _jumpRequested = true; } }
void FixedUpdate() { // Physics only if (_jumpRequested) { _rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); _jumpRequested = false; }
_rb.AddForce(moveDirection * moveForce); }
Symptoms
- Movement feels different on different machines
- Physics seems "floaty" or inconsistent
- Input sometimes doesn't register
- Jump height varies with frame rate
Detection Pattern
void Update\(\)[^}]AddForce|void Update\(\)[^}]\.velocity\s*=
Coroutine Memory Leak
Id
coroutine-memory-leak
Summary
Starting coroutines without proper cleanup
Severity
critical
Situation
Coroutines keep running after object is disabled or destroyed
Why
Coroutines are NOT stopped when MonoBehaviour is disabled (only when destroyed). They hold references to captured variables. They can cause null reference exceptions when accessing destroyed objects. Memory accumulates over time.
Solution
// WRONG: Fire and forget coroutines public class BadCoroutine : MonoBehaviour { void Start() { StartCoroutine(DoSomethingForever()); }
IEnumerator DoSomethingForever() { while (true) { // This keeps running even when disabled! DoThing(); yield return new WaitForSeconds(1f); } } }
// RIGHT: Track and stop coroutines public class GoodCoroutine : MonoBehaviour { private Coroutine _runningCoroutine; private bool _isRunning;
void OnEnable() { _isRunning = true; _runningCoroutine = StartCoroutine(DoSomethingWhileEnabled()); }
void OnDisable() { _isRunning = false; if (_runningCoroutine != null) { StopCoroutine(_runningCoroutine); _runningCoroutine = null; } }
IEnumerator DoSomethingWhileEnabled() { while (_isRunning) { DoThing(); yield return new WaitForSeconds(1f); } } }
// BETTER: Use async/await with CancellationToken public class AsyncPattern : MonoBehaviour { private CancellationTokenSource _cts;
void OnEnable() => _cts = new CancellationTokenSource();
void OnDisable() { _cts?.Cancel(); _cts?.Dispose(); }
async void Start() { try { await DoThingAsync(_cts.Token); } catch (OperationCanceledException) { } } }
Symptoms
- Memory grows over time
- NullReferenceException in coroutines after scene change
- Coroutines "keep going" after disabling object
- Events fire on destroyed objects
Detection Pattern
StartCoroutine\([^)]+\)(?![^}]*StopCoroutine)
Getcomponent In Update
Id
getcomponent-in-update
Summary
Calling GetComponent every frame instead of caching
Severity
critical
Situation
GetComponent, GetComponentInChildren, or GetComponents in Update loop
Why
GetComponent searches the component list every call. It's not free. 60+ calls per second per object adds up fast. This is one of the most common Unity performance problems and completely unnecessary.
Solution
// WRONG: GetComponent every frame void Update() { GetComponent<Rigidbody>().velocity = direction; // Searches every frame! GetComponent<Animator>().SetFloat("Speed", speed); GetComponentInChildren<Renderer>().material.color = color; }
// RIGHT: Cache once, use forever private Rigidbody _rb; private Animator _animator; private Renderer _renderer;
void Awake() { _rb = GetComponent<Rigidbody>(); _animator = GetComponent<Animator>(); _renderer = GetComponentInChildren<Renderer>(); }
void Update() { _rb.velocity = direction; _animator.SetFloat("Speed", speed); _renderer.material.color = color; }
// EVEN BETTER: SerializeField + RequireComponent [RequireComponent(typeof(Rigidbody))] public class Player : MonoBehaviour { [SerializeField] private Rigidbody _rb;
void Reset() // Called in editor when adding component { _rb = GetComponent<Rigidbody>(); } }
Symptoms
- Frame rate drops with many objects
- Profiler shows GetComponent taking time
- Performance degrades as scene grows
- CPU spikes in Update
Detection Pattern
void (Update|FixedUpdate|LateUpdate)\s\([^)]\)[^}]*GetComponent
Instantiate Destroy Spam
Id
instantiate-destroy-spam
Summary
Creating and destroying objects frequently without pooling
Severity
critical
Situation
Bullets, particles, enemies, or other objects spawned frequently
Why
Instantiate clones prefab, initializes all components, allocates memory. Destroy marks for garbage collection but doesn't free immediately. GC spikes cause visible frame drops. Memory fragments over time. This is particularly bad on mobile devices.
Solution
// WRONG: Instantiate/Destroy every shot public class BadGun : MonoBehaviour { [SerializeField] private GameObject bulletPrefab;
void Fire() { var bullet = Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); Destroy(bullet, 2f); // GC spike every 2 seconds! } }
// RIGHT: Object pooling public class BulletPool : MonoBehaviour { [SerializeField] private Bullet prefab; [SerializeField] private int poolSize = 50;
private Queue<Bullet> _available = new(); private HashSet<Bullet> _inUse = new();
void Awake() { for (int i = 0; i < poolSize; i++) { var bullet = Instantiate(prefab, transform); bullet.gameObject.SetActive(false); _available.Enqueue(bullet); } }
public Bullet Get(Vector3 position, Quaternion rotation) { var bullet = _available.Count > 0 ? _available.Dequeue() : Instantiate(prefab, transform);
bullet.transform.SetPositionAndRotation(position, rotation); bullet.gameObject.SetActive(true); bullet.OnSpawned(); _inUse.Add(bullet);
return bullet; }
public void Return(Bullet bullet) { if (!_inUse.Contains(bullet)) return;
bullet.OnDespawned(); bullet.gameObject.SetActive(false); _inUse.Remove(bullet); _available.Enqueue(bullet); } }
Symptoms
- Frame rate hitches during gameplay
- Profiler shows GC.Alloc spikes
- Memory grows during play sessions
- Stuttering when spawning many objects
Detection Pattern
Instantiate\([^)]+\)[^;];[^}]Destroy\(
Physics In Wrong Update
Id
physics-in-wrong-update
Summary
Physics queries or operations outside FixedUpdate
Why
Physics simulation runs at fixed intervals. Doing physics work in Update means you're working with potentially stale data. Results can be inconsistent between frames. Raycasts in Update are usually fine, but force application is not.
Severity
high
Situation
Rigidbody manipulation, collision detection, or force application in Update
Solution
// WRONG: Setting velocity in Update void Update() { // This runs at variable rate - physics becomes inconsistent _rb.velocity = new Vector3(input.x, _rb.velocity.y, input.z) * speed; }
// RIGHT: Use FixedUpdate for physics void FixedUpdate() { _rb.velocity = new Vector3(_input.x, _rb.velocity.y, _input.z) * speed; }
void Update() { // Read input in Update (runs every frame) _input = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")); }
// For Rigidbody movement without forces, use MovePosition in FixedUpdate void FixedUpdate() { Vector3 newPos = _rb.position + _moveDirection speed Time.fixedDeltaTime; _rb.MovePosition(newPos); }
Symptoms
- Physics feels "jittery"
- Movement inconsistent across frame rates
- Collisions sometimes miss
- Character "slides" on slopes
Detection Pattern
void Update\([^)]\)[^}](\.velocity\s*=|MovePosition|AddForce)
Serialization Trap
Id
serialization-trap
Summary
Not understanding Unity's serialization rules
Severity
high
Situation
Data not saving, prefab overrides not working, values resetting
Why
Unity serialization has specific rules that aren't obvious:
- Private fields need [SerializeField]
- Properties don't serialize
- Static fields don't serialize
- Dictionary doesn't serialize
- Polymorphism requires [SerializeReference]
Breaking these causes silent data loss.
Solution
// WRONG: Expecting these to serialize public class BadSerialization : MonoBehaviour { private int health = 100; // Won't serialize - private without attribute public int Health { get; set; } // Won't serialize - property static int score; // Won't serialize - static public Dictionary<string, int> inventory; // Won't serialize - Dictionary }
// RIGHT: Proper serialization public class GoodSerialization : MonoBehaviour { [SerializeField] private int _health = 100; // Serializes with attribute
[field: SerializeField] // C# 7.3+ - serializes backing field public int Armor { get; private set; }
// Dictionary alternative - use two lists [SerializeField] private List<string> _inventoryKeys; [SerializeField] private List<int> _inventoryValues;
// Or use serializable wrapper [Serializable] public class SerializableDictionary { public List<string> keys = new(); public List<int> values = new(); } [SerializeField] private SerializableDictionary _inventory;
// Polymorphism requires SerializeReference [SerializeReference] private IWeapon _currentWeapon; }
// Check what serializes with debug inspector // Or use SerializationUtility.HasManagedReferencesWithMissingTypes
Symptoms
- Values reset after play mode
- Prefab overrides don't stick
- Data lost between sessions
- Inspector shows unexpected values
Detection Pattern
private\s+\w+\s+\w+\s=\s[^;]+;(?![^}]*\[SerializeField\])
Mobile Performance Traps
Id
mobile-performance-traps
Summary
Not considering mobile device limitations
Severity
high
Situation
Building for iOS/Android without mobile-specific optimizations
Why
Mobile GPUs are fill-rate limited. Mobile CPUs throttle under load. Memory is constrained. Battery drain matters. What runs fine in editor will melt a phone.
Solution
// Mobile performance checklist:
// 1. Draw call batching // Use GPU Instancing, SRP Batcher, or Static/Dynamic batching // Target < 100 draw calls for low-end mobile
// 2. Texture compression // Use ASTC for modern devices, ETC2 for older Android // Generate mipmaps for 3D, disable for UI
// 3. Reduce overdraw // - Use occlusion culling // - Avoid transparent objects // - Use cutout instead of transparent when possible Graphics.activeTier = GraphicsTier.Tier1; // For testing
// 4. Optimize scripts void Update() { // DON'T do expensive operations every frame // Use coroutines or spread across frames }
// Spread work across frames IEnumerator ProcessEnemiesOverFrames() { int processed = 0; foreach (var enemy in _enemies) { enemy.UpdateAI(); processed++; if (processed % 10 == 0) // Process 10 per frame { yield return null; } } }
// 5. Profile on actual devices // Editor performance != Device performance // Test on lowest target device
// 6. Memory management Resources.UnloadUnusedAssets(); // Periodically System.GC.Collect(); // Only at loading screens
Symptoms
- Works in editor, not on device
- Device overheats
- Frame rate drops on mobile
- App gets killed by OS
Detection Pattern
String Animator Parameters
Id
string-animator-parameters
Summary
Using string-based Animator parameter access in hot paths
Severity
high
Situation
Setting animator parameters using strings in Update
Why
String hashing happens every call. Animator.StringToHash caches the hash. The difference is small but adds up across many objects every frame.
Solution
// WRONG: String every frame void Update() { animator.SetFloat("Speed", currentSpeed); // String hash every frame! animator.SetBool("IsGrounded", isGrounded); animator.SetTrigger("Attack"); }
// RIGHT: Cache hashes private static readonly int SpeedHash = Animator.StringToHash("Speed"); private static readonly int IsGroundedHash = Animator.StringToHash("IsGrounded"); private static readonly int AttackHash = Animator.StringToHash("Attack");
void Update() { animator.SetFloat(SpeedHash, currentSpeed); animator.SetBool(IsGroundedHash, isGrounded); animator.SetTrigger(AttackHash); }
Symptoms
- Profiler shows Animator string operations
- Performance degrades with many animated objects
- Unnecessary allocations each frame
Detection Pattern
animator\.(SetFloat|SetBool|SetInteger|SetTrigger)\s\(\s"
Find Methods Runtime
Id
find-methods-runtime
Summary
Using Find methods during gameplay
Severity
critical
Situation
Calling Find, FindObjectOfType, FindObjectsOfType frequently
Why
Find methods search the entire scene hierarchy. O(n) complexity where n is all GameObjects. FindObjectsOfType is even worse - searches all objects of type. Extremely expensive. There's almost never a good reason to use these at runtime.
Solution
// WRONG: Finding at runtime void Update() { var player = GameObject.Find("Player"); // Searches entire hierarchy! var enemies = FindObjectsOfType<Enemy>(); // Searches ALL components! }
// WRONG: Finding in Start (still bad pattern) void Start() { _player = FindObjectOfType<Player>(); // Works but poor architecture }
// RIGHT: Direct references public class EnemyAI : MonoBehaviour { [SerializeField] private Transform _player; // Assign in inspector [SerializeField] private EnemyManager _manager; // Reference manager }
// RIGHT: ScriptableObject registry [CreateAssetMenu] public class PlayerRegistry : ScriptableObject { public Transform CurrentPlayer { get; private set; }
public void Register(Transform player) => CurrentPlayer = player; public void Unregister() => CurrentPlayer = null; }
public class Player : MonoBehaviour { [SerializeField] private PlayerRegistry _registry;
void OnEnable() => _registry.Register(transform); void OnDisable() => _registry.Unregister(); }
// RIGHT: Event-based discovery public class EnemySpawner : MonoBehaviour { public event Action<Enemy> OnEnemySpawned; public event Action<Enemy> OnEnemyDestroyed;
void SpawnEnemy() { var enemy = Instantiate(_prefab); OnEnemySpawned?.Invoke(enemy); } }
Symptoms
- Frame rate drops during gameplay
- Profiler shows Find operations
- Long frames when many objects exist - '"Stuttering" as scene grows'
Detection Pattern
GameObject\.Find\s*\(|FindObjectOfType|FindObjectsOfType
New Allocations Update
Id
new-allocations-update
Summary
Creating new objects in Update causes GC pressure
Severity
high
Situation
new Vector3, new List, string concatenation in Update
Why
Every 'new' allocates memory. Unity's GC is stop-the-world. Enough allocations trigger GC, causing frame drops. This is especially bad on mobile where GC is slower.
Solution
// WRONG: Allocations in Update void Update() { var direction = new Vector3(input.x, 0, input.y); // Allocation! var enemies = new List<Enemy>(); // Allocation! Debug.Log("Position: " + transform.position); // String allocation!
foreach (var item in GetComponents<Collider>()) // Array allocation! { // ... } }
// RIGHT: Reuse and cache private Vector3 _direction; private List<Enemy> _enemiesBuffer = new(); private Collider[] _colliderBuffer = new Collider[10]; private StringBuilder _sb = new();
void Update() { _direction.Set(input.x, 0, input.y); // No allocation
_enemiesBuffer.Clear(); // Reuse list GetEnemiesNonAlloc(_enemiesBuffer);
// Use GetComponentsNonAlloc int count = GetComponents(_colliderBuffer); for (int i = 0; i < count; i++) { var collider = _colliderBuffer[i]; // ... }
// StringBuilder for strings _sb.Clear(); _sb.Append("Position: ").Append(transform.position); Debug.Log(_sb); // Still allocates, but less }
// Use struct instead of class for temporary data public readonly struct DamageEvent { public readonly float Amount; public readonly Vector3 Position; }
Symptoms
- GC.Alloc in profiler
- Periodic frame drops
- Memory grows during play
- Stuttering every few seconds
Detection Pattern
void (Update|FixedUpdate|LateUpdate)\s\([^)]\)[^}](new\s+(List|Vector|Array|\w+\[\])|\+\s")
Addressables Release Forgotten
Id
addressables-release-forgotten
Summary
Not releasing Addressables handles causes memory leaks
Severity
high
Situation
Loading Addressables assets without releasing them
Why
Addressables uses reference counting. Each LoadAssetAsync needs a Release. Without Release, assets stay in memory forever. This is different from Resources which Unity manages automatically.
Solution
// WRONG: Load without release public class BadLoader : MonoBehaviour { async void LoadCharacter() { var handle = Addressables.LoadAssetAsync<GameObject>("character"); await handle.Task; Instantiate(handle.Result); // handle never released - MEMORY LEAK! } }
// RIGHT: Track and release handles public class GoodLoader : MonoBehaviour { private AsyncOperationHandle<GameObject> _handle; private GameObject _instance;
public async UniTask LoadCharacter() { _handle = Addressables.LoadAssetAsync<GameObject>("character"); await _handle;
if (_handle.Status == AsyncOperationStatus.Succeeded) { _instance = Instantiate(_handle.Result); } }
void OnDestroy() { // CRITICAL: Release the handle if (_handle.IsValid()) { Addressables.Release(_handle); }
if (_instance != null) { Destroy(_instance); } } }
// BETTER: Use InstantiateAsync for automatic tracking public async UniTask<GameObject> SpawnTracked(AssetReference reference) { // InstantiateAsync tracks the instance // Destroying the instance releases the reference var handle = reference.InstantiateAsync(); await handle; return handle.Result; }
Symptoms
- Memory grows over time
- Assets stay loaded after scene change
- "Memory pressure" warnings
- App crashes after extended play
Detection Pattern
Addressables\.LoadAssetAsync(?![^}]*Release)
Transform Hierarchy Deep
Id
transform-hierarchy-deep
Summary
Deep transform hierarchies hurt performance
Severity
medium
Situation
Many nested child objects, complex UI hierarchies
Why
Every Transform change propagates to all children. Deep hierarchies mean more matrices to update. UI Canvas rebuilds entire hierarchy on change. 10+ levels deep starts to hurt.
Solution
// WRONG: Deep nesting // Player -> Body -> Arm -> Hand -> Fingers -> Finger1 -> Nail -> ... // Every move of Player recalculates entire chain
// RIGHT: Flatten where possible // Player -> Body, Player -> Arm, Player -> Hand (siblings, not nested)
// For UI specifically: // - Split into multiple Canvases // - Static elements on separate Canvas (no rebuild) // - Dynamic elements on minimal Canvas
// Use Transform.SetParent with worldPositionStays=false // to avoid matrix recalculation child.SetParent(newParent, worldPositionStays: false);
// Detach frequently-moving objects from static hierarchy public class DetachOnStart : MonoBehaviour { [SerializeField] private bool _detachFromParent = true;
void Start() { if (_detachFromParent) { transform.SetParent(null); } } }
// Profile with: Profiler.BeginSample("TransformWork"); // ... transform operations Profiler.EndSample();
Symptoms
- UI feels slow
- Moving parent causes lag
- SetParent is expensive
- Canvas.BuildBatch in profiler
Detection Pattern
Missing Null Checks
Id
missing-null-checks
Summary
Accessing destroyed Unity objects causes errors
Severity
high
Situation
Accessing components/GameObjects that might be destroyed
Why
Unity overloads == for UnityEngine.Object. Destroyed objects == null but aren't C# null. Accessing destroyed objects throws MissingReferenceException. This is common in callbacks, events, and coroutines.
Solution
// WRONG: No null check after potential destruction void OnEnemyKilled(Enemy enemy) { // Enemy might be destroyed by the time this event fires enemy.DropLoot(); // MissingReferenceException! }
// WRONG: C# null check doesn't work for Unity objects if (enemy != null) // This works but... { enemy.DropLoot(); // ...still might throw! }
// RIGHT: Unity null check if (enemy != null && enemy) // Redundant but clear { enemy.DropLoot(); }
// RIGHT: Unity-style null check if (enemy) // Unity's operator bool { enemy.DropLoot(); }
// RIGHT: Explicit destroyed check if (!ReferenceEquals(enemy, null) && enemy != null) { enemy.DropLoot(); }
// For coroutines, check at yield points IEnumerator DelayedAction(Transform target) { yield return new WaitForSeconds(1f);
if (!target) // Target might be destroyed during wait { yield break; }
target.position = newPosition; }
// Use ?. with care - it uses C# null, not Unity null enemy?.DropLoot(); // Won't throw, but might not work as expected
Symptoms
- MissingReferenceException in callbacks
- NullReferenceException after Destroy
- Errors in coroutines
- Race conditions with destruction
Detection Pattern
Destroy\([^)]+\)[^}]\n[^}]\1\.
Unity Development - Validations
GetComponent in Update Loop
Id
unity-getcomponent-in-update
Severity
error
Type
regex
Pattern
void\s+(Update|FixedUpdate|LateUpdate)\s\([^)]\)[^}]GetComponent\s<
Message
GetComponent called in Update loop. Cache the component reference in Awake or Start.
Fix Action
Cache in Awake: private ComponentType _cached; void Awake() => _cached = GetComponent<ComponentType>();
Applies To
- *.cs
GetComponent (non-generic) in Update
Id
unity-getcomponent-in-update-nongeneric
Severity
error
Type
regex
Pattern
void\s+(Update|FixedUpdate|LateUpdate)\s\([^)]\)[^}]GetComponent\s\(\s*typeof
Message
GetComponent(typeof()) called in Update. Cache the reference.
Fix Action
Cache in Awake using the generic version: GetComponent<T>()
Applies To
- *.cs
Find Methods at Runtime
Id
unity-find-at-runtime
Severity
error
Type
regex
Pattern
void\s+(Update|FixedUpdate|Start|OnEnable)\s\([^)]\)[^}]*(GameObject\.Find|FindObjectOfType|FindObjectsOfType|FindGameObjectsWithTag)
Message
Find methods are expensive. Use SerializeField references or event-based architecture.
Fix Action
Replace with: [SerializeField] private TargetType _target; Or use a ScriptableObject registry pattern.
Applies To
- *.cs
Find Methods Usage (Warning)
Id
unity-find-any-usage
Severity
warning
Type
regex
Pattern
(GameObject\.Find\s\(|FindObjectOfType\s<|FindObjectsOfType\s<|FindGameObjectsWithTag\s\()
Message
Find methods should be avoided. Consider using direct references or registries.
Fix Action
Use [SerializeField] references or ScriptableObject registries
Applies To
- *.cs
Physics Operations in Update
Id
unity-physics-in-update
Severity
error
Type
regex
Pattern
void\s+Update\s\([^)]\)[^}](\.AddForce|\.AddTorque|\.velocity\s=|\.angularVelocity\s*=|\.MovePosition|\.MoveRotation)
Message
Physics operations should be in FixedUpdate, not Update.
Fix Action
Move physics code to FixedUpdate()
Applies To
- *.cs
Instantiate/Destroy Pattern
Id
unity-instantiate-destroy-loop
Severity
error
Type
regex
Pattern
Instantiate\s\([^)]+\)[^}]{0,500}Destroy\s\([^)]+\)
Message
Instantiate/Destroy pattern detected. Use object pooling for frequently spawned objects.
Fix Action
Implement an object pool - reuse objects instead of creating/destroying
Applies To
- *.cs
Object Allocation in Update
Id
unity-new-in-update
Severity
warning
Type
regex
Pattern
void\s+(Update|FixedUpdate|LateUpdate)\s\([^)]\)[^}]new\s+(Vector3|Vector2|Quaternion|List|Dictionary|HashSet|StringBuilder)\s\(
Message
Creating new objects in Update causes GC pressure. Reuse or cache.
Fix Action
Cache and reuse: private Vector3 _cachedVector; void Update() { _cachedVector.Set(x, y, z); }
Applies To
- *.cs
String Concatenation in Update
Id
unity-string-concat-update
Severity
warning
Type
regex
Pattern
void\s+(Update|FixedUpdate|LateUpdate)\s\([^)]\)[^}](\+\s"|\+\s*\w+\.ToString\(\))
Message
String concatenation in Update allocates memory. Use StringBuilder or cache.
Fix Action
Use StringBuilder or string interpolation outside hot paths
Applies To
- *.cs
Animator String Parameter in Update
Id
unity-animator-string-update
Severity
warning
Type
regex
Pattern
void\s+Update\s\([^)]\)[^}]_?[aA]nimator\s\.\s(SetFloat|SetBool|SetInteger|SetTrigger)\s\(\s*"
Message
String-based Animator parameter in Update. Cache hash with Animator.StringToHash.
Fix Action
Cache the hash: private static readonly int SpeedHash = Animator.StringToHash("Speed"); void Update() { animator.SetFloat(SpeedHash, speed); }
Applies To
- *.cs
SendMessage Usage
Id
unity-sendmessage
Severity
warning
Type
regex
Pattern
\.(SendMessage|BroadcastMessage|SendMessageUpwards)\s*\(
Message
SendMessage is slow and error-prone. Use direct calls, interfaces, or events.
Fix Action
Use interfaces, UnityEvents, or direct method calls
Applies To
- *.cs
Tag Comparison with String
Id
unity-tag-compare-string
Severity
warning
Type
regex
Pattern
\.tag\s==\s"
Message
String comparison for tags. Use CompareTag() for better performance.
Fix Action
Replace with: gameObject.CompareTag("TagName")
Applies To
- *.cs
StartCoroutine Without Stop
Id
unity-coroutine-no-stop
Severity
warning
Type
regex
Pattern
StartCoroutine\s\([^)]+\)(?)
Message
Coroutine started without stop logic. May cause memory leaks.
Fix Action
Track and stop coroutines: private Coroutine _routine; void OnEnable() { _routine = StartCoroutine(MyRoutine()); } void OnDisable() { if (_routine != null) StopCoroutine(_routine); }
Applies To
- *.cs
Async Void Method
Id
unity-async-void
Severity
warning
Type
regex
Pattern
async\s+void\s+\w+\s*\(
Message
async void cannot be cancelled or awaited. Use async UniTaskVoid or async Task.
Fix Action
Use UniTask: async UniTaskVoid MethodName() with CancellationToken
Applies To
- *.cs
GetComponent Without RequireComponent
Id
unity-missing-requirecomponent
Severity
info
Type
regex
Pattern
GetComponent<(Rigidbody|Rigidbody2D|Collider|Collider2D|Animator|AudioSource)>\s\(\s\)(?![^}]*\[RequireComponent)
Message
GetComponent for common component without RequireComponent attribute.
Fix Action
Add [RequireComponent(typeof(ComponentType))] to the class
Applies To
- *.cs
DestroyImmediate Usage
Id
unity-destroy-immediate
Severity
warning
Type
regex
Pattern
DestroyImmediate\s*\(
Message
DestroyImmediate should only be used in editor scripts. Use Destroy() in runtime.
Fix Action
Replace with Destroy() for runtime, keep DestroyImmediate only in Editor scripts
Applies To
- *.cs
Camera.main in Update
Id
unity-camera-main
Severity
warning
Type
regex
Pattern
void\s+Update\s\([^)]\)[^}]*Camera\.main
Message
Camera.main does a FindWithTag internally. Cache the reference.
Fix Action
Cache in Awake: private Camera _mainCamera; void Awake() => _mainCamera = Camera.main;
Applies To
- *.cs
Potentially Incorrect Null Check
Id
unity-null-check-pattern
Severity
info
Type
regex
Pattern
if\s\(\s\w+\s!=\snull\s\)[^}]\.(transform|gameObject|GetComponent)
Message
Unity objects may pass C# null check but still be destroyed. Use Unity's bool operator.
Fix Action
Use: if (obj) { } instead of if (obj != null) { }
Applies To
- *.cs
Resources.Load in Update
Id
unity-resources-load-update
Severity
error
Type
regex
Pattern
void\s+Update\s\([^)]\)[^}]*Resources\.Load
Message
Resources.Load in Update is extremely expensive. Load and cache in Awake/Start.
Fix Action
Load in Awake/Start and cache the reference
Applies To
- *.cs
Addressables Load Without Release
Id
unity-addressables-no-release
Severity
warning
Type
regex
Pattern
Addressables\.LoadAssetAsync(?![^}]*Addressables\.Release)
Message
Addressables load without apparent release. Remember to release handles.
Fix Action
Track and release: private AsyncOperationHandle _handle; void OnDestroy() { if (_handle.IsValid()) Addressables.Release(_handle); }
Applies To
- *.cs
GetComponents (plural) in Update
Id
unity-getcomponents-update
Severity
error
Type
regex
Pattern
void\s+(Update|FixedUpdate)\s\([^)]\)[^}]GetComponents\s[<\(]
Message
GetComponents allocates a new array every call. Use GetComponentsNonAlloc.
Fix Action
Use non-allocating version: private ComponentType[] _buffer = new ComponentType[10]; void Update() { int count = GetComponents(_buffer); }
Applies To
- *.cs
Allocating Raycast in Update
Id
unity-raycast-allocating
Severity
warning
Type
regex
Pattern
void\s+Update\s\([^)]\)[^}]Physics\.RaycastAll\s\(
Message
RaycastAll allocates. Use RaycastNonAlloc with pre-allocated buffer.
Fix Action
Use non-allocating version: private RaycastHit[] _hits = new RaycastHit[10]; void Update() { int count = Physics.RaycastNonAlloc(ray, _hits); }
Applies To
- *.cs
Allocating Overlap in Update
Id
unity-overlap-allocating
Severity
warning
Type
regex
Pattern
void\s+Update\s\([^)]\)[^}]Physics\.(OverlapSphere|OverlapBox|OverlapCapsule)\s\([^)]*\)
Message
Overlap methods allocate. Use OverlapNonAlloc variants.
Fix Action
Use non-allocating version: private Collider[] _colliders = new Collider[10]; void Update() { int count = Physics.OverlapSphereNonAlloc(pos, radius, _colliders); }
Applies To
- *.cs
Magic Numbers in Game Logic
Id
unity-magic-numbers
Severity
info
Type
regex
Pattern
(speed|velocity|force|damage|health|gravity)\s[+\-/]=?\s*\d+\.\d+
Message
Consider using SerializeField or ScriptableObject for game constants.
Fix Action
Use configurable values: [SerializeField] private float _speed = 5f; Or use ScriptableObject for shared configuration.
Applies To
- *.cs
Public Field Without SerializeField
Id
unity-public-field
Severity
info
Type
regex
Pattern
public\s+(int|float|string|bool|Vector[23]|Transform|GameObject)\s+\w+\s*;
Message
Public fields are serialized by default. Consider [SerializeField] private for encapsulation.
Fix Action
Use: [SerializeField] private TypeName _fieldName;
Applies To
- *.cs
Hardcoded Layer Number
Id
unity-hardcoded-layer
Severity
info
Type
regex
Pattern
LayerMask\s\.\sGetMask\s\(\s\d+|1\s<<\s\d+
Message
Hardcoded layer numbers are fragile. Use LayerMask.GetMask with names.
Fix Action
Use: LayerMask.GetMask("LayerName") or [SerializeField] LayerMask
Applies To
- *.cs
GetComponent Without Null Check in Awake
Id
unity-awake-getcomponent-null
Severity
info
Type
regex
Pattern
void\s+Awake\s\(\s\)[^}]=\sGetComponent\s<[^>]+>\s\(\s\)\s;(?![^}]!=\snull|[^}]==\snull)
Message
GetComponent in Awake without null check. Component might not exist.
Fix Action
Add null check or use TryGetComponent: if (!TryGetComponent<ComponentType>(out _cached)) { Debug.LogError("Missing required component", this); }
Applies To
- *.cs
LINQ in Update Loop
Id
unity-linq-in-update
Severity
warning
Type
regex
Pattern
void\s+(Update|FixedUpdate)\s\([^)]\)[^}]*\.(Where|Select|OrderBy|FirstOrDefault|ToList|ToArray|Count\(\))
Message
LINQ methods allocate. Use loops or cache results.
Fix Action
Replace LINQ with for loops in hot paths
Applies To
- *.cs
Debug.Log Without Conditional
Id
unity-debug-log-build
Severity
info
Type
regex
Pattern
Debug\.(Log|LogWarning|LogError)\s*\([^)]+\)
Message
Debug.Log calls remain in builds. Consider using [Conditional] or removing for release.
Fix Action
Wrap with conditional: [Conditional("UNITY_EDITOR")] void Log(string msg) => Debug.Log(msg);
Applies To
- *.cs
String-Based Invoke
Id
unity-invoke-string
Severity
warning
Type
regex
Pattern
\.(Invoke|InvokeRepeating|CancelInvoke)\s\(\s"
Message
String-based Invoke is slow and error-prone. Use coroutines or async.
Fix Action
Replace with coroutines or async/await patterns
Applies To
- *.cs
Foreach on Collection in Update
Id
unity-foreach-update
Severity
info
Type
regex
Pattern
void\s+Update\s\([^)]\)[^}]foreach\s\([^)]*\s+in\s+\w+\)
Message
foreach may allocate enumerator. Consider using for loop in hot paths.
Fix Action
Use for loop: for (int i = 0; i < list.Count; i++)
Applies To
- *.cs