Skip to main content

Infinite Runner

A fast-paced endless runner — my first shipped game.

Status: Prototype complete — polishing for itch.io release


Overview

A fast-paced side-scrolling endless runner where the player dodges, slides, and jumps through procedurally generated obstacle courses. The difficulty ramps continuously; the goal is to outlast your previous best distance.


Motivation

An infinite runner was a scoped enough project to explore new concepts without drowning in complexity:

  • Procedural generation — how to make levels that never repeat but always feel fair
  • Difficulty curves — mathematical models for scaling challenge over time

Technologies

ToolUse
C++Gameplay systems
Git + GitHubVersion control

Screenshots

(Screenshots will go here — static/img/infinite-runner/)


Features

  • Procedurally generated obstacle chunks (no two runs identical)
  • Four movement mechanics: run, jump, double-jump, slide
  • Increasing speed over time — from comfortable to frantic
  • Combo multiplier: survive consecutive obstacles without slowing
  • High score system with local leaderboard
  • Replay viewer — watch your run back at the end

Architecture

GameMode

LevelGenerator → ChunkPool

PlayerPawn ← InputComponent

MovementComponent

ObstacleDetector → GameMode (death event)

ScoreManager ← TimerComponent

LeaderboardSave (SaveGame)

Key design decision: Chunk-based generation. The world is built from ~30 handcrafted chunk assets. The generator selects and sequences them using a weighted randomizer that adjusts weights based on current difficulty — harder chunks appear more frequently as distance increases.


Interesting Problem: Fair Procedural Difficulty

Problem: Pure random chunk selection produced runs that were unfair — sometimes brutal at the start, sometimes easy 5 minutes in.

Investigation: I graphed the difficulty profile of each chunk (obstacle density × required reaction time). Then I modeled the intended difficulty curve as a function of distance.

Solution: A DifficultyBudget system: each chunk has a difficulty score (1–10). The generator maintains a rolling 5-chunk window and only selects a new chunk whose score falls within [target ± 1.5]. Target is derived from distance via a smooth exponential curve.

Outcome: The game now feels consistently fair at the start and consistently difficult at high distances — without the generator ever repeating the same sequence.


Development Timeline

PhaseDurationWhat happened
Core movement2 weeksJump, slide, double-jump, death
Chunk generation3 weeksPool, sequencer, difficulty budget
Score + leaderboard1 weekSave system, UI
PolishOngoingParticles, camera shake, juice

Lessons Learned

  • Chunk design matters more than the algorithm. The generator is only as good as the chunks it draws from. Spent 40% of generation time tweaking chunk content, not code.
  • The built-in save system is simpler than expected. JSON was my first instinct; the built-in save system handled everything I needed without extra code.

Future Improvements

  • Mobile controls (swipe gestures)
  • Additional biome themes (snow, desert, space)
  • Online leaderboard
  • Power-ups (magnet, shield, score boost)
  • Accessibility: adjustable speed cap

Dev Journey

The finished game looks clean. Getting here wasn't. This is the actual story.


Attempt 1: Movement That Felt Wrong

The first movement controller worked — technically. The character ran, jumped, and slid. But something felt off that I couldn't name. Playtesting confirmed it: the jump felt floaty, the slide felt instant, and the double-jump was almost useless.

What I thought the problem was: Input lag.

What it actually was: The jump arc was symmetric — same time going up as coming down. Real-feeling jumps fall faster than they rise. I added a gravity multiplier that kicks in after the apex. The difference was immediate.

The slide had the opposite problem: it ended before the player felt like they'd slid. Fixed by adding a minimum slide duration and a subtle camera dip. Suddenly it felt like a committed move instead of a button press.

Time lost: About 4 days reworking something I thought was done.


Attempt 2: Procedural Generation That Wasn't

My first "procedural" level generator wasn't procedural — it was a shuffle. I had 10 chunks and I played them in random order. Players noticed within 3 runs.

I rebuilt it with a proper chunk pool and a weighted selection system. That solved repetition, but created a new problem: the randomness had no memory. You could get three hard chunks back to back right at the start, or three easy ones 10 minutes in. Neither is fun.

First fix attempt: Cap consecutive hard chunks at 2. This helped but felt arbitrary and broke down at high speeds.

Actual fix: The DifficultyBudget system. Each chunk has a rated difficulty score. The generator tracks a rolling window and only selects chunks within a target range that rises smoothly over distance. Now the curve is predictable without being predictable.

Time lost: 3 weeks. The first two weeks produced something that worked but wasn't right. The third week produced something that actually solved the problem.


Attempt 3: The Score System I Deleted

I built a combo multiplier first — survive obstacles in a row, multiply your score. It felt good in testing. Then I added time bonuses. Then distance bonuses. Then speed bonuses.

By the time I was done, I had no idea what my score meant. Neither did anyone I showed it to.

I deleted everything except distance-based scoring and the combo multiplier. Simpler, clearer, more satisfying.

Lesson: Adding systems is easy. Knowing which ones to cut is the skill.


Attempt 4: Performance — Spawn and Destroy

My first chunk system spawned chunks from scratch and destroyed them when they scrolled off screen. This worked fine at normal speed. At high speed, the GC spikes were noticeable — small freezes right when the game was most intense.

Object pooling was the obvious fix and it took about a day to implement. Chunks are now recycled instead of destroyed. The spikes disappeared.

What I learned: Profile before optimizing. I assumed the problem was the chunk geometry. It was the allocation/deallocation cycle, which I only confirmed after adding a simple frame timer log.


The Leaderboard That Saved Wrong

The save system worked on my machine. On a second test device, it didn't — the leaderboard loaded blank every time.

The bug: I was constructing the save file path with a hardcoded separator (\ on Windows). The second device ran a different OS. Switched to the engine's built-in path API. Fixed everywhere at once.

Time lost: Half a day plus an embarrassing amount of confusion before I thought to check the path.


What the Dev Timeline Actually Looked Like

The timeline in the overview shows clean phases. Here's what it actually looked like:

Week 1: Movement prototype (too floaty)
Week 2: Fixed jump arc, fixed slide feel
Week 3: First chunk generator (basically a shuffle)
Week 4: Rebuilt generator with weighted selection
Week 5: Difficulty budget system (first version too aggressive)
Week 6: Difficulty budget v2 (actually worked)
Week 7: Score system v1 (overcomplicated)
Week 8: Score system v2 (deleted 70% of it)
Week 9: Leaderboard + save (broke on second device)
Week 10: Fixed save, added pooling, polish

Most of the time was spent on things that didn't ship. That's normal. The shipped version is the sum of the things that survived.


Play the Game

Play on itch.io
GitHub Repository (source code)