| name | unity |
| description | [Applies to: **/*] This guide provides opinionated, actionable best practices for Unity C# development, focusing on code organization, performance, and common pitfalls to ensure a clean, maintainable, and performant codebase. |
| source | cursor_mdc |
unity Best Practices
This document outlines our definitive guidelines for Unity C# development. Adhere to these rules to maintain a high-quality, performant, and collaborative codebase.
1. Code Organization & Project Structure
1.1 Project Folder Hierarchy
Organize assets by type within a _Project root, separating custom assets from third-party packages. Avoid root-level clutter.
❌ BAD
Assets/
MyScript.cs
PlayerModel.fbx
Level1.unity
ThirdPartyAsset/
...
✅ GOOD
Assets/
_Project/
Audio/
Materials/
Models/
Prefabs/
Scenes/
Scripts/
Core/
Gameplay/
UI/
Textures/
Plugins/ // Reserved for external plugins
Standard Assets/ // Reserved for Unity's standard assets
ThirdPartyAsset/ // Leave imported packages intact
1.2 Scene Hierarchy Structure
Prefab everything. Group GameObjects logically within scenes for clarity and easier navigation.
❌ BAD
- Player
- Enemy1
- UI_HealthBar
- MainCamera
- Light
- Environment_Tree
✅ GOOD
- Managers
- GameManager
- AudioManager
- Cameras
- MainCamera
- Lights
- Directional Light
- UI
- Canvas
- HUD
- PauseMenu
- World
- Terrain
- Environment
- Trees
- Rocks
- Gameplay
- Actors
- Player
- Enemy (Prefab instances)
- Items
- Collectible (Prefab instances)
- _DynamicObjects // Parent for all runtime instantiated objects
1.3 Naming Conventions
Use PascalCase for public members and filenames, camelCase for private fields and local variables. Prefix private fields with _.
❌ BAD
public class player_controller : MonoBehaviour {
public float movementSpeed;
private GameObject target;
void Start() { }
}
✅ GOOD
public class PlayerController : MonoBehaviour
{
[SerializeField] private float _movementSpeed = 5f;
private GameObject _target;
public float MovementSpeed => _movementSpeed;
void Start()
{
GameObject tempObject = new GameObject("Temp");
}
}
2. Common Patterns & Anti-patterns
2.1 Explicit Access Modifiers
Always declare access modifiers (public, private, protected). Do not rely on default private.
❌ BAD
class MyClass {
int value;
void DoSomething() { }
}
✅ GOOD
public class MyClass
{
private int _value;
public void DoSomething() { }
}
2.2 XML Documentation
Document all public classes, methods, and fields using XML /// comments.
❌ BAD
public class PlayerHealth : MonoBehaviour {
public int currentHealth;
public void TakeDamage(int amount) { }
}
✅ GOOD
public class PlayerHealth : MonoBehaviour
{
public int CurrentHealth { get; private set; } = 100;
public void TakeDamage(int amount)
{
CurrentHealth -= amount;
if (CurrentHealth <= 0)
{
Debug.Log("Player defeated!");
}
}
}
2.3 Singleton Access
Cache references to singletons or use a dedicated manager. Avoid FindObjectOfType in Update or frequently called methods.
❌ BAD
public class GameManager : MonoBehaviour {
void Update() {
PlayerController player = FindObjectOfType<PlayerController>();
if (player != null) {
}
}
}
✅ GOOD
public class PlayerController : MonoBehaviour
{
public static PlayerController Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
}
}
}
public class GameManager : MonoBehaviour
{
private PlayerController _player;
void Start()
{
_player = PlayerController.Instance;
}
void Update()
{
if (_player != null)
{
}
}
}
3. Performance Considerations
3.1 String vs. ID Lookups
Always use ID-based lookups for Animator, Material, and Shader properties to avoid runtime string hashing.
❌ BAD
animator.SetBool("IsWalking", true);
material.SetFloat("_Glossiness", 0.5f);
✅ GOOD
private static readonly int _isWalkingHash = Animator.StringToHash("IsWalking");
private static readonly int _glossinessID = Shader.PropertyToID("_Glossiness");
void SetProperties(Animator animator, Material material)
{
animator.SetBool(_isWalkingHash, true);
material.SetFloat(_glossinessID, 0.5f);
}
3.2 Garbage Collection (GC) Avoidance
Minimize per-frame allocations. Use object pooling, reuse collections, and avoid unnecessary boxing or string concatenations in hot paths.
3.2.1 Object Pooling
Reuse frequently instantiated objects (e.g., projectiles, particles) instead of destroying and recreating them.
❌ BAD
void FireProjectile() {
Instantiate(projectilePrefab, transform.position, Quaternion.identity);
}
✅ GOOD
using UnityEngine.Pool;
public class ProjectileSpawner : MonoBehaviour
{
public GameObject projectilePrefab;
private ObjectPool<GameObject> _projectilePool;
void Awake()
{
_projectilePool = new ObjectPool<GameObject>(
() => Instantiate(projectilePrefab),
(obj) => obj.SetActive(true),
(obj) => obj.SetActive(false),
(obj) => Destroy(obj),
collectionCheck: false,
defaultCapacity: 20,
maxSize: 100
);
}
public void FireProjectile()
{
GameObject projectile = _projectilePool.Get();
projectile.transform.position = transform.position;
}
}
3.2.2 Collection Reuse
Declare collections once and clear them for reuse instead of allocating new ones in loops.
❌ BAD
void Update() {
List<Enemy> enemiesInRange = new List<Enemy>();
}
✅ GOOD
private readonly List<Enemy> _enemiesInRange = new List<Enemy>();
void Update() {
_enemiesInRange.Clear();
}
3.2.3 Non-Allocating Physics APIs
Use NonAlloc versions of physics queries to prevent GC spikes.
❌ BAD
RaycastHit[] hits = Physics.RaycastAll(transform.position, transform.forward, 10f);
✅ GOOD
private RaycastHit[] _raycastHits = new RaycastHit[10];
void CheckForHits() {
int numHits = Physics.RaycastNonAlloc(transform.position, transform.forward, _raycastHits, 10f);
for (int i = 0; i < numHits; i++) {
}
}
3.3 UnityEngine.Object Null Comparisons
Comparisons against UnityEngine.Object subclasses are more expensive than plain C# objects. Cache references and avoid in tight loops.
❌ BAD
void Update() {
if (myGameObject != null) {
}
}
✅ GOOD
private GameObject _myGameObject;
void Start() {
_myGameObject = GameObject.Find("MyObject");
}
void Update() {
if (_myGameObject) {
}
}
3.4 Debug Logging
Wrap debug logs in [Conditional] attributes to strip them from non-development builds, preventing performance overhead and log spam in production.
❌ BAD
void Update() {
Debug.Log("Player position: " + transform.position);
}
✅ GOOD
using System.Diagnostics;
using UnityEngine;
public static class AppLogger
{
[Conditional("ENABLE_LOGS")]
public static void Log(string message)
{
Debug.Log($"[{Time.frameCount}] {message}");
}
[Conditional("ENABLE_LOGS")]
public static void LogWarning(string message)
{
Debug.LogWarning($"[{Time.frameCount}] {message}");
}
}
public class PlayerMovement : MonoBehaviour
{
void Update()
{
AppLogger.Log("Player position: " + transform.position);
}
}
4. Common Pitfalls & Gotchas
4.1 Spaces in File/Folder Names
Avoid spaces in asset names. Unity's command-line tools and some platforms have issues with paths containing spaces. Use PascalCase.
❌ BAD
Assets/My Game Scene.unity
Assets/Player Controller.cs
✅ GOOD
Assets/MyGameScene.unity
Assets/PlayerController.cs
4.2 Modifying Third-Party Assets
Never modify imported third-party asset files directly. Copy them to your _Project folder and modify the copy. This prevents update conflicts.
❌ BAD
// Directly modifying a script inside Assets/ThirdPartyAsset/Scripts/EnemyAI.cs
namespace ThirdParty {
public class EnemyAI : MonoBehaviour {
// Your custom changes here
}
}
✅ GOOD
// Create Assets/_Project/Scripts/CustomEnemyAI.cs
namespace MyGame {
public class CustomEnemyAI : ThirdParty.EnemyAI { // Inherit if possible
// Your custom changes/overrides here
}
}
// Or, if inheritance isn't suitable, copy the file and modify the copy in _Project.