C# for Game Development: Ultimate Cheatsheet & Reference Guide

Introduction

C# is a versatile, object-oriented programming language widely used in game development, particularly with engines like Unity. This cheatsheet provides essential C# concepts, patterns, and techniques specifically tailored for game development to help you write efficient, maintainable code for your games.

Core C# Concepts for Game Development

Basic Syntax & Types

// Variables and basic types
int health = 100;               // Integer
float speed = 5.5f;             // Float (note the 'f' suffix)
bool isAlive = true;            // Boolean
string playerName = "Player1";  // String
Vector3 position = new Vector3(0, 0, 0);  // Unity Vector3

// Arrays and collections
int[] scores = new int[5];      // Fixed-size array
List<string> inventory = new List<string>();  // Dynamic list
Dictionary<string, int> itemCounts = new Dictionary<string, int>();  // Key-value pairs

// Constants and readonly
const int MAX_HEALTH = 100;     // Compile-time constant
readonly float GRAVITY = 9.81f;  // Runtime constant

Object-Oriented Programming in C#

// Class definition
public class Player : MonoBehaviour
{
    // Fields (class variables)
    public int health;
    private float _speed;
    
    // Properties
    public float Speed {
        get { return _speed; }
        set { _speed = Mathf.Clamp(value, 1f, 10f); }
    }
    
    // Methods
    public void TakeDamage(int amount) {
        health -= amount;
        if (health <= 0) {
            Die();
        }
    }
    
    private void Die() {
        // Death logic
    }
}

// Inheritance
public class Enemy : Character
{
    // Override parent method
    public override void Attack() {
        // Custom enemy attack logic
    }
}

// Interface implementation
public class Weapon : MonoBehaviour, IDamageable
{
    public void ApplyDamage(float amount) {
        // Damage logic
    }
}

C# Access Modifiers

ModifierAccess LevelUse Case in Games
publicAccessible from any codeFor components that need to be exposed in Unity Inspector
privateOnly within the same classFor internal implementation details
protectedWithin the same class or derived classesFor methods that subclasses might need to override
internalWithin the same assemblyFor systems that shouldn’t be accessed by external plugins
protected internalWithin the same assembly or derived classesFor base functionality that might be extended

Unity-Specific C# Concepts

MonoBehaviour Lifecycle Methods

// Called when script instance is loaded
void Awake() {
    // Initialize components, set references
}

// Called before first frame update
void Start() {
    // Start game logic, initialize state that depends on other components
}

// Called once per frame
void Update() {
    // Handle input, movement, continuous game logic
    // Time-dependent: frame rate varies
    // Use for visual updates, input detection
}

// Called at fixed time intervals (default: 0.02s)
void FixedUpdate() {
    // Physics calculations, consistent movement
    // Time-independent: always same frequency
    // Use for physics and consistent movement
}

// Called after all Update functions
void LateUpdate() {
    // Final position adjustments, camera following
}

// Called when component is enabled
void OnEnable() {
    // Subscribe to events, enable functionality
}

// Called when component is disabled
void OnDisable() {
    // Unsubscribe from events, disable functionality
}

// Called when object is destroyed
void OnDestroy() {
    // Clean up resources, save data
}

Coroutines for Time-Based Operations

// Start a coroutine
StartCoroutine(SpawnEnemies());

// Coroutine definition
IEnumerator SpawnEnemies() {
    while (gameActive) {
        Instantiate(enemyPrefab, spawnPoint.position, Quaternion.identity);
        yield return new WaitForSeconds(spawnRate);
    }
}

// Common yield instructions
yield return null;                   // Wait for next frame
yield return new WaitForSeconds(1f); // Wait for 1 second (real-time)
yield return new WaitForFixedUpdate(); // Wait for next physics update
yield return new WaitUntil(() => playerIsReady); // Wait for condition
yield return StartCoroutine(AnotherCoroutine()); // Wait for another coroutine

Common Game Development Patterns in C#

Singleton Pattern

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }
    
    private void Awake() {
        if (Instance == null) {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        } else {
            Destroy(gameObject);
        }
    }
}

// Usage
GameManager.Instance.StartGame();

Object Pooling

public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int poolSize = 20;
    
    private List<GameObject> pool;
    
    private void Awake() {
        pool = new List<GameObject>();
        
        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, expand pool
        GameObject obj = Instantiate(prefab);
        obj.SetActive(false);
        pool.Add(obj);
        return obj;
    }
}

// Usage
GameObject bullet = objectPool.GetPooledObject();
bullet.transform.position = gunPoint.position;
bullet.SetActive(true);

State Machine Pattern

public enum EnemyState { Idle, Patrol, Chase, Attack }

public class Enemy : MonoBehaviour
{
    private EnemyState currentState;
    
    private void Update() {
        switch (currentState) {
            case EnemyState.Idle:
                UpdateIdleState();
                break;
            case EnemyState.Patrol:
                UpdatePatrolState();
                break;
            case EnemyState.Chase:
                UpdateChaseState();
                break;
            case EnemyState.Attack:
                UpdateAttackState();
                break;
        }
    }
    
    private void ChangeState(EnemyState newState) {
        // Exit current state
        switch (currentState) {
            case EnemyState.Idle:
                ExitIdleState();
                break;
            // Other exit states...
        }
        
        currentState = newState;
        
        // Enter new state
        switch (currentState) {
            case EnemyState.Idle:
                EnterIdleState();
                break;
            // Other enter states...
        }
    }
    
    // State methods...
}

Observer Pattern with C# Events

// Event publisher
public class Player : MonoBehaviour
{
    // Event declaration
    public event Action OnDeath;
    public event Action<int> OnHealthChanged;
    
    private int health = 100;
    
    public void TakeDamage(int damage) {
        health -= damage;
        // Trigger event with parameter
        OnHealthChanged?.Invoke(health);
        
        if (health <= 0) {
            // Trigger event
            OnDeath?.Invoke();
        }
    }
}

// Event subscriber
public class GameManager : MonoBehaviour
{
    [SerializeField] private Player player;
    
    private void OnEnable() {
        player.OnDeath += HandlePlayerDeath;
        player.OnHealthChanged += UpdateHealthUI;
    }
    
    private void OnDisable() {
        player.OnDeath -= HandlePlayerDeath;
        player.OnHealthChanged -= UpdateHealthUI;
    }
    
    private void HandlePlayerDeath() {
        // Game over logic
    }
    
    private void UpdateHealthUI(int newHealth) {
        // Update UI
    }
}

Unity-Specific C# Techniques

Input System

// Legacy Input System
void Update() {
    // Keyboard and mouse input
    if (Input.GetKeyDown(KeyCode.Space)) {
        Jump();
    }
    
    // Axis input (for smoother movement)
    float horizontalInput = Input.GetAxis("Horizontal");
    float verticalInput = Input.GetAxis("Vertical");
    
    Vector3 movement = new Vector3(horizontalInput, 0, verticalInput);
    transform.Translate(movement * speed * Time.deltaTime);
    
    // Mouse input
    if (Input.GetMouseButtonDown(0)) {
        Shoot();
    }
}

// New Input System (requires Input System package)
private PlayerInput playerInput;
private InputAction moveAction;

private void Awake() {
    playerInput = GetComponent<PlayerInput>();
    moveAction = playerInput.actions["Move"];
}

private void Update() {
    Vector2 moveInput = moveAction.ReadValue<Vector2>();
    Vector3 movement = new Vector3(moveInput.x, 0, moveInput.y);
    transform.Translate(movement * speed * Time.deltaTime);
}

// Set up callbacks
private void OnEnable() {
    playerInput.actions["Jump"].performed += OnJump;
    playerInput.actions["Fire"].performed += OnFire;
}

private void OnDisable() {
    playerInput.actions["Jump"].performed -= OnJump;
    playerInput.actions["Fire"].performed -= OnFire;
}

private void OnJump(InputAction.CallbackContext context) {
    Jump();
}

private void OnFire(InputAction.CallbackContext context) {
    Shoot();
}

Physics Interactions

// Raycasting
void ShootRaycast() {
    Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    RaycastHit hit;
    
    if (Physics.Raycast(ray, out hit, 100f)) {
        if (hit.collider.CompareTag("Enemy")) {
            hit.collider.GetComponent<Enemy>().TakeDamage(10);
        }
    }
}

// Collision detection
private void OnCollisionEnter(Collision collision) {
    if (collision.gameObject.CompareTag("Enemy")) {
        TakeDamage(10);
    }
}

// Trigger detection
private void OnTriggerEnter(Collider other) {
    if (other.CompareTag("Pickup")) {
        CollectItem(other.gameObject);
    }
}

// Physics movement
void MoveRigidbody() {
    Rigidbody rb = GetComponent<Rigidbody>();
    
    // Apply force (gradually accelerates)
    rb.AddForce(Vector3.forward * force);
    
    // Apply impulse (instant force)
    rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
    
    // Set velocity directly (overrides current velocity)
    rb.velocity = new Vector3(horizontalInput * speed, rb.velocity.y, verticalInput * speed);
}

Data Persistence

// PlayerPrefs for simple data
void SaveData() {
    PlayerPrefs.SetInt("HighScore", highScore);
    PlayerPrefs.SetString("PlayerName", playerName);
    PlayerPrefs.Save();
}

void LoadData() {
    highScore = PlayerPrefs.GetInt("HighScore", 0);  // Default 0
    playerName = PlayerPrefs.GetString("PlayerName", "Player");  // Default "Player"
}

// JSON serialization for complex data
[System.Serializable]
public class SaveData {
    public int level;
    public float health;
    public Vector3 position;
    public List<string> inventory;
}

void SaveToJSON() {
    SaveData data = new SaveData {
        level = currentLevel,
        health = playerHealth,
        position = player.transform.position,
        inventory = playerInventory
    };
    
    string json = JsonUtility.ToJson(data);
    File.WriteAllText(Application.persistentDataPath + "/save.json", json);
}

void LoadFromJSON() {
    string path = Application.persistentDataPath + "/save.json";
    if (File.Exists(path)) {
        string json = File.ReadAllText(path);
        SaveData data = JsonUtility.FromJson<SaveData>(json);
        
        currentLevel = data.level;
        playerHealth = data.health;
        player.transform.position = data.position;
        playerInventory = data.inventory;
    }
}

Optimization Techniques for Unity C#

Memory Management

// Avoid allocations in frequent methods
void Update() {
    // Bad: Creates garbage each frame
    GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
    
    // Good: Cache reference
    if (_enemies == null) {
        _enemies = GameObject.FindGameObjectsWithTag("Enemy");
    }
}

// Use object pooling for frequently created/destroyed objects
// See Object Pooling pattern above

// Use structs for small data types
public struct InventoryItem {
    public int id;
    public int count;
}

// Avoid string concatenation in Update
void Update() {
    // Bad: Creates garbage each frame
    debugText.text = "Score: " + score + " | Health: " + health;
    
    // Good: Use string.Format or string interpolation
    debugText.text = string.Format("Score: {0} | Health: {1}", score, health);
    // or
    debugText.text = $"Score: {score} | Health: {health}";
}

Performance Comparison: Common C# Approaches

OperationEfficient ApproachInefficient ApproachPerformance Impact
Finding ObjectsCache references in Start/AwakeUse GameObject.Find in UpdateReduces per-frame overhead
Vector MathUse Vector3.sqrMagnitudeUse Vector3.magnitudeAvoids expensive square root
String OperationsUse StringBuilder for multiple concatenationsUse string + string repeatedlyReduces garbage collection
CollectionsUse List<T> for dynamic collectionsUse arrays with resizingBetter memory management
Foreach loopsUse for loops with direct indexingUse foreach on collectionsAvoids enumerator allocation
PhysicsUse non-alloc Physics methodsUse standard Physics methodsReduces garbage collection
ComponentsUse GetComponent in Start/Awake and cacheCall GetComponent repeatedlyReduces per-frame overhead

Multithreading in Unity (with limitations)

// Use Jobs System for performance-critical code (requires Jobs package)
using Unity.Collections;
using Unity.Jobs;

public struct ProcessDataJob : IJob {
    public NativeArray<float> input;
    public NativeArray<float> output;
    
    public void Execute() {
        for (int i = 0; i < input.Length; i++) {
            output[i] = Mathf.Sqrt(input[i]);
        }
    }
}

void ProcessDataInParallel() {
    NativeArray<float> input = new NativeArray<float>(1000, Allocator.TempJob);
    NativeArray<float> output = new NativeArray<float>(1000, Allocator.TempJob);
    
    // Fill input data
    for (int i = 0; i < input.Length; i++) {
        input[i] = i;
    }
    
    // Create and schedule job
    ProcessDataJob job = new ProcessDataJob {
        input = input,
        output = output
    };
    
    JobHandle handle = job.Schedule();
    handle.Complete(); // Wait for job to complete
    
    // Use output data
    for (int i = 0; i < output.Length; i++) {
        Debug.Log(output[i]);
    }
    
    // Clean up
    input.Dispose();
    output.Dispose();
}

// For simple background tasks, use System.Threading.Tasks
using System.Threading.Tasks;

async void ProcessDataAsync() {
    // Perform heavy calculation off the main thread
    int result = await Task.Run(() => {
        int sum = 0;
        for (int i = 0; i < 1000000; i++) {
            sum += i;
        }
        return sum;
    });
    
    // Back on main thread
    Debug.Log($"Result: {result}");
}

Common Challenges and Solutions

Null Reference Exceptions

// Problem: Getting null reference exceptions
private Rigidbody rb;

void Start() {
    // Forgot to assign rb, will cause NullReferenceException later
}

// Solutions:

// 1. Initialization in Awake/Start
void Awake() {
    rb = GetComponent<Rigidbody>();
    if (rb == null) {
        Debug.LogError("Rigidbody component missing!");
    }
}

// 2. Null checking before use
void Update() {
    if (rb != null) {
        rb.AddForce(Vector3.forward);
    }
}

// 3. Using [SerializeField] and checking in Editor
[SerializeField] private Rigidbody rb;

private void OnValidate() {
    if (rb == null) {
        rb = GetComponent<Rigidbody>();
    }
}

// 4. Using C# 8.0+ Null-conditional operator
void Update() {
    rb?.AddForce(Vector3.forward);
}

Performance Issues

// Problem: Game slows down with many GameObjects

// Solutions:

// 1. Use object pooling (see pattern above)

// 2. Optimize Update calls
// Only update when needed
public class EnemyAI : MonoBehaviour {
    private float updateInterval = 0.5f;
    private float timeSinceLastUpdate = 0f;
    
    void Update() {
        timeSinceLastUpdate += Time.deltaTime;
        
        if (timeSinceLastUpdate >= updateInterval) {
            UpdateAI();
            timeSinceLastUpdate = 0f;
        }
    }
}

// 3. Batch similar operations
// Instead of:
foreach (Enemy enemy in enemies) {
    enemy.UpdatePath();
}

// Better:
Vector3 targetPosition = player.position;
foreach (Enemy enemy in enemies) {
    enemy.UpdatePathToTarget(targetPosition);
}

Scene Transitions and Data Persistence

// Problem: Losing data between scenes

// Solutions:

// 1. Use DontDestroyOnLoad
public class GameManager : MonoBehaviour {
    void Awake() {
        DontDestroyOnLoad(gameObject);
    }
}

// 2. Use static classes (be careful with this)
public static class GameData {
    public static int Score { get; set; }
    public static List<string> Inventory { get; set; } = new List<string>();
}

// 3. Save data before scene change and load after
void ChangeScene() {
    SaveGameData();
    SceneManager.LoadScene("NextLevel");
}

void OnSceneLoaded(Scene scene, LoadSceneMode mode) {
    LoadGameData();
}

private void OnEnable() {
    SceneManager.sceneLoaded += OnSceneLoaded;
}

private void OnDisable() {
    SceneManager.sceneLoaded -= OnSceneLoaded;
}

Best Practices for C# Game Development

Code Organization

  1. Use namespaces to organize code

    namespace MyGame.Combat {
        public class Weapon { }
    }
    
    namespace MyGame.Inventory {
        public class Item { }
    }
    
  2. Follow consistent naming conventions

    • PascalCase for classes, methods, properties
    • camelCase for variables, parameters
    • _camelCase for private fields
    • ALL_CAPS for constants
  3. Group related functionality into components

    // Instead of one large Player class,
    // Split into PlayerMovement, PlayerHealth, PlayerInventory, etc.
    
  4. Use [RequireComponent] to enforce dependencies

    [RequireComponent(typeof(Rigidbody))]
    public class PlayerMovement : MonoBehaviour {
        private Rigidbody rb;
        
        void Awake() {
            rb = GetComponent<Rigidbody>();
            // No null check needed - component is guaranteed
        }
    }
    

Performance Tips

  1. Avoid using Find methods in Update

    // Bad
    void Update() {
        GameObject player = GameObject.Find("Player");
    }
    
    // Good
    private GameObject player;
    
    void Start() {
        player = GameObject.Find("Player");
    }
    
  2. Cache component references

    // Bad
    void Update() {
        GetComponent<Rigidbody>().AddForce(Vector3.up);
    }
    
    // Good
    private Rigidbody rb;
    
    void Awake() {
        rb = GetComponent<Rigidbody>();
    }
    
    void Update() {
        rb.AddForce(Vector3.up);
    }
    
  3. Use appropriate data structures

    // For frequent lookups by key: Dictionary
    Dictionary<string, Item> itemLookup = new Dictionary<string, Item>();
    
    // For ordered collections you iterate through: List
    List<Enemy> enemies = new List<Enemy>();
    
    // For fixed-size collections: Array
    Enemy[] enemySpawners = new Enemy[10];
    
  4. Minimize garbage collection

    // Avoid allocations in frequently called methods
    void Update() {
        // Bad: Creates new Vector3 every frame
        transform.position += new Vector3(1, 0, 0) * Time.deltaTime;
        
        // Good: Reuses vector
        transform.position += Vector3.right * Time.deltaTime;
    }
    

Debugging Tips

  1. Use conditional compilation for debug code

    #if UNITY_EDITOR || DEVELOPMENT_BUILD
    Debug.Log("This only shows in editor or development builds");
    #endif
    
  2. Create custom debug visualization

    void OnDrawGizmos() {
        // Draw attack range
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRange);
        
        // Draw patrol path
        Gizmos.color = Color.blue;
        for (int i = 0; i < patrolPoints.Length - 1; i++) {
            Gizmos.DrawLine(patrolPoints[i].position, patrolPoints[i+1].position);
        }
    }
    
  3. Use logging levels

    public enum LogLevel { None, Error, Warning, Info }
    public static LogLevel CurrentLogLevel = LogLevel.Warning;
    
    public static void Log(string message, LogLevel level = LogLevel.Info) {
        if (level <= CurrentLogLevel) {
            switch (level) {
                case LogLevel.Error:
                    Debug.LogError(message);
                    break;
                case LogLevel.Warning:
                    Debug.LogWarning(message);
                    break;
                default:
                    Debug.Log(message);
                    break;
            }
        }
    }
    
    // Usage
    Log("Player damaged", LogLevel.Info);
    Log("Missing component!", LogLevel.Error);
    

Resources for Further Learning

Official Documentation

Books

  • “C# in Depth” by Jon Skeet
  • “Game Programming Patterns” by Robert Nystrom
  • “Unity in Action” by Joseph Hocking

Online Courses and Tutorials

  • Brackeys YouTube Channel
  • Unity Learn Platform
  • Catlike Coding Unity Tutorials
  • Coursera/Udemy Game Development with C# courses

Community Resources

  • Unity Forums
  • Unity Answers
  • Stack Overflow (Unity and C# tags)
  • r/Unity3D and r/gamedev subreddits
  • Game Developer Discord communities

Tools

  • JetBrains Rider (C# IDE optimized for Unity)
  • Visual Studio with Unity Tools
  • Unity Asset Store Debugging Tools
  • Unity Profiler for performance optimization
Scroll to Top