Appendix A - Best C# Practices For Unity Game Developers
Introduction
C# is the primary programming language used in Unity game development. While general C# best practices apply to Unity development, there are specific considerations and patterns that are particularly important in game development contexts. This guide provides Unity-specific C# best practices to help you write efficient, maintainable, and performant game code.
Unity-Specific C# Coding Style
Unity Coding Conventions
-
Unity's Recommended Practices:
- Follow Unity's recommended coding practices, which build upon standard C# conventions but include game-specific considerations. For detailed guidance, visit Unity Scripting Best Practices.
-
MonoBehaviour Naming and Organization:
- Component Naming: Name MonoBehaviour scripts after their functionality (e.g.,
PlayerController
,EnemySpawner
). - Method Order: Organize Unity lifecycle methods in chronological order (
Awake
,Start
,Update
, etc.). - Serialized Fields: Use
[SerializeField]
for inspector-exposed private fields rather than making fields public.
public class PlayerController : MonoBehaviour
{
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float jumpForce = 10f;
private Rigidbody2D rb;
private bool isGrounded;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
}
private void Start()
{
// Initialization code
}
private void Update()
{
HandleInput();
}
private void FixedUpdate()
{
HandleMovement();
}
private void HandleInput()
{
// Input processing logic
}
private void HandleMovement()
{
// Movement implementation
}
} - Component Naming: Name MonoBehaviour scripts after their functionality (e.g.,
Unity-Specific Naming Conventions
-
Script Naming:
- Name scripts to match their class names and functionality (e.g.,
PlayerController.cs
contains thePlayerController
class). - Use PascalCase for both file names and class names.
- Name scripts to match their class names and functionality (e.g.,
-
Unity-Specific Naming Patterns:
- Prefix interfaces with "I" (e.g.,
IDamageable
). - Use descriptive suffixes for scriptable objects (e.g.,
WeaponData
,EnemyConfig
). - Name event handlers with "On" prefix (e.g.,
OnPlayerDeath
,OnLevelComplete
).
- Prefix interfaces with "I" (e.g.,
-
Inspector-Friendly Naming:
- Use spaces in serialized field names with the
[DisplayName]
attribute for better inspector readability.
[SerializeField]
[DisplayName("Maximum Health")]
private float maxHealth = 100f; - Use spaces in serialized field names with the
Unity Performance Optimization
Memory Management in Unity
-
Garbage Collection Awareness:
- Minimize garbage generation, especially in frequently called methods like
Update()
. - Cache references to components and avoid
GetComponent<T>()
calls in update loops.
// Bad practice - generates garbage every frame
private void Update()
{
Vector3 direction = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
transform.Translate(direction * moveSpeed * Time.deltaTime);
}
// Good practice - reuses the vector
private Vector3 _direction;
private void Update()
{
_direction.x = Input.GetAxis("Horizontal");
_direction.y = 0;
_direction.z = Input.GetAxis("Vertical");
transform.Translate(_direction * moveSpeed * Time.deltaTime);
} - Minimize garbage generation, especially in frequently called methods like
-
Object Pooling:
- Implement object pooling for frequently instantiated and destroyed objects like projectiles, particles, or enemies.
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
[SerializeField] private int poolSize = 20;
private List<GameObject> pool;
private void Awake()
{
pool = new List<GameObject>(poolSize);
for (int i = 0; i < poolSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Add(obj);
}
}
public GameObject GetPooledObject()
{
for (int i = 0; i < pool.Count; i++)
{
if (!pool[i].activeInHierarchy)
{
return pool[i];
}
}
// If no inactive objects, return null or expand pool
return null;
}
}
Efficient Unity Scripting
-
Update Method Optimization:
- Use the appropriate update method for each task:
Update()
: For input handling and regular game logic.FixedUpdate()
: For physics calculations.LateUpdate()
: For camera follow and final position adjustments.Coroutines
: For time-based operations that don't need to run every frame.
- Use the appropriate update method for each task:
-
Coroutines vs. Update:
- Use coroutines for operations that occur over time but don't need to be checked every frame.
// Instead of checking in Update
private float cooldownTimer = 0f;
private float cooldownDuration = 2f;
private void Update()
{
if (cooldownTimer > 0)
{
cooldownTimer -= Time.deltaTime;
}
}
// Use a coroutine
private IEnumerator CooldownCoroutine()
{
yield return new WaitForSeconds(cooldownDuration);
// Cooldown complete
} -
Physics Optimization:
- Use non-alloc physics methods to reduce garbage generation.
- Implement physics layers and collision matrices to limit unnecessary collision checks.
// Preallocate array to avoid garbage
private RaycastHit[] hits = new RaycastHit[10];
private void CheckForEnemies()
{
int hitCount = Physics.SphereCastNonAlloc(
transform.position,
detectionRadius,
transform.forward,
hits,
0f,
enemyLayerMask
);
for (int i = 0; i < hitCount; i++)
{
// Process hits
}
}
Unity-Specific Design Patterns
Component-Based Architecture
-
Single Responsibility Principle:
- Create focused components that do one thing well rather than monolithic scripts.
- Compose complex behaviors from simple components.
// Instead of one large Player class
public class PlayerHealth : MonoBehaviour { /* ... */ }
public class PlayerMovement : MonoBehaviour { /* ... */ }
public class PlayerCombat : MonoBehaviour { /* ... */ }
public class PlayerInventory : MonoBehaviour { /* ... */ } -
Component Communication:
- Use Unity's built-in methods for component communication:
- Direct references for tightly coupled components.
GetComponent<T>()
for occasional access.- Events and delegates for decoupled communication.
- ScriptableObjects for shared data.
// Using events for decoupled communication
public class PlayerHealth : MonoBehaviour
{
public event Action<float> OnHealthChanged;
public event Action OnPlayerDeath;
private float currentHealth;
public void TakeDamage(float damage)
{
currentHealth -= damage;
OnHealthChanged?.Invoke(currentHealth);
if (currentHealth <= 0)
{
OnPlayerDeath?.Invoke();
}
}
}
public class UIHealthBar : MonoBehaviour
{
[SerializeField] private PlayerHealth playerHealth;
[SerializeField] private Image healthBarFill;
private void OnEnable()
{
playerHealth.OnHealthChanged += UpdateHealthBar;
}
private void OnDisable()
{
playerHealth.OnHealthChanged -= UpdateHealthBar;
}
private void UpdateHealthBar(float health)
{
healthBarFill.fillAmount = health / 100f;
}
} - Use Unity's built-in methods for component communication:
Scriptable Objects for Data Management
-
Data-Driven Design:
- Use ScriptableObjects to store game data separate from behavior.
- Create modular, reusable data assets for items, enemies, levels, etc.
[CreateAssetMenu(fileName = "NewWeaponData", menuName = "Game/Weapon Data")]
public class WeaponData : ScriptableObject
{
public string weaponName;
public Sprite weaponIcon;
public GameObject weaponPrefab;
public float damage;
public float fireRate;
public AudioClip fireSound;
}
public class Weapon : MonoBehaviour
{
[SerializeField] private WeaponData weaponData;
private float nextFireTime;
public void Fire()
{
if (Time.time >= nextFireTime)
{
// Use weaponData to implement firing logic
nextFireTime = Time.time + 1f / weaponData.fireRate;
}
}
} -
ScriptableObject Events:
- Implement a ScriptableObject-based event system for global events.
[CreateAssetMenu(fileName = "NewGameEvent", menuName = "Game/Game Event")]
public class GameEvent : ScriptableObject
{
private List<GameEventListener> listeners = new List<GameEventListener>();
public void Raise()
{
for (int i = listeners.Count - 1; i >= 0; i--)
{
listeners[i].OnEventRaised();
}
}
public void RegisterListener(GameEventListener listener)
{
if (!listeners.Contains(listener))
listeners.Add(listener);
}
public void UnregisterListener(GameEventListener listener)
{
if (listeners.Contains(listener))
listeners.Remove(listener);
}
}
public class GameEventListener : MonoBehaviour
{
[SerializeField] private GameEvent gameEvent;
[SerializeField] private UnityEvent response;
private void OnEnable()
{
gameEvent.RegisterListener(this);
}
private void OnDisable()
{
gameEvent.UnregisterListener(this);
}
public void OnEventRaised()
{
response.Invoke();
}
}
Unity-Specific C# Security and Stability
Safe Unity Serialization
-
Serialization Best Practices:
- Mark non-serialized fields with
[System.NonSerialized]
or make themprivate
. - Use
[SerializeReference]
for polymorphic serialization in Unity 2019.3+. - Implement
ISerializationCallbackReceiver
for custom serialization logic.
- Mark non-serialized fields with
-
Scene References and Prefabs:
- Use prefab variants instead of duplicating prefabs with minor changes.
- Implement runtime initialization for objects that need scene-specific setup.
- Use addressables for large assets and dynamic loading.
Error Handling in Unity
-
Unity-Specific Debugging:
- Use
Debug.Log()
,Debug.LogWarning()
, andDebug.LogError()
appropriately. - Implement custom debug visualization with
OnDrawGizmos()
andOnDrawGizmosSelected()
. - Create editor tools for complex debugging scenarios.
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, detectionRadius);
Gizmos.color = Color.blue;
Gizmos.DrawRay(transform.position, transform.forward * attackRange);
} - Use
-
Graceful Error Recovery:
- Implement fallback behaviors for missing components or assets.
- Use try-catch blocks for operations that might fail at runtime.
- Log detailed error information to help with debugging.
public void LoadLevel(string levelName)
{
try
{
SceneManager.LoadScene(levelName);
}
catch (Exception e)
{
Debug.LogError($"Failed to load level '{levelName}': {e.Message}");
// Fallback to main menu or default level
SceneManager.LoadScene("MainMenu");
}
}
Unity Editor Extensions
Custom Editors and Inspector Tools
-
Custom Inspectors:
- Create custom inspectors for complex components to improve workflow.
- Use
[CustomEditor]
attribute to extend the Unity editor.
[CustomEditor(typeof(EnemySpawner))]
public class EnemySpawnerEditor : Editor
{
public override void OnInspectorGUI()
{
EnemySpawner spawner = (EnemySpawner)target;
DrawDefaultInspector();
EditorGUILayout.Space();
if (GUILayout.Button("Spawn Enemy"))
{
spawner.SpawnEnemy();
}
if (GUILayout.Button("Clear All Enemies"))
{
spawner.ClearEnemies();
}
}
} -
Editor Tools:
- Create editor tools for repetitive tasks or complex setups.
- Use
EditorWindow
for custom windows and tools.
public class LevelSetupWindow : EditorWindow
{
private GameObject playerPrefab;
private GameObject[] enemyPrefabs;
private int enemyCount = 5;
[MenuItem("Tools/Level Setup")]
public static void ShowWindow()
{
GetWindow<LevelSetupWindow>("Level Setup");
}
private void OnGUI()
{
GUILayout.Label("Level Setup Tool", EditorStyles.boldLabel);
playerPrefab = (GameObject)EditorGUILayout.ObjectField("Player Prefab", playerPrefab, typeof(GameObject), false);
EditorGUILayout.Space();
GUILayout.Label("Enemy Setup", EditorStyles.boldLabel);
enemyCount = EditorGUILayout.IntField("Enemy Count", enemyCount);
if (GUILayout.Button("Setup Level"))
{
SetupLevel();
}
}
private void SetupLevel()
{
// Implementation of level setup logic
}
}
Conclusion
Applying these Unity-specific C# best practices will help you create games that are not only functional but also maintainable, performant, and scalable. Remember that game development often requires balancing between ideal code architecture and practical performance considerations. Always profile your game to identify bottlenecks and optimize the areas that will have the most significant impact on player experience.
As you continue your Unity development journey, regularly revisit these practices and stay updated with the latest Unity features and optimizations. The Unity ecosystem evolves rapidly, and new tools and techniques are constantly emerging to help you write better game code.