Part of these game systems:
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.
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
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();
}
}