Endless fun

overview

A vibrant and engaging endless runner built in Unity 3D using the SYNTY Kids asset pack. Players take control of a small, adventurous dinosaur, dodging obstacles and collecting rewards in a cheerful low-poly world. Designed with dynamic gameplay, intuitive controls, and randomized challenges to keep the experience fresh and fun.

Year

2025

Genre

Endless runner

Platform

PC

Play!
Code Highlights
C#
				using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [Header("Player Settings")]
    [SerializeField] private GameObject playerVisual;

    [Header("Jump Settings")]
    [SerializeField] private float jumpForce = 5f;
    [SerializeField] private LayerMask groundLayer;

    [Header("Gravity Modifiers")]
    [SerializeField] private float fallMultiplier = 2.5f;

    private PlayerInputActions inputActions;
    private Rigidbody rb;
    private bool jumpPressed = false;

    private PlayerDamageable damageable;
    private BounceWalk playerBounceWalk;

    #region Unity Callbacks
    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
        inputActions = new PlayerInputActions();
        inputActions.Enable();
        inputActions.Player.Jump.performed += Jump_performed;

        damageable = GetComponent<PlayerDamageable>();
        damageable.OnDeath += HandleDeath;
        
        playerBounceWalk = GetComponent<BounceWalk>();
    }
    private void FixedUpdate()
    {
        if(jumpPressed && IsGrounded())
        {
            // Grab current linear velocity, set Y to JumpForce
            Vector3 v = rb.linearVelocity;
            v.y = jumpForce;
            rb.linearVelocity = v;
        }

        if(rb.linearVelocity.y < 0f)
        {
            Vector3 v = rb.linearVelocity;
            v.y += Physics.gravity.y * (fallMultiplier - 1f) * Time.deltaTime;
            rb.linearVelocity = v;
        }

        jumpPressed = false;
    }
    #endregion

    #region Ground Check
    private bool IsGrounded() 
    {
        return Physics.Raycast(transform.position, Vector3.down, 0.6f, groundLayer);
    }
    #endregion

    #region Input Actions
    private void Jump_performed(UnityEngine.InputSystem.InputAction.CallbackContext obj)
    {
        if(IsGrounded())
            FeedbackManager.Instance.GetPlayerJumpFeedback().PlayFeedbacks();
    
        jumpPressed = true;
    }
    #endregion

    #region Event Methods
    private void HandleDeath()
    {
        Debug.Log("Player is dead");
        GameManager.Instance.GameOver();
    }
    #endregion
}
			
C#
				using UnityEngine;
using System.Collections;

public class BounceWalk : MonoBehaviour
{
    [SerializeField] private Transform visual;
    [SerializeField] private float bounceHeight = 0.2f;
    [SerializeField] private float bounceDuration = 0.4f;
    [SerializeField] private float rotationAngle = 15f;

    private bool flip = false;
    [SerializeField] private bool canBounce = false;
    private Coroutine bounceLoop;

    public void StartBounceLoop()
    {
        if (bounceLoop == null)
            bounceLoop = StartCoroutine(BounceLoop());
    }

    public void StopBounceLoop()
    {
        if (bounceLoop != null)
        {
            StopCoroutine(bounceLoop);
            bounceLoop = null;
            visual.localPosition = Vector3.zero;
            visual.localRotation = Quaternion.identity;
        }
    }

    private IEnumerator BounceLoop()
    {
        Vector3 basePos = visual.localPosition;

        while (canBounce)
        {
            Vector3 topPos = basePos + new Vector3(0, bounceHeight, 0);
            float targetAngle = flip ? -rotationAngle : rotationAngle;
            Quaternion startRot = Quaternion.identity;
            Quaternion peakRot = Quaternion.Euler(targetAngle, 0f, 0f);
            flip = !flip;

            // Bounce Up
            float elapsed = 0f;
            while (elapsed < bounceDuration / 2f)
            {
                float t = elapsed / (bounceDuration / 2f);
                visual.localPosition = Vector3.Lerp(basePos, topPos, t);
                visual.localRotation = Quaternion.Lerp(startRot, peakRot, t);
                elapsed += Time.deltaTime;
                yield return null;
            }

            // Bounce Down
            elapsed = 0f;
            while (elapsed < bounceDuration / 2f)
            {
                float t = elapsed / (bounceDuration / 2f);
                visual.localPosition = Vector3.Lerp(topPos, basePos, t);
                visual.localRotation = Quaternion.Lerp(peakRot, startRot, t);
                elapsed += Time.deltaTime;
                yield return null;
            }

            // Reset to exact base state to avoid drift
            visual.localPosition = basePos;
            visual.localRotation = Quaternion.identity;
        }

        bounceLoop = null;
    }

    public void ToggleBounce(bool value)
    {
        canBounce = value;

        if (canBounce)
        {
            StartBounceLoop();
        }
        else
        {
            StopBounceLoop();
        }
    }
}
			
C#
				using UnityEngine;

public class PlatformHazard : MonoBehaviour, IMoveable
{
    [Header("Platform Hazard Settings")]
    [SerializeField] private bool isHazard = false;

    private Collider col;
    private float hazardSpeed;
    private Vector3 hazardTarget;
    private bool isMoving;
    private float localTimeScale = 1f;

    #region Interface
    public void MoveTo(Vector3 targetPos, float speed)
    {
        hazardTarget = targetPos;
        hazardSpeed = speed;
        isMoving = true;
        localTimeScale = 1f; // Reset
    }
    #endregion

    #region Unity Callbacks
    private void Awake()
    {
        col = GetComponent<Collider>();
        UpdateCollider();
    }
    #endregion

    /// <summary>
    /// This runs in the editor when you flip isHazard in the Inspector, you see the inmidate change.
    /// </summary>
    private void OnValidate() 
    {
        if(col == null) col = GetComponent<Collider>();
        UpdateCollider();
    }

    /// <summary>
    /// If we want to update the hazard in run time from anothher script.
    /// </summary>
    /// <param name="hazardState"></param>
    public void SetHazard(bool hazardState)
    {
        isHazard = hazardState;
        UpdateCollider();
    }
    private void UpdateCollider()
    {
        col.enabled = isHazard;
    }

    private void OnTriggerEnter(Collider other)
    {
        // Look for the PlayerDamageable on whatever we hit.
        var hit = other.GetComponent<PlayerDamageable>();
        if (hit != null) 
        {
            hit.PlayerHit();
        }
    }

    public void SetSpeed(float newSpeed) { hazardSpeed = newSpeed; } // Call this function mid flight to increase speedd based on the new index.

    public void SetTimeScale(float t) { localTimeScale = t; } // With this function we can modify the time scale per object.

    #region Unity Callbacks
    private void Update()
    {
        if (!isMoving) return;

        float spd = hazardSpeed * Time.deltaTime * localTimeScale;

        transform.position = Vector3.MoveTowards(transform.position, hazardTarget, spd);

        if (Vector3.Distance(transform.position, hazardTarget) < 0.01f)
            Destroy(gameObject); // Destination reached
    }
    #endregion
}
			
C#
				using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;

public class ObstacleSpawner : MonoBehaviour
{
    [Header("Spawner Settings")]
    [SerializeField] private Transform startPos;
    [SerializeField] private Transform endPos;

    [Header("Obstacle Settings")]
    [SerializeField] private List<GameObject> obstacles = new List<GameObject>();
    [SerializeField] private float spawnInterval = 2f;
    [SerializeField] private float spawnChance = 0.5f;
    [SerializeField] private float spawnDelay = 1f;

    [Header("Speed Settings")]
    [SerializeField] private float baseSpeed = 1f;
    [SerializeField] private float speedIncrement = .5f;

    private float currentSpeed;
    private bool canSpawn = true;
    private Coroutine spawnRoutine;

    private readonly List<IMoveable> activeObstacles = new List<IMoveable>();

    #region Unity Callbacks
    private void Awake()
    {
        // Initalize speed and subscribe to index up event
        currentSpeed = baseSpeed;
    }

    private void Start()
    {
        ScoreManager.Instance.OnIndexChanged += OnIndexIncreased;
    }
    #endregion

    public void StartSpawning()
    {
        if (spawnRoutine == null)
            spawnRoutine = StartCoroutine(SpawnRoutine());
    }
    public void StopSpawning()
    {
        if (spawnRoutine != null)
        {
            StopCoroutine(spawnRoutine);
            spawnRoutine = null;
        }
    }

    private IEnumerator SpawnRoutine()
    {
        while (true)
        {
            TrySpawn();
            yield return new WaitForSeconds(spawnInterval);
        }
    }

    private IEnumerator ResetSpawnCooldown()
    {
        yield return new WaitForSeconds(spawnDelay);
        canSpawn = true;
    }

    private void TrySpawn()
    {
        if (!canSpawn) return;

        if(Random.value <= spawnChance)
        {
            SpawnOne();
            canSpawn = false;
            StartCoroutine(ResetSpawnCooldown());
        }
    }

    public void SpawnOne()
    {
        if (obstacles.Count == 0) return;

        GameObject obstacle = obstacles[Random.Range(0, obstacles.Count)];
        GameObject obstacleGO = Instantiate(obstacle, startPos.position, startPos.rotation);
        MoveObstacle(obstacleGO);
    }

    private void MoveObstacle(GameObject obj)
    {
        var moveable = obj.GetComponent<IMoveable>();
        if (moveable != null)
            moveable.MoveTo(endPos.position, currentSpeed);
    }

    /// <summary>
    /// Called on each index‐up:
    /// - bump overall speed
    /// - prune any destroyed movers
    /// - reissue MoveTo() so live ones speed up
    /// </summary>
    private void OnIndexIncreased(int newIndex)
    {
        currentSpeed += speedIncrement;

        var allBehaviours = Object.FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);

        // Then pick out only those that implement IMoveable:
        foreach (var mb in allBehaviours)
        {
            if (mb is IMoveable mover)
            {
                mover.MoveTo(endPos.position, currentSpeed);
            }
        }
    }
}
			

Used assets