Skip to content
Happy Programming Guide
Start learning
C#

C# and Unity Game Development

How C# works inside Unity: MonoBehaviour, the lifecycle methods, Update versus FixedUpdate, prefabs, and the performance mistakes beginners make.

A rocket lifting off against the sky

Unity uses C# for its scripting, but the way you write it differs from ordinary C#. You rarely create objects with new, there is no Main method, and the engine calls your code through named lifecycle methods. This guide covers those differences, the frame-rate trap that catches everyone, and the performance mistakes that matter most.

The shape of a Unity script#

CSHARP
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    [SerializeField] private float jumpForce = 7f;

    private Rigidbody2D _body;
    private bool _grounded;

    private void Awake()
    {
        _body = GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
        float horizontal = Input.GetAxisRaw("Horizontal");
        transform.Translate(Vector3.right * horizontal * speed * Time.deltaTime);

        if (_grounded && Input.GetButtonDown("Jump"))
        {
            _body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
            _grounded = false;
        }
    }
}

Two things differ from ordinary C#. The class inherits MonoBehaviour, which is what lets Unity attach it to a GameObject and call its methods. And [SerializeField] exposes a private field in the Inspector, so designers can tune values without editing code.

The lifecycle methods#

Method When it runs Use for
Awake once, before everything getting your own components
OnEnable each time the object is enabled subscribing to events
Start once, before the first frame referencing other objects
Update every frame input, most game logic
FixedUpdate at a fixed rate physics forces
LateUpdate after all Updates cameras following a target
OnDisable when disabled unsubscribing

The Awake and Start split matters. Awake runs on every object before any Start, so setting up your own references in Awake and reaching for other objects in Start avoids depending on which order objects happened to initialise in.

deltaTime, the frame-rate trap#

CSHARP
// Wrong - speed depends on frame rate
transform.position += Vector3.right * 5f;

// Right - metres per second, whatever the frame rate
transform.position += Vector3.right * 5f * Time.deltaTime;

Update runs once per frame, and frames are not evenly spaced. Without Time.deltaTime, your character moves twice as fast on a 120 Hz monitor as on a 60 Hz one — the classic symptom being a game that is unplayable on a faster machine.

Multiply anything per-frame by Time.deltaTime: movement, rotation, timers, gradual changes.

Update or FixedUpdate#

CSHARP
private void Update()
{
    // Input, every frame, or you miss button presses
    _jumpQueued |= Input.GetButtonDown("Jump");
}

private void FixedUpdate()
{
    // Physics forces, at a fixed rate
    if (_jumpQueued)
    {
        _body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
        _jumpQueued = false;
    }
}

FixedUpdate runs on a fixed timestep independent of frame rate, which is what the physics engine needs to be deterministic. Read input in Update, because GetButtonDown is only true for one frame and a FixedUpdate may skip it entirely.

Getting references#

CSHARP
private void Awake()
{
    _body = GetComponent<Rigidbody2D>();          // on this object
    _sprite = GetComponentInChildren<SpriteRenderer>();
}
CSHARP
// Never in Update - both are expensive
private void Update()
{
    GameObject.Find("Player").transform.position = ...;   // searches every object
    GetComponent<Rigidbody2D>().AddForce(...);            // repeated lookup
}

Cache references in Awake or Start and reuse them. Better still, assign them in the Inspector with [SerializeField], which costs nothing at run time and makes the dependency visible.

Prefabs and instantiation#

CSHARP
public class Spawner : MonoBehaviour
{
    [SerializeField] private GameObject bulletPrefab;
    [SerializeField] private Transform muzzle;

    public void Fire()
    {
        GameObject bullet = Instantiate(bulletPrefab, muzzle.position, muzzle.rotation);
        Destroy(bullet, 3f);      // clean up after three seconds
    }
}

A prefab is a saved GameObject with its components and values. Instantiate creates a copy at run time.

Creating and destroying objects constantly causes garbage collection pauses. For anything frequent — bullets, particles, enemies — reuse instead:

CSHARP
public class BulletPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int size = 30;

    private readonly Queue<GameObject> _available = new();

    private void Awake()
    {
        for (int i = 0; i < size; i++)
        {
            var obj = Instantiate(prefab, transform);
            obj.SetActive(false);
            _available.Enqueue(obj);
        }
    }

    public GameObject Take(Vector3 position, Quaternion rotation)
    {
        var obj = _available.Count > 0 ? _available.Dequeue() : Instantiate(prefab, transform);
        obj.transform.SetPositionAndRotation(position, rotation);
        obj.SetActive(true);
        return obj;
    }

    public void Return(GameObject obj)
    {
        obj.SetActive(false);
        _available.Enqueue(obj);
    }
}

Coroutines#

CSHARP
private IEnumerator FadeOut(float duration)
{
    float elapsed = 0f;
    var colour = _sprite.color;

    while (elapsed < duration)
    {
        elapsed += Time.deltaTime;
        colour.a = Mathf.Lerp(1f, 0f, elapsed / duration);
        _sprite.color = colour;
        yield return null;              // wait for the next frame
    }

    gameObject.SetActive(false);
}

// Start it
StartCoroutine(FadeOut(1.5f));

A coroutine spreads work across frames without blocking. yield return null waits one frame; yield return new WaitForSeconds(2f) waits two seconds. Note that the second allocates, so avoid it inside a tight loop — cache the wait object instead.

The five mistakes#

  1. Forgetting Time.deltaTime. The game plays differently on different hardware.
  2. GetComponent or Find in Update. Cache the reference once.
  3. Moving a Rigidbody with transform.position. It bypasses physics and causes objects to pass through walls. Use MovePosition or forces.
  4. Comparing tags with gameObject.tag == "Enemy". That allocates a string every call; use CompareTag("Enemy").
  5. Instantiate and Destroy every frame. Pool instead.

Ordinary C# still applies#

CSHARP
// A plain class, not a MonoBehaviour - no engine overhead
[Serializable]
public class InventoryItem
{
    public string Name;
    public int Quantity;
}

// ScriptableObject for shared data assets
[CreateAssetMenu(menuName = "Game/Weapon")]
public class WeaponData : ScriptableObject
{
    public float damage;
    public float fireRate;
}

Not everything needs to be a MonoBehaviour. Data, rules and calculations belong in plain C# classes, which are lighter and can be unit tested without starting the engine.

Questions people ask#

Do I need to know C# before Unity?

Basic syntax, variables, conditions, loops and classes — yes. You can learn the rest as you go, but starting with zero programming makes Unity considerably harder than it needs to be.

Why is there no Main method?

Unity owns the program loop. Your scripts are components it calls at defined points, which is why the lifecycle methods matter so much.

Does Unity C# differ from normal C#?

It is the same language. The differences are the engine API, the lifecycle model, and some older Unity versions supporting older C# language versions.

Can I unit test Unity code?

Yes — the Test Framework package supports both edit-mode and play-mode tests. Code in plain C# classes is far easier to test than code inside MonoBehaviours, which is a good reason to keep logic out of them.

Where to go next#

C# basics, the foundation Unity scripting sits onRead next

Keep reading

C#

CORS Not Working in ASP.NET Core

The policy is defined and the browser still blocks the request. Middleware order, the policy that was never applied, credentials with a…

4 min read

C#

Asynchronous Programming with C#

What async and await actually do, why .Result deadlocks, how to run work concurrently with Task.WhenAll, and cancellation done properly.

5 min read

C#

C# Performance Optimization

Profiling before guessing, then the fixes that pay: allocations, LINQ in hot paths, string building, Span, async correctness and database queries.

5 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *