Easy Learn C#

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:

  1. Create a new script: In the Project window, right-click → Create → C# Script → Name it "MyFirstScript"
  2. Open the script: Double-click the new script file to open it in your code editor
  3. Observe the default template: Unity creates a basic MonoBehaviour script with Start and Update methods
  4. 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:

  1. First Scene Load: Awake → OnEnable → Start
  2. Every Frame: Update → LateUpdate
  3. Fixed Timestep: FixedUpdate (runs at fixed intervals)
  4. 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:

  1. Install via Window → Package Manager → Unity Registry → Input System
  2. Create an input action asset for configuring inputs visually
  3. 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

  1. Follow naming conventions: Use PascalCase for class names and public members, camelCase for private variables
  2. Cache component references: Get components in Start() or Awake() rather than repeatedly in Update()
  3. Use the right update method: Update() for general logic, FixedUpdate() for physics
  4. Avoid empty methods: Delete unused Start() and Update() methods to improve performance
  5. Modular design: Give each script a single responsibility
  6. Optimize performance: Use object pooling for frequently created/destroyed objects
  7. Check for null references: Always validate references before using them
  8. Use SerializeField: Keep variables private but visible in the Inspector with [SerializeField]
  9. Comment your code: Explain complex logic and class purposes
  10. Use namespaces: Organize larger projects with namespaces