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; // Serialized private field
private GameObject _target; // Private field
public float MovementSpeed => _movementSpeed; // Public property
void Start()
{
// Local variable
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; // Defaults to private, but implicit
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
/// <summary>
/// Manages the player's health and handles damage/healing.
/// </summary>
public class PlayerHealth : MonoBehaviour
{
/// <summary>The current health of the player.</summary>
public int CurrentHealth { get; private set; } = 100;
/// <summary>
/// Applies damage to the player, reducing their health.
/// </summary>
/// <param name="amount">The amount of damage to apply.</param>
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() {
// Repeatedly calling FindObjectOfType is a performance drain
PlayerController player = FindObjectOfType<PlayerController>();
if (player != null) {
// ...
}
}
}
✅ GOOD
// PlayerController.cs
public class PlayerController : MonoBehaviour
{
public static PlayerController Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
}
}
}
// GameManager.cs
public class GameManager : MonoBehaviour
{
private PlayerController _player;
void Start()
{
_player = PlayerController.Instance; // Cache reference once
}
void Update()
{
if (_player != null)
{
// Use the cached reference
}
}
}
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; // Requires Unity 2021+
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;
// ... set up projectile, then return to pool when done
}
}
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>(); // Allocates every frame
// ... populate and use list
}
✅ GOOD
private readonly List<Enemy> _enemiesInRange = new List<Enemy>(); // Allocated once
void Update() {
_enemiesInRange.Clear(); // Reuse existing list
// ... populate and use list
}
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]; // Pre-allocate array
void CheckForHits() {
int numHits = Physics.RaycastNonAlloc(transform.position, transform.forward, _raycastHits, 10f);
for (int i = 0; i < numHits; i++) {
// Process _raycastHits[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) { // Expensive null check every frame
// ...
}
}
✅ GOOD
private GameObject _myGameObject; // Reference cached in Start/Awake
void Start() {
_myGameObject = GameObject.Find("MyObject");
}
void Update() {
if (_myGameObject) { // Unity's implicit bool conversion is optimized
// ...
}
}
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); // Always included
}
✅ GOOD
using System.Diagnostics; // For [Conditional]
using UnityEngine;
public static class AppLogger
{
[Conditional("ENABLE_LOGS")] // Define ENABLE_LOGS in Player Settings for debug builds
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); // Stripped in release builds
}
}
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.