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.
Developed using a rapid prototyping approach to quickly iterate on core features, validate ideas through feedback, and deliver a functional proof-of-concept efficiently.
Year
2025
Genre
Endless runner
Platform
PC
Take a look!

Code Highlights
C#
using UnityEngine;
public class PlayerController : MonoBehaviour
{
[Header("Player Settings")]
[SerializeField] private GameObject playerVisual;
[Header("Jump Settings")]
[SerializeField] private float jumpForce = 5f; // Force applied when jumping
[SerializeField] private LayerMask groundLayer; // Ground layer for raycast
[Header("Gravity Modifiers")]
[SerializeField] private float fallMultiplier = 2.5f; // Gravity multiplier when falling
private PlayerInputActions inputActions;
private Rigidbody rb;
private bool jumpPressed = false;
private PlayerDamageable damageable;
private BounceWalk playerBounceWalk;
// Setup references and input bindings
private void Awake()
{
rb = GetComponent<Rigidbody>();
inputActions = new PlayerInputActions();
inputActions.Enable();
// Bind jump action
inputActions.Player.Jump.performed += Jump_performed;
// Subscribe to death event
damageable = GetComponent<PlayerDamageable>();
damageable.OnDeath += HandleDeath;
// Reference to bounce walking visuals (if any)
playerBounceWalk = GetComponent<BounceWalk>();
}
// Called on a physics update
private void FixedUpdate()
{
// Apply jump if pressed and grounded
if (jumpPressed && IsGrounded())
{
Vector3 v = rb.velocity;
v.y = jumpForce;
rb.velocity = v;
}
// Apply extra gravity when falling for snappier jumps
if (rb.velocity.y < 0f)
{
Vector3 v = rb.velocity;
v.y += Physics.gravity.y * (fallMultiplier - 1f) * Time.deltaTime;
rb.velocity = v;
}
// Reset jump flag after applying it
jumpPressed = false;
}
// Check if player is on the ground using a downward raycast
private bool IsGrounded()
{
return Physics.Raycast(transform.position, Vector3.down, 0.6f, groundLayer);
}
// Called when the jump input is triggered
private void Jump_performed(UnityEngine.InputSystem.InputAction.CallbackContext obj)
{
// Play feedback only when grounded
if (IsGrounded())
FeedbackManager.Instance.GetPlayerJumpFeedback().PlayFeedbacks();
jumpPressed = true;
}
// Called when the player dies
private void HandleDeath()
{
Debug.Log("Player is dead");
GameManager.Instance.GameOver();
}
}
C#
using UnityEngine;
using System.Collections;
public class BounceWalk : MonoBehaviour
{
[SerializeField] private Transform visual; // The visual element to bounce (e.g. player model)
[SerializeField] private float bounceHeight = 0.2f; // How high the bounce goes
[SerializeField] private float bounceDuration = 0.4f;// Total duration of a full bounce cycle (up + down)
[SerializeField] private float rotationAngle = 15f; // Tilt angle during bounce
private bool flip = false; // Used to alternate rotation direction
[SerializeField] private bool canBounce = false; // Whether bounce animation is allowed to play
private Coroutine bounceLoop; // Reference to the bounce coroutine
// Starts the bounce animation loop if it's not already running
public void StartBounceLoop()
{
if (bounceLoop == null)
bounceLoop = StartCoroutine(BounceLoop());
}
// Stops the bounce animation and resets position/rotation
public void StopBounceLoop()
{
if (bounceLoop != null)
{
StopCoroutine(bounceLoop);
bounceLoop = null;
// Reset visual back to resting position and rotation
visual.localPosition = Vector3.zero;
visual.localRotation = Quaternion.identity;
}
}
// Coroutine that continuously bounces the visual up/down and rotates it slightly
private IEnumerator BounceLoop()
{
Vector3 basePos = visual.localPosition;
while (canBounce)
{
// Calculate top position and alternating rotation
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 Phase
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 Phase
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 avoid floating-point drift
visual.localPosition = basePos;
visual.localRotation = Quaternion.identity;
}
bounceLoop = null;
}
// External toggle to start or stop the bounce effect
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; // Toggle whether this platform should be harmful
private Collider col; // Cached reference to the collider
private float hazardSpeed; // Movement speed
private Vector3 hazardTarget; // Target position to move toward
private bool isMoving; // Is this hazard currently moving?
private float localTimeScale = 1f; // Time scale modifier for this object (used to slow or speed up movement)
// Called by other scripts to move this hazard to a new position
public void MoveTo(Vector3 targetPos, float speed)
{
hazardTarget = targetPos;
hazardSpeed = speed;
isMoving = true;
localTimeScale = 1f; // Reset time scale to normal
}
// Called when the object is created
private void Awake()
{
col = GetComponent<Collider>();
UpdateCollider(); // Enable/disable collider based on isHazard value
}
// Runs automatically in the Unity Editor when isHazard is changed in the inspector
private void OnValidate()
{
if (col == null) col = GetComponent<Collider>();
UpdateCollider();
}
// Allows hazard state to be updated at runtime by other scripts
public void SetHazard(bool hazardState)
{
isHazard = hazardState;
UpdateCollider();
}
// Enable or disable the collider based on isHazard
private void UpdateCollider()
{
col.enabled = isHazard;
}
// Called when something enters this object's collider
private void OnTriggerEnter(Collider other)
{
// Check if the object has a PlayerDamageable component
var hit = other.GetComponent<PlayerDamageable>();
if (hit != null)
{
hit.PlayerHit(); // Trigger damage
}
}
// Set movement speed while it's already moving (for dynamic adjustments)
public void SetSpeed(float newSpeed)
{
hazardSpeed = newSpeed;
}
// Set custom time scale for this object (e.g., slow motion or speed up)
public void SetTimeScale(float t)
{
localTimeScale = t;
}
// Handles movement logic each frame
private void Update()
{
if (!isMoving) return;
float spd = hazardSpeed * Time.deltaTime * localTimeScale;
transform.position = Vector3.MoveTowards(transform.position, hazardTarget, spd);
// If the hazard reached its destination, destroy it
if (Vector3.Distance(transform.position, hazardTarget) < 0.01f)
Destroy(gameObject);
}
}
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; // Where the obstacle spawns
[SerializeField] private Transform endPos; // Where the obstacle should move to
[Header("Obstacle Settings")]
[SerializeField] private List<GameObject> obstacles = new List<GameObject>(); // List of obstacle prefabs
[SerializeField] private float spawnInterval = 2f; // Time between spawn attempts
[SerializeField] private float spawnChance = 0.5f; // Chance for an obstacle to spawn per interval
[SerializeField] private float spawnDelay = 1f; // Cooldown after a successful spawn
[Header("Speed Settings")]
[SerializeField] private float baseSpeed = 1f; // Starting speed of obstacles
[SerializeField] private float speedIncrement = .5f; // Speed increase per level/index change
private float currentSpeed; // Current speed for new and existing obstacles
private bool canSpawn = true; // Whether spawning is currently allowed
private Coroutine spawnRoutine; // Reference to the active spawn coroutine
private readonly List<IMoveable> activeObstacles = new List<IMoveable>(); // Optional: to keep track of obstacles
private void Awake()
{
// Initialize starting speed
currentSpeed = baseSpeed;
}
private void Start()
{
// Subscribe to index change event to increase speed dynamically
ScoreManager.Instance.OnIndexChanged += OnIndexIncreased;
}
// Start the continuous spawning coroutine
public void StartSpawning()
{
if (spawnRoutine == null)
spawnRoutine = StartCoroutine(SpawnRoutine());
}
// Stop the continuous spawning coroutine
public void StopSpawning()
{
if (spawnRoutine != null)
{
StopCoroutine(spawnRoutine);
spawnRoutine = null;
}
}
// Coroutine to attempt spawning obstacles at regular intervals
private IEnumerator SpawnRoutine()
{
while (true)
{
TrySpawn();
yield return new WaitForSeconds(spawnInterval);
}
}
// Delay after each successful spawn to prevent immediate back-to-back spawns
private IEnumerator ResetSpawnCooldown()
{
yield return new WaitForSeconds(spawnDelay);
canSpawn = true;
}
// Try to spawn an obstacle based on chance and cooldown
private void TrySpawn()
{
if (!canSpawn) return;
if (Random.value <= spawnChance)
{
SpawnOne();
canSpawn = false;
StartCoroutine(ResetSpawnCooldown());
}
}
// Instantiates a random obstacle from the list and moves it
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);
}
// Sends a MoveTo call to the obstacle so it starts moving toward the endpoint
private void MoveObstacle(GameObject obj)
{
var moveable = obj.GetComponent<IMoveable>();
if (moveable != null)
moveable.MoveTo(endPos.position, currentSpeed);
}
/// <summary>
/// Called every time the score index increases.
/// - Speeds up all existing obstacles that implement IMoveable
/// </summary>
private void OnIndexIncreased(int newIndex)
{
// Increase movement speed for future and current obstacles
currentSpeed += speedIncrement;
// Find all MonoBehaviours in the scene
var allBehaviours = Object.FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);
// Reapply MoveTo with the new speed to all live IMoveable objects
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