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
- POLYGON - Kids Pack
Publisher: Synty studio's
- Violet Themed UI
Publisher: The GUI Guy
- Cartoon FX Remaster Free
Publisher: Jean Moreno
- FEEL
Publisher: MoreMountains