Unity 2D Platformer Tutorial: Free Scripts & Complete Setup

Build a complete 2D platformer in Unity with free scripts for player movement, camera follow, collectibles, health, and polish effects. Step-by-step guide with code.

The 2D platformer is one of the most beloved genres in gaming history — from Super Mario Bros. to Celeste to Hollow Knight. It's also the perfect first project for new Unity developers: it teaches physics, input handling, animation, camera work, and game feel in a concrete, buildable way.

TL;DR: This tutorial guides you through building a complete 2D platformer in Unity using free, ready-made scripts. You'll wire up player movement with variable jump height, a smooth camera follow system, collectible pickups, a health and damage system, and polish effects like camera shake and screen fading. Every script is available for free download, or grab the full 2D Platformer Kit bundle.

This guide assumes you have a fresh Unity 2D project open and basic familiarity with the Unity Editor (creating GameObjects, adding components, navigating the Inspector). We'll go from an empty scene to a playable platformer level with all the core systems wired up.

What You'll Build

By the end of this tutorial, your platformer will have:

  • Responsive player movement — Ground detection, variable jump height, coyote time, and jump buffering for tight controls.
  • Smooth camera follow — A camera that tracks the player with configurable smoothing, dead zones, and bounds clamping.
  • Collectible pickups — Coins, gems, or power-ups with visual feedback and score tracking.
  • Health and damage — A health system with invincibility frames, knockback, and death/respawn.
  • Camera shake and screen fader — Juice effects that make the game feel alive.

Each system uses a standalone script that works independently. You can adopt one or all of them.

Player Movement & Jumping

Player movement is the foundation of every platformer. The difference between a "floaty" platformer and a "tight" one comes down to a handful of tuning parameters. Our Player Controller 2D script handles all of this, but here's the core logic so you understand what's happening under the hood:

C#
using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class PlayerController2D : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float moveSpeed = 8f;
    [SerializeField] private float acceleration = 50f;
    [SerializeField] private float deceleration = 50f;

    [Header("Jumping")]
    [SerializeField] private float jumpForce = 16f;
    [SerializeField] private float coyoteTime = 0.12f;
    [SerializeField] private float jumpBufferTime = 0.1f;
    [SerializeField] private float fallMultiplier = 3f;
    [SerializeField] private float lowJumpMultiplier = 5f;

    [Header("Ground Check")]
    [SerializeField] private Transform groundCheck;
    [SerializeField] private float groundCheckRadius = 0.15f;
    [SerializeField] private LayerMask groundLayer;

    private Rigidbody2D rb;
    private float coyoteTimer;
    private float jumpBufferTimer;
    private bool isGrounded;

    void Awake() => rb = GetComponent<Rigidbody2D>();

    void Update()
    {
        // Ground detection
        isGrounded = Physics2D.OverlapCircle(groundCheck.position, groundCheckRadius, groundLayer);
        coyoteTimer = isGrounded ? coyoteTime : coyoteTimer - Time.deltaTime;

        // Jump buffer — stores the input briefly so pressing jump
        // slightly before landing still registers
        if (Input.GetButtonDown("Jump"))
            jumpBufferTimer = jumpBufferTime;
        else
            jumpBufferTimer -= Time.deltaTime;

        // Execute jump when both conditions are met
        if (jumpBufferTimer > 0f && coyoteTimer > 0f)
        {
            rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
            jumpBufferTimer = 0f;
            coyoteTimer = 0f;
        }

        // Variable jump height — release the button to cut the jump short
        if (rb.linearVelocity.y > 0 && !Input.GetButton("Jump"))
            rb.linearVelocity += Vector2.up * Physics2D.gravity.y * (lowJumpMultiplier - 1) * Time.deltaTime;
        else if (rb.linearVelocity.y < 0)
            rb.linearVelocity += Vector2.up * Physics2D.gravity.y * (fallMultiplier - 1) * Time.deltaTime;
    }

    void FixedUpdate()
    {
        float targetSpeed = Input.GetAxisRaw("Horizontal") * moveSpeed;
        float accelRate = Mathf.Abs(targetSpeed) > 0.01f ? acceleration : deceleration;
        float speedDiff = targetSpeed - rb.linearVelocity.x;
        rb.AddForce(Vector2.right * speedDiff * accelRate, ForceMode2D.Force);
    }
}

The key techniques here are coyote time (a short grace period after walking off a ledge where you can still jump), jump buffering (pressing jump slightly before landing still registers), and variable jump height (releasing the jump button early produces a shorter hop). These three features are what make platformers like Celeste feel responsive and forgiving.

Set up your player: create a 2D sprite, add a Rigidbody2D (freeze Z rotation), a BoxCollider2D, and create a child empty GameObject at the player's feet named "GroundCheck". Assign the ground layer to your tilemap or platform colliders.

Camera Follow

A raw Camera.main.transform.position = player.position produces a jerky, unpleasant camera. Our Camera Follow 2D script provides smooth tracking with a dead zone (the camera doesn't move until the player reaches the edge of a region) and bounds clamping (the camera stops at level edges).

C#
using UnityEngine;

public class CameraFollow2D : MonoBehaviour
{
    [SerializeField] private Transform target;
    [SerializeField] private float smoothSpeed = 5f;
    [SerializeField] private Vector2 offset = new Vector2(0, 1f);
    [SerializeField] private Vector2 deadZone = new Vector2(0.5f, 0.5f);

    [Header("Bounds (optional)")]
    [SerializeField] private bool useBounds;
    [SerializeField] private Vector2 boundsMin;
    [SerializeField] private Vector2 boundsMax;

    private Vector3 currentVelocity;

    void LateUpdate()
    {
        if (target == null) return;

        Vector3 targetPos = (Vector2)target.position + offset;
        targetPos.z = transform.position.z;

        // Dead zone — only move if target exceeds threshold
        Vector3 diff = targetPos - transform.position;
        if (Mathf.Abs(diff.x) < deadZone.x) targetPos.x = transform.position.x;
        if (Mathf.Abs(diff.y) < deadZone.y) targetPos.y = transform.position.y;

        // Smooth follow
        Vector3 smoothed = Vector3.SmoothDamp(transform.position, targetPos, ref currentVelocity, 1f / smoothSpeed);

        // Clamp to bounds
        if (useBounds)
        {
            smoothed.x = Mathf.Clamp(smoothed.x, boundsMin.x, boundsMax.x);
            smoothed.y = Mathf.Clamp(smoothed.y, boundsMin.y, boundsMax.y);
        }

        transform.position = smoothed;
    }
}

Attach this to your Main Camera and drag your player into the Target field. Tune the dead zone values — larger dead zones mean the player can move more before the camera follows, which feels better for exploration-focused games. Smaller dead zones feel better for fast-paced games.

Collectibles & Pickups

Collectibles give players a reason to explore. Our Pickup Collectible script handles the interaction, but the pattern is straightforward: trigger collider + OnTriggerEnter2D + feedback.

C#
using UnityEngine;

public class Collectible : MonoBehaviour
{
    [SerializeField] private int scoreValue = 10;
    [SerializeField] private AudioClip pickupSound;
    [SerializeField] private GameObject pickupVFX;
    [SerializeField] private float bobAmplitude = 0.2f;
    [SerializeField] private float bobFrequency = 2f;

    private Vector3 startPos;

    void Start() => startPos = transform.position;

    void Update()
    {
        // Floating bob animation
        float yOffset = Mathf.Sin(Time.time * bobFrequency) * bobAmplitude;
        transform.position = startPos + Vector3.up * yOffset;
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (!other.CompareTag("Player")) return;

        // Add score
        ScoreManager.Instance?.AddScore(scoreValue);

        // Feedback
        if (pickupSound != null)
            AudioSource.PlayClipAtPoint(pickupSound, transform.position);
        if (pickupVFX != null)
            Instantiate(pickupVFX, transform.position, Quaternion.identity);

        Destroy(gameObject);
    }
}

Set the collider on your collectible to Is Trigger = true. The bob animation makes pickups visually noticeable without requiring an animator. For particle feedback, create a simple burst particle system prefab and assign it to pickupVFX.

Health & Damage

A health system handles damage, invincibility frames (so the player doesn't take 10 hits per frame from a single spike), and death. Our Health System script is a complete, event-driven solution. Here's the core pattern:

C#
using System;
using UnityEngine;

public class HealthSystem : MonoBehaviour
{
    [SerializeField] private int maxHealth = 3;
    [SerializeField] private float invincibilityDuration = 1.5f;

    public int CurrentHealth { get; private set; }
    public bool IsInvincible { get; private set; }
    public event Action<int, int> OnHealthChanged; // current, max
    public event Action OnDeath;

    private float invincibilityTimer;

    void Start()
    {
        CurrentHealth = maxHealth;
        OnHealthChanged?.Invoke(CurrentHealth, maxHealth);
    }

    void Update()
    {
        if (IsInvincible)
        {
            invincibilityTimer -= Time.deltaTime;
            if (invincibilityTimer <= 0f)
                IsInvincible = false;
        }
    }

    public void TakeDamage(int damage)
    {
        if (IsInvincible || CurrentHealth <= 0) return;

        CurrentHealth = Mathf.Max(0, CurrentHealth - damage);
        OnHealthChanged?.Invoke(CurrentHealth, maxHealth);

        if (CurrentHealth <= 0)
        {
            OnDeath?.Invoke();
            return;
        }

        // Start invincibility frames
        IsInvincible = true;
        invincibilityTimer = invincibilityDuration;
    }

    public void Heal(int amount)
    {
        CurrentHealth = Mathf.Min(maxHealth, CurrentHealth + amount);
        OnHealthChanged?.Invoke(CurrentHealth, maxHealth);
    }
}

Subscribe to OnHealthChanged to update a hearts-based health bar UI, and subscribe to OnDeath to trigger a death animation and respawn sequence. During invincibility frames, flash the player sprite by toggling the SpriteRenderer on and off in a coroutine.

Polish: Camera Shake & Screen Fader

Game feel — often called "juice" — is what separates a prototype from a game that feels professional. Two high-impact polish effects are camera shake on damage and a screen fader for scene transitions.

Camera shake adds impact to hits, explosions, and heavy landings. A simple implementation offsets the camera by a random vector each frame during the shake duration, decaying the intensity over time:

C#
using UnityEngine;
using System.Collections;

public class CameraShake : MonoBehaviour
{
    public static CameraShake Instance { get; private set; }
    void Awake() => Instance = this;

    public void Shake(float duration = 0.2f, float magnitude = 0.3f)
    {
        StartCoroutine(ShakeRoutine(duration, magnitude));
    }

    private IEnumerator ShakeRoutine(float duration, float magnitude)
    {
        Vector3 originalPos = transform.localPosition;
        float elapsed = 0f;

        while (elapsed < duration)
        {
            float x = Random.Range(-1f, 1f) * magnitude;
            float y = Random.Range(-1f, 1f) * magnitude;
            transform.localPosition = originalPos + new Vector3(x, y, 0);

            magnitude = Mathf.Lerp(magnitude, 0f, elapsed / duration);
            elapsed += Time.deltaTime;
            yield return null;
        }

        transform.localPosition = originalPos;
    }
}

Call CameraShake.Instance.Shake() from your damage handler, collectible pickup, or any impactful event. For the screen fader, create a full-screen CanvasGroup Image and tween its alpha to fade in/out during scene transitions. This tiny addition makes scene changes feel polished instead of jarring.

Download the Complete Kit

Every script in this tutorial is available as a free standalone download. But if you want the complete package — all scripts pre-configured, a sample scene with a playable level, tilemap setup, animation controllers, and a settings menu — grab the 2D Platformer Kit game system bundle.

The kit includes the Player Controller 2D, Camera Follow 2D, Pickup Collectible, Health System, a checkpoint/respawn manager, moving platform support, one-way platforms, and a parallax scrolling background.

Looking to expand beyond a single level? Add an Inventory System for power-ups and collectible tracking, or wire up a Save System to persist player progress between sessions.