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
Modifier | Access Level | Use Case in Games |
---|---|---|
public | Accessible from any code | For components that need to be exposed in Unity Inspector |
private | Only within the same class | For internal implementation details |
protected | Within the same class or derived classes | For methods that subclasses might need to override |
internal | Within the same assembly | For systems that shouldn’t be accessed by external plugins |
protected internal | Within the same assembly or derived classes | For 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
Operation | Efficient Approach | Inefficient Approach | Performance Impact |
---|---|---|---|
Finding Objects | Cache references in Start/Awake | Use GameObject.Find in Update | Reduces per-frame overhead |
Vector Math | Use Vector3.sqrMagnitude | Use Vector3.magnitude | Avoids expensive square root |
String Operations | Use StringBuilder for multiple concatenations | Use string + string repeatedly | Reduces garbage collection |
Collections | Use List<T> for dynamic collections | Use arrays with resizing | Better memory management |
Foreach loops | Use for loops with direct indexing | Use foreach on collections | Avoids enumerator allocation |
Physics | Use non-alloc Physics methods | Use standard Physics methods | Reduces garbage collection |
Components | Use GetComponent in Start/Awake and cache | Call GetComponent repeatedly | Reduces 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
Use namespaces to organize code
namespace MyGame.Combat { public class Weapon { } } namespace MyGame.Inventory { public class Item { } }
Follow consistent naming conventions
- PascalCase for classes, methods, properties
- camelCase for variables, parameters
- _camelCase for private fields
- ALL_CAPS for constants
Group related functionality into components
// Instead of one large Player class, // Split into PlayerMovement, PlayerHealth, PlayerInventory, etc.
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
Avoid using Find methods in Update
// Bad void Update() { GameObject player = GameObject.Find("Player"); } // Good private GameObject player; void Start() { player = GameObject.Find("Player"); }
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); }
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];
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
Use conditional compilation for debug code
#if UNITY_EDITOR || DEVELOPMENT_BUILD Debug.Log("This only shows in editor or development builds"); #endif
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); } }
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