C# Scripting in Unity
Understanding C# Scripts in Unity
C# scripting is the primary way to program game logic in Unity. Scripts define how GameObjects behave, respond to user input, and interact with each other. Understanding C# scripting is essential for creating dynamic, interactive games and applications in Unity.
In this guide, you'll learn:
- The structure of C# scripts in Unity
- Important MonoBehaviour methods and when to use them
- How to create and attach scripts to GameObjects
- Unity's event functions and execution order
- Best practices for Unity C# scripting
Creating Your First C# Script in Unity
Let's start by creating a basic C# script and understanding its structure:
Step-by-Step Creation:
- Create a new script: In the Project window, right-click → Create → C# Script → Name it "MyFirstScript"
- Open the script: Double-click the new script file to open it in your code editor
- Observe the default template: Unity creates a basic MonoBehaviour script with Start and Update methods
- Attach to a GameObject: Drag the script from the Project window onto a GameObject in the Hierarchy
Default C# Script Template:
using UnityEngine;
public class MyFirstScript : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
// This code runs once when the GameObject is enabled
Debug.Log("Script started!"); // Prints a message to the Console window
}
// Update is called once per frame
void Update()
{
// This code runs every frame
// Use for input handling, timer updates, and non-physics movement
}
}
Let's break down the key components:
- using UnityEngine; - Imports Unity's core functionality and classes
- public class MyFirstScript : MonoBehaviour - Defines a class that inherits from MonoBehaviour
- MonoBehaviour - The base class for all Unity scripts that attach to GameObjects
- Start() - Initialization method that runs once when the script is enabled
- Update() - Method that runs every frame, used for regular updates
Understanding MonoBehaviour Lifecycle Methods
Unity scripts use special methods that run at specific times during a GameObject's lifecycle. Understanding when each method is called is crucial for organizing your code:
Key MonoBehaviour Methods:
using UnityEngine;
public class LifecycleDemo : MonoBehaviour
{
// Called when the script instance is being loaded
void Awake()
{
// Use for initialization that must occur before Start
// Great for setting up references between scripts
Debug.Log("Awake: GameObject is initializing");
}
// Called before the first frame update, after Awake
void Start()
{
// Use for initialization that uses values potentially set by other scripts in their Awake functions
Debug.Log("Start: GameObject is starting");
}
// Called every frame
void Update()
{
// Use for:
// - Regular updates that don't involve physics
// - Input detection
// - Timer updates
// Debug.Log("Update: Running every frame"); // Uncomment to see (will spam the console)
}
// Called at fixed intervals (default: 0.02 seconds or 50 times per second)
void FixedUpdate()
{
// Use for physics updates
// All physics calculations happen right after FixedUpdate
// Debug.Log("FixedUpdate: Physics update"); // Uncomment to see
}
// Called after all Update functions have been called
void LateUpdate()
{
// Use for:
// - Camera follow scripts
// - Final position adjustments after other updates
// - Anything that needs to happen after all Updates
// Debug.Log("LateUpdate: After all updates"); // Uncomment to see
}
// Called when the GameObject is destroyed
void OnDestroy()
{
// Use for cleanup when an object is destroyed
Debug.Log("OnDestroy: GameObject is being destroyed");
}
// Called when the GameObject becomes disabled or inactive
void OnDisable()
{
// Called when the behaviour becomes disabled
Debug.Log("OnDisable: GameObject is now inactive");
}
// Called when the GameObject becomes enabled and active
void OnEnable()
{
// Called when the behaviour becomes enabled
Debug.Log("OnEnable: GameObject is now active");
}
}
When to use each method:
- Awake(): For initial setup, runs before any Start methods
- Start(): For initialization that depends on other components
- Update(): For frame-dependent code, input handling
- FixedUpdate(): For physics calculations at fixed time steps
- LateUpdate(): For code that should run after all Updates
- OnEnable()/OnDisable(): When GameObject is activated/deactivated
- OnDestroy(): For cleanup when GameObject is destroyed
The Execution Order
Unity follows a specific execution order when running scripts:
- First Scene Load: Awake → OnEnable → Start
- Every Frame: Update → LateUpdate
- Fixed Timestep: FixedUpdate (runs at fixed intervals)
- When destroyed: OnDisable → OnDestroy
You can customize script execution order in Project Settings → Script Execution Order.
Variables and Components in Unity Scripts
Unity C# scripts can expose variables to the Inspector window, allowing you to adjust values without changing code.
Public and Serialized Variables:
using UnityEngine;
public class VariablesDemo : MonoBehaviour
{
// Public variables appear in the Inspector by default
public float speed = 5f; // Movement speed with default value
public string playerName = "Hero"; // Player name with default value
// [SerializeField] makes private variables appear in the Inspector
[SerializeField]
private int health = 100; // Health is private but still visible in Inspector
// Private variables (not visible in Inspector by default)
private bool isJumping = false; // Tracks if player is jumping
// Reference to another component
public Rigidbody2D rb; // Will be visible in Inspector
// Using [Header] adds a header in the Inspector
[Header("Weapon Settings")]
public int damage = 10; // Weapon damage
public float fireRate = 0.5f; // Weapon fire rate
// Using [Range] creates a slider in the Inspector
[Range(0, 100)]
public int accuracy = 75; // Weapon accuracy with slider
void Start()
{
// Auto-get component if not assigned in Inspector
if (rb == null)
{
// GetComponent searches the GameObject this script is attached to
rb = GetComponent();
// Log a warning if component is still not found
if (rb == null)
{
Debug.LogWarning("No Rigidbody2D found on " + gameObject.name);
}
}
// Using our variables
Debug.Log($"Player {playerName} created with {health} health!");
}
void Update()
{
// Example of using the variables
if (Input.GetKey(KeyCode.Space) && !isJumping)
{
// Use the speed variable from Inspector
rb.AddForce(Vector2.up * speed, ForceMode2D.Impulse);
isJumping = true;
}
}
// Reset jumping state when hitting the ground
void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isJumping = false;
}
}
}
Key concepts:
- public variables appear in the Inspector
- [SerializeField] makes private variables appear in the Inspector
- GetComponent<T>() finds components on the same GameObject
- [Header] and [Range] improve Inspector organization
- References to components can be assigned via code or Inspector
Input Handling in Unity Scripts
Getting user input is a fundamental part of game development. Unity provides several ways to handle input in your C# scripts:
Basic Input Handling:
using UnityEngine;
public class InputExample : MonoBehaviour
{
public float moveSpeed = 5f; // Character movement speed
private Rigidbody2D rb; // Reference to Rigidbody component
void Start()
{
// Get the Rigidbody2D component
rb = GetComponent();
}
void Update()
{
// KEYBOARD INPUT EXAMPLES
// Check if a specific key is currently being held down
if (Input.GetKey(KeyCode.W))
{
// W key is being held down
Debug.Log("Moving forward");
}
// Check if a key was pressed down this frame
if (Input.GetKeyDown(KeyCode.Space))
{
// Space key was just pressed
Debug.Log("Jump initiated");
}
// Check if a key was released this frame
if (Input.GetKeyUp(KeyCode.Space))
{
// Space key was just released
Debug.Log("Jump released");
}
// AXIS INPUT EXAMPLES (from Input Manager)
// Get horizontal input (A/D or Left/Right arrows)
// Returns value between -1 (left) and 1 (right)
float horizontalInput = Input.GetAxis("Horizontal");
// Get vertical input (W/S or Up/Down arrows)
// Returns value between -1 (down) and 1 (up)
float verticalInput = Input.GetAxis("Vertical");
// Create a movement vector
Vector2 movement = new Vector2(horizontalInput, verticalInput);
// MOUSE INPUT EXAMPLES
// Get mouse position in screen coordinates
Vector3 mousePos = Input.mousePosition;
// Convert mouse position to world coordinates
Vector3 worldPosition = Camera.main.ScreenToWorldPoint(mousePos);
worldPosition.z = 0; // Set z to 0 for 2D games
// Check for mouse button clicks
if (Input.GetMouseButtonDown(0))
{
// Left mouse button clicked
Debug.Log("Left click at: " + worldPosition);
}
if (Input.GetMouseButtonDown(1))
{
// Right mouse button clicked
Debug.Log("Right click at: " + worldPosition);
}
}
void FixedUpdate()
{
// Get axis input again in FixedUpdate for physics movement
float horizontalInput = Input.GetAxis("Horizontal");
float verticalInput = Input.GetAxis("Vertical");
// Create a normalized movement vector (prevents diagonal movement from being faster)
Vector2 movement = new Vector2(horizontalInput, verticalInput).normalized;
// Apply movement in FixedUpdate for consistent physics
rb.velocity = movement * moveSpeed;
}
}
Input handling methods:
- Input.GetKey/GetKeyDown/GetKeyUp - For keyboard input
- Input.GetAxis - For smooth input from multiple sources
- Input.GetMouseButton/GetMouseButtonDown/GetMouseButtonUp - For mouse clicks
- Input.mousePosition - For cursor position
- Camera.main.ScreenToWorldPoint - Converts screen position to game world position
Input System Package (New)
Unity now offers a new Input System package that provides more flexibility and features:
- Install via Window → Package Manager → Unity Registry → Input System
- Create an input action asset for configuring inputs visually
- Use the new system with PlayerInput component or directly through code
The new system offers better support for different devices, rebinding, and action maps.
Advanced C# Scripting Concepts
As you become more familiar with Unity scripting, you'll encounter these advanced topics:
Coroutines for Time-Based Operations:
using System.Collections;
using UnityEngine;
public class CoroutineExample : MonoBehaviour
{
// Coroutines allow you to spread operations over multiple frames
void Start()
{
// Start a coroutine that fades an object
StartCoroutine(FadeOut());
// Start a coroutine with parameters
StartCoroutine(DelayedAction("Hello from the future!", 3.0f));
}
// A simple coroutine that fades out an object
IEnumerator FadeOut()
{
// Get the renderer component
SpriteRenderer renderer = GetComponent();
Color startColor = renderer.color; // Starting color
// Fade over 2 seconds
float duration = 2.0f;
float elapsedTime = 0f;
while (elapsedTime < duration)
{
// Calculate how far along we are (0 to 1)
float t = elapsedTime / duration;
// Set new alpha value by lerping from 1 to 0
Color newColor = startColor;
newColor.a = Mathf.Lerp(1f, 0f, t);
renderer.color = newColor;
// Wait until next frame
yield return null;
// Add the time since last frame
elapsedTime += Time.deltaTime;
}
// Ensure we end at fully transparent
Color finalColor = startColor;
finalColor.a = 0f;
renderer.color = finalColor;
Debug.Log("Fade complete!");
}
// Coroutine with parameters that waits for a specific time
IEnumerator DelayedAction(string message, float delay)
{
Debug.Log("Waiting for " + delay + " seconds...");
// Wait for the specified number of seconds
yield return new WaitForSeconds(delay);
// This code runs after the delay
Debug.Log(message);
}
// Stop all coroutines when the object is disabled
void OnDisable()
{
// Stop all coroutines on this MonoBehaviour
StopAllCoroutines();
}
}
Coroutines allow you to:
- Spread operations over multiple frames
- Create sequences of events with delays
- Perform time-based operations like animations or transitions
- Wait for conditions without blocking the main thread
Common yield instructions:
- yield return null; - Wait until next frame
- yield return new WaitForSeconds(1.0f); - Wait for 1 second
- yield return new WaitForFixedUpdate(); - Wait for next physics update
- yield return new WaitUntil(() => condition); - Wait until condition is true
Best Practices for Unity C# Scripting
- Follow naming conventions: Use PascalCase for class names and public members, camelCase for private variables
- Cache component references: Get components in Start() or Awake() rather than repeatedly in Update()
- Use the right update method: Update() for general logic, FixedUpdate() for physics
- Avoid empty methods: Delete unused Start() and Update() methods to improve performance
- Modular design: Give each script a single responsibility
- Optimize performance: Use object pooling for frequently created/destroyed objects
- Check for null references: Always validate references before using them
- Use SerializeField: Keep variables private but visible in the Inspector with [SerializeField]
- Comment your code: Explain complex logic and class purposes
- Use namespaces: Organize larger projects with namespaces