classic endless runner effect
Here's how to set it up:
Basic Setup Structure
Hierarchy:
- Player (stationary at world position)
- Environment Parent (empty GameObject - this moves)
├─ Ground
├─ Obstacles
├─ Walls
└─ Props
1. Environment Mover Script
Attach this to your Environment Parent object:
using UnityEngine;
public class EnvironmentMover : MonoBehaviour
{
public float moveSpeed = 10f;
void Update()
{
// Move environment backward (toward camera/player)
transform.position += Vector3.back * moveSpeed * Time.deltaTime;
// Alternative: move left/right for side-scrolling
// transform.position += Vector3.left * moveSpeed * Time.deltaTime;
}
}
2. Player Controller (Stays in Place but can move left/right)
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float strafeSpeed = 5f;
public float minX = -3f;
public float maxX = 3f;
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
// Move player left/right only
Vector3 newPosition = transform.position + Vector3.right * horizontal * strafeSpeed * Time.deltaTime;
// Clamp position so player stays in bounds
newPosition.x = Mathf.Clamp(newPosition.x, minX, maxX);
transform.position = newPosition;
}
}
3. Ground/Platform Looping (Essential for Endless Effect)
For continuous ground that never ends:
using UnityEngine;
public class GroundLoop : MonoBehaviour
{
public float groundLength = 20f; // Length of your ground piece
public Transform player;
void Update()
{
// When ground piece passes behind player, move it forward
if (transform.position.z < player.position.z - groundLength)
{
transform.position += Vector3.forward * groundLength * 2;
}
}
}
Alternative Method - Using multiple ground pieces:
using UnityEngine;
public class GroundLoopSystem : MonoBehaviour
{
public GameObject groundPrefab;
public Transform player;
public float groundLength = 20f;
public int numberOfGroundPieces = 3;
private GameObject[] groundPieces;
void Start()
{
groundPieces = new GameObject[numberOfGroundPieces];
for (int i = 0; i < numberOfGroundPieces; i++)
{
Vector3 spawnPos = Vector3.forward * (groundLength * i);
groundPieces[i] = Instantiate(groundPrefab, spawnPos, Quaternion.identity, transform);
}
}
void Update()
{
foreach (GameObject ground in groundPieces)
{
if (ground.transform.position.z < player.position.z - groundLength)
{
// Find furthest ground piece
float maxZ = float.MinValue;
foreach (GameObject g in groundPieces)
{
if (g.transform.position.z > maxZ)
maxZ = g.transform.position.z;
}
// Move this ground piece ahead
ground.transform.position = new Vector3(0, 0, maxZ + groundLength);
}
}
}
}
4. Complete Scene Setup Example
Step-by-step:
-
Create Player:
- Create Cube or Capsule
- Position at (0, 1, 0)
- Add the PlayerController script
- Tag as "Player"
-
Create Environment Parent:
- Create Empty GameObject named "Environment"
- Position at (0, 0, 0)
- Add EnvironmentMover script
-
Create Ground:
- Create Plane (scale to 2, 1, 10 or larger)
- Make it child of Environment
- Position at (0, 0, 0)
- Duplicate 2-3 times, space them along Z-axis
- Add GroundLoop script to each
-
Add Obstacles:
- Create cubes/objects as children of Environment
- They'll move with the environment automatically
5. Advanced: Speed Increase Over Time
using UnityEngine;
public class EnvironmentMoverAdvanced : MonoBehaviour
{
public float startSpeed = 5f;
public float maxSpeed = 20f;
public float acceleration = 0.5f;
private float currentSpeed;
void Start()
{
currentSpeed = startSpeed;
}
void Update()
{
// Gradually increase speed
currentSpeed = Mathf.Min(currentSpeed + acceleration * Time.deltaTime, maxSpeed);
// Move environment
transform.position += Vector3.back * currentSpeed * Time.deltaTime;
}
}
6. Camera Setup
For best effect:
using UnityEngine;
public class CameraFollow : MonoBehaviour
{
public Transform player;
public Vector3 offset = new Vector3(0, 5, -10);
void LateUpdate()
{
// Keep camera following player (even though player isn't really moving)
transform.position = player.position + offset;
transform.LookAt(player);
}
}
Key Tips:
✅ Player stays at fixed Z position (like Z = 0)
✅ Environment moves in negative Z (backward)
✅ Ground loops/respawns to create endless feeling
✅ Obstacles are children of Environment so they move automatically
✅ Player can only move left/right (X-axis)
Common Issues & Fixes:
Problem: Ground has visible seams
Fix: Make sure ground pieces are exactly the same length and positioned precisely
Problem: Objects fall through ground
Fix: Make sure Rigidbody on player has "Is Kinematic" checked, or use Character Controller
Problem: Movement feels jerky
Fix: Always use Time.deltaTime and consider using FixedUpdate() for physics-based movement
DETAILED CODE BREAKDOWN
1. ENVIRONMENT MOVER - Line by Line
using UnityEngine;
What it does: Imports Unity's core functionality so you can use classes like MonoBehaviour, Vector3, Time, etc.
public class EnvironmentMover : MonoBehaviour
What it does:
public class- Creates a new script component you can attach to GameObjectsEnvironmentMover- The name of your script (must match filename): MonoBehaviour- Inherits from Unity's base class, giving access toUpdate(),Start(),transform, etc.
public float moveSpeed = 10f;
What it does:
public- Makes this variable visible in Unity Inspector so you can adjust it without editing codefloat- Decimal number (like 10.5, 5.0, etc.)moveSpeed- Variable name= 10f- Default value of 10, thefmeans "float" (not double)
Why: Controls how fast the environment moves. Higher number = faster movement.
void Update()
What it does: This function runs every single frame (typically 60 times per second)
Why: Perfect for continuous movement since it keeps updating position constantly
transform.position += Vector3.back * moveSpeed * Time.deltaTime;
This is the CORE line. Let's break it down piece by piece:
transform
- Every GameObject has a
transformcomponent - Contains position, rotation, and scale
transform.positionis the current location in 3D space (x, y, z coordinates)
transform.position +=
+=means "add to the current value"- Example: If position is (0, 0, 0) and you add (0, 0, 1), new position becomes (0, 0, 1)
- This creates relative movement (moving FROM where you are)
Alternative:
transform.position = new Vector3(0, 0, 0); // ABSOLUTE - teleports to exact location
transform.position += Vector3.back; // RELATIVE - moves from current position
Vector3.back
Vector3 is a structure representing direction and magnitude in 3D space (x, y, z)
Unity's built-in directions:
Vector3.forward = (0, 0, 1) // Positive Z - toward "north"
Vector3.back = (0, 0, -1) // Negative Z - toward "south"
Vector3.right = (1, 0, 0) // Positive X - toward "east"
Vector3.left = (-1, 0, 0) // Negative X - toward "west"
Vector3.up = (0, 1, 0) // Positive Y - toward sky
Vector3.down = (0, -1, 0) // Negative Y - toward ground
Why Vector3.back?
- Player faces forward (positive Z)
- Environment must come TOWARD player
- So environment moves in NEGATIVE Z (backward)
- This creates illusion player is moving forward
Visual:
Player at Z=0 (stays here)
↓
🧍
←←←← Environment moving backward (negative Z)
[Ground][Obstacles][Trees]
* moveSpeed
- Multiplies the direction by speed
Vector3.back * 10= (0, 0, -10)- Without this, movement would be only 1 unit per frame (way too fast!)
Example:
Vector3.back * 5 = (0, 0, -5) // Moves 5 units backward
Vector3.back * 20 = (0, 0, -20) // Moves 20 units backward (faster)
* Time.deltaTime
THIS IS CRITICAL - THE MOST IMPORTANT CONCEPT
Problem: Update() runs at different speeds on different computers
- Fast PC: 120 frames per second (FPS)
- Slow PC: 30 frames per second (FPS)
Without Time.deltaTime:
transform.position += Vector3.back * 10; // Moves 10 units PER FRAME
- On 60 FPS: Moves 10 × 60 = 600 units per second
- On 30 FPS: Moves 10 × 30 = 300 units per second
- Result: Different speeds on different computers! ❌
With Time.deltaTime:
transform.position += Vector3.back * 10 * Time.deltaTime;
What is Time.deltaTime?
- Time elapsed since last frame (in seconds)
- At 60 FPS:
Time.deltaTime ≈ 0.0166seconds - At 30 FPS:
Time.deltaTime ≈ 0.0333seconds
The Math:
- 60 FPS:
10 * 0.0166 * 60 frames = 10 units per second - 30 FPS:
10 * 0.0333 * 30 frames = 10 units per second - Result: Same speed on all computers! ✅
Think of it like this:
moveSpeed= units per second (not per frame)Time.deltaTimeconverts it to units per frame
Complete Line Explained:
transform.position += Vector3.back * moveSpeed * Time.deltaTime;
In plain English:
"Every frame, move this object backward (negative Z direction) at a speed of moveSpeed units per second"
Numerical Example:
- Current position: (0, 0, 100)
- moveSpeed: 10
- Time.deltaTime: 0.016 (assuming 60 FPS)
- Calculation: (0, 0, -1) × 10 × 0.016 = (0, 0, -0.16)
- New position: (0, 0, 100) + (0, 0, -0.16) = (0, 0, 99.84)
After 1 second (60 frames): Position becomes approximately (0, 0, 90) The object moved 10 units backward in 1 second!
2. PLAYER CONTROLLER - Detailed Breakdown
using UnityEngine;
public class PlayerController : MonoBehaviour
{
public float strafeSpeed = 5f;
public float minX = -3f;
public float maxX = 3f;
What these variables do:
strafeSpeed- How fast player moves left/right (side-to-side movement is called "strafing")minX- Leftmost boundary (-3 means 3 units to the left of origin)maxX- Rightmost boundary (3 means 3 units to the right of origin)
Why we need boundaries: Without them, player could move infinitely left/right and go off-screen!
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
Input.GetAxis("Horizontal") - Gets keyboard/controller input:
- Press A or Left Arrow: Returns -1.0 (move left)
- Press D or Right Arrow: Returns +1.0 (move right)
- Press nothing: Returns 0.0 (no movement)
- Smoothed: Gradually transitions between values (not instant)
Example:
// If pressing D:
horizontal = 1.0f; // Move right
// If pressing A:
horizontal = -1.0f; // Move left
// If pressing nothing:
horizontal = 0.0f; // Don't move
Vector3 newPosition = transform.position + Vector3.right * horizontal * strafeSpeed * Time.deltaTime;
Breaking it down:
-
transform.position- Current player position, e.g., (0, 1, 0) -
Vector3.right- Direction vector (1, 0, 0) - pointing right -
Vector3.right * horizontal- If horizontal = 1 (pressing D): (1, 0, 0) × 1 = (1, 0, 0) - move right
- If horizontal = -1 (pressing A): (1, 0, 0) × -1 = (-1, 0, 0) - move left
- If horizontal = 0 (no input): (1, 0, 0) × 0 = (0, 0, 0) - don't move
-
* strafeSpeed * Time.deltaTime- Same concept as before - makes movement frame-rate independent
- strafeSpeed controls how fast
-
transform.position +- Adds movement to current position
- Stores in temporary variable
newPosition(doesn't apply yet)
Example Calculation:
- Current position: (0, 1, 0)
- Pressing D (horizontal = 1)
- strafeSpeed = 5
- Time.deltaTime = 0.016
- Movement: (1, 0, 0) × 1 × 5 × 0.016 = (0.08, 0, 0)
- newPosition: (0, 1, 0) + (0.08, 0, 0) = (0.08, 1, 0)
newPosition.x = Mathf.Clamp(newPosition.x, minX, maxX);
Mathf.Clamp(value, min, max) - Restricts a value between min and max
How it works:
Mathf.Clamp(5, 0, 10) = 5 // Within range, stays same
Mathf.Clamp(15, 0, 10) = 10 // Too high, clamped to max
Mathf.Clamp(-5, 0, 10) = 0 // Too low, clamped to min
In our case:
// If newPosition.x = 4 (player went too far right)
// minX = -3, maxX = 3
newPosition.x = Mathf.Clamp(4, -3, 3) = 3; // Stopped at right boundary
// If newPosition.x = -5 (player went too far left)
newPosition.x = Mathf.Clamp(-5, -3, 3) = -3; // Stopped at left boundary
Why: Keeps player within playable area, can't escape boundaries
transform.position = newPosition;
Finally applies the new position to the player
- We calculated the movement
- We clamped it to boundaries
- Now we actually move the player
Why we used a temporary variable:
- Allows us to modify X coordinate only
- Keeps Y and Z unchanged
- If we modified
transform.position.xdirectly, it wouldn't work (Unity doesn't allow it)
3. GROUND LOOP - Detailed Breakdown
public class GroundLoop : MonoBehaviour
{
public float groundLength = 20f;
public Transform player;
Variables:
groundLength- How long your ground piece is in Unity unitsTransform player- Reference to player's transform (drag player object here in Inspector)
Why Transform not GameObject?
- We only need position data
Transformis lighter and more direct
void Update()
{
if (transform.position.z < player.position.z - groundLength)
Breaking down the condition:
-
transform.position.z- Current Z position of THIS ground piece -
player.position.z- Player's Z position (usually stays around 0) -
player.position.z - groundLength- Trigger point behind player
Visual explanation:
Trigger Point Player
↓ ↓
[-20]=========[-10]=============[0]=========[10]==========
Ground Piece A Ground Piece B
When does the condition trigger?
- Player at Z = 0
- groundLength = 20
- Trigger point: 0 - 20 = -20
- If ground piece A is at Z = -21 (past trigger point), it needs to teleport forward
The Logic: "If this ground piece has passed 20 units behind the player, teleport it ahead"
transform.position += Vector3.forward * groundLength * 2;
Breaking it down:
-
Vector3.forward- Direction (0, 0, 1) - positive Z -
* groundLength * 2- How far to move forward- If groundLength = 20
- Movement: (0, 0, 1) × 20 × 2 = (0, 0, 40)
Why multiply by 2?
- You typically have 2-3 ground pieces
- Moving by
groundLength × 2ensures it goes ahead of other pieces - Creates seamless loop
Visual:
BEFORE:
Ground A at Z=-21 (behind player)
Ground B at Z=0
Ground C at Z=20
AFTER (Ground A teleports):
Ground B at Z=0
Ground C at Z=20
Ground A at Z=19 (was -21, moved +40)
4. ADVANCED VERSION - Multiple Ground Pieces
private GameObject[] groundPieces;
GameObject[] - Array (list) of GameObjects
- Can store multiple ground pieces
- Access them like:
groundPieces[0],groundPieces[1], etc.
void Start()
{
groundPieces = new GameObject[numberOfGroundPieces];
new GameObject[3] - Creates empty array with 3 slots
groundPieces[0]= nullgroundPieces[1]= nullgroundPieces[2]= null
for (int i = 0; i < numberOfGroundPieces; i++)
{
Vector3 spawnPos = Vector3.forward * (groundLength * i);
groundPieces[i] = Instantiate(groundPrefab, spawnPos, Quaternion.identity, transform);
}
for loop - Repeats code multiple times
Iteration breakdown:
// i = 0:
spawnPos = (0, 0, 1) * (20 * 0) = (0, 0, 0) // First ground at origin
// i = 1:
spawnPos = (0, 0, 1) * (20 * 1) = (0, 0, 20) // Second ground 20 units ahead
// i = 2:
spawnPos = (0, 0, 1) * (20 * 2) = (0, 0, 40) // Third ground 40 units ahead
Instantiate() - Creates a copy of a GameObject
groundPrefab- What to copyspawnPos- Where to create itQuaternion.identity- No rotation (0, 0, 0)transform- Parent object (makes it child of this object)
foreach (GameObject ground in groundPieces)
foreach - Loop through each item in array
- Simpler than
forloop when you need to check all items groundis temporary variable representing current ground piece
float maxZ = float.MinValue;
foreach (GameObject g in groundPieces)
{
if (g.transform.position.z > maxZ)
maxZ = g.transform.position.z;
}
Purpose: Find the furthest ground piece forward
float.MinValue - Smallest possible float number (like -infinity)
- Ensures any ground position will be larger
- Common trick for finding maximum values
Example:
// Ground A at Z=0
// Ground B at Z=20
// Ground C at Z=40
// After loop: maxZ = 40 (furthest forward)
ground.transform.position = new Vector3(0, 0, maxZ + groundLength);
Teleports ground piece ahead of the furthest piece
Example:
- maxZ = 40 (furthest piece)
- groundLength = 20
- New position: (0, 0, 40 + 20) = (0, 0, 60)
Result: Ground that was behind player is now ahead of all other pieces!
KEY CONCEPTS SUMMARY
Time.deltaTime- ALWAYS use for smooth, frame-rate independent movement+=vs=-+=adds to current (relative),=sets exact value (absolute)Vector3directions - Unity's coordinate system for 3D spaceMathf.Clamp- Restricts values within rangetransform.position- Object's location in world space- Arrays - Store multiple objects of same type
- Loops - Repeat code multiple times efficiently
Comments