Part of these game systems:
intermediate Touch & Mobile

Endless Runner Controller

Complete endless runner character controller with auto-forward movement, lane switching, jump and slide mechanics. Speed increases over time with death and restart handling.

Unity 2022.3+ · 4.8 KB · EndlessRunnerController.cs

How to Use

1

Attach to your player GameObject and add a CharacterController component

2

Set lane count and lane width to match your level geometry

3

Configure jump force, gravity, and slide duration in the inspector

4

Wire up onJump, onSlide, onDeath, and onRestart events for animations and UI

5

Call Die() from obstacle collisions and RestartGame() from your retry button

Source Code

EndlessRunnerController.cs
C#
using UnityEngine;
using UnityEngine.Events;

[RequireComponent(typeof(CharacterController))]
public class EndlessRunnerController : MonoBehaviour
{
    [Header("Movement")]
    [SerializeField] private float initialSpeed = 8f;
    [SerializeField] private float maxSpeed = 25f;
    [SerializeField] private float speedIncreaseRate = 0.1f;
    [SerializeField] private float laneSwitchSpeed = 10f;

    [Header("Lanes")]
    [SerializeField] private float laneWidth = 2.5f;
    [SerializeField] private int totalLanes = 3;

    [Header("Jump & Slide")]
    [SerializeField] private float jumpForce = 10f;
    [SerializeField] private float gravity = -30f;
    [SerializeField] private float slideDuration = 0.6f;
    [SerializeField] private float slideColliderHeight = 0.5f;

    [Header("Swipe Input")]
    [SerializeField] private float minSwipeDistance = 50f;

    [Header("Events")]
    public UnityEvent onJump;
    public UnityEvent onSlide;
    public UnityEvent onLaneSwitch;
    public UnityEvent onDeath;
    public UnityEvent onRestart;

    public float CurrentSpeed { get; private set; }
    public int CurrentLane { get; private set; }
    public bool IsAlive { get; private set; }
    public float DistanceRun { get; private set; }

    private CharacterController controller;
    private Vector3 moveDirection;
    private int targetLane;
    private float verticalVelocity;
    private float originalColliderHeight;
    private float originalColliderCenterY;
    private float slideTimer;
    private bool isSliding;

    // Swipe tracking
    private Vector2 swipeStart;
    private bool swiping;

    private void Awake()
    {
        controller = GetComponent<CharacterController>();
        originalColliderHeight = controller.height;
        originalColliderCenterY = controller.center.y;
    }

    private void Start()
    {
        StartRun();
    }

    public void StartRun()
    {
        IsAlive = true;
        CurrentSpeed = initialSpeed;
        targetLane = totalLanes / 2;
        CurrentLane = targetLane;
        DistanceRun = 0f;
        verticalVelocity = 0f;
        isSliding = false;
        controller.height = originalColliderHeight;
        controller.center = new Vector3(0f, originalColliderCenterY, 0f);
    }

    private void Update()
    {
        if (!IsAlive) return;

        HandleSwipeInput();
        UpdateMovement();
        UpdateSlide();

        CurrentSpeed = Mathf.Min(CurrentSpeed + speedIncreaseRate * Time.deltaTime, maxSpeed);
        DistanceRun += CurrentSpeed * Time.deltaTime;
    }

    private void HandleSwipeInput()
    {
        if (Input.touchCount > 0)
        {
            Touch touch = Input.GetTouch(0);

            if (touch.phase == TouchPhase.Began)
            {
                swipeStart = touch.position;
                swiping = true;
            }
            else if (touch.phase == TouchPhase.Ended && swiping)
            {
                swiping = false;
                ProcessSwipe(touch.position - swipeStart);
            }
        }

        // Keyboard fallback
        if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.A))
            SwitchLane(-1);
        if (Input.GetKeyDown(KeyCode.RightArrow) || Input.GetKeyDown(KeyCode.D))
            SwitchLane(1);
        if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.W))
            Jump();
        if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.S))
            Slide();
    }

    private void ProcessSwipe(Vector2 delta)
    {
        if (delta.magnitude < minSwipeDistance) return;

        if (Mathf.Abs(delta.x) > Mathf.Abs(delta.y))
        {
            SwitchLane(delta.x > 0 ? 1 : -1);
        }
        else
        {
            if (delta.y > 0) Jump();
            else Slide();
        }
    }

    private void SwitchLane(int direction)
    {
        targetLane = Mathf.Clamp(targetLane + direction, 0, totalLanes - 1);
        CurrentLane = targetLane;
        onLaneSwitch?.Invoke();
    }

    private void Jump()
    {
        if (!controller.isGrounded) return;

        verticalVelocity = jumpForce;

        if (isSliding) EndSlide();

        onJump?.Invoke();
    }

    private void Slide()
    {
        if (isSliding || !controller.isGrounded) return;

        isSliding = true;
        slideTimer = slideDuration;
        controller.height = slideColliderHeight;
        controller.center = new Vector3(0f, slideColliderHeight * 0.5f, 0f);
        onSlide?.Invoke();
    }

    private void UpdateSlide()
    {
        if (!isSliding) return;

        slideTimer -= Time.deltaTime;
        if (slideTimer <= 0f)
            EndSlide();
    }

    private void EndSlide()
    {
        isSliding = false;
        controller.height = originalColliderHeight;
        controller.center = new Vector3(0f, originalColliderCenterY, 0f);
    }

    private void UpdateMovement()
    {
        // Forward movement
        moveDirection.z = CurrentSpeed;

        // Lane position
        int centerLane = totalLanes / 2;
        float targetX = (targetLane - centerLane) * laneWidth;
        float currentX = Mathf.Lerp(transform.position.x, targetX, Time.deltaTime * laneSwitchSpeed);
        moveDirection.x = (currentX - transform.position.x) / Time.deltaTime;

        // Gravity
        if (controller.isGrounded && verticalVelocity < 0f)
            verticalVelocity = -2f;

        verticalVelocity += gravity * Time.deltaTime;
        moveDirection.y = verticalVelocity;

        controller.Move(moveDirection * Time.deltaTime);
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Obstacle"))
        {
            Die();
        }
    }

    private void Die()
    {
        if (!IsAlive) return;
        IsAlive = false;
        CurrentSpeed = 0f;
        onDeath?.Invoke();
    }

    public void Restart()
    {
        transform.position = Vector3.zero;
        StartRun();
        onRestart?.Invoke();
    }
}
Ready for more? Stack Mechanic Tap-to-place stacking game mechanic where a moving block must be stopped to align with the previous block.