Stream Runner: Difficulty as a Speed Curve, UI Reduced to Two Buttons
A minimalist auto-runner case study: score equals distance, a one-obstacle-one-button mapping, and how a 0.6s dodge window keeps high speeds readable.
분석
- Auto-run: base speed 10 units/s, ramping +0.5 per second linearly
- Ground obstacles require SLIDE, air obstacles require JUMP; dodge window is 0.6s
- Score equals distance (1 unit = 1 point, rounded at settlement); 3 lives, run ends on the third crash
- High score is compared and updated at settlement; Play Again restarts instantly
- Tap Start Game in menu → data resets → core/runner.start
- Read the obstacle type while auto-scrolling → press the matching button → dodge or lose 1 life
- runner.finish → score/distance written back → result screen shows score and high score
- A hands-off run lasts ~6 seconds for ~65 points: an ultra-short loop of about ten seconds per round
- core/runner carries all gameplay: five actions (start/jump/slide/setSpeed/reset) plus runner.crash and runner.finish events
- Obstacle interval starts at 2s, shrinks 0.05 per second, floored at 0.8s — stacking with the speed curve into one pressure curve
- All parameters are data-key driven (runnerSpeed/obstacleInterval/jumpDuration, etc.); tuning requires no code change
- Known limitation: skin text {{key}} placeholders currently render literally, so HUD numbers do not tick
핵심 코드
데모
Tap Start Game to run: SLIDE for ground obstacles, JUMP for air obstacles. Three lives, and your score is exactly your distance.
아티클
Stream Runner: Difficulty as a Speed Curve, UI Reduced to Two Buttons
Essence in One Sentence
Stream Runner is a minimalist auto-runner (Runner archetype with a Dodge / Avoid core): the character rolls forward on its own, and the player's only tools are JUMP and SLIDE to dodge randomly spawned air and ground obstacles. Your score is exactly your distance, and three crashes end the run. Difficulty is driven entirely by a single linear speed curve, while readability is guaranteed by a strict one-obstacle-one-button mapping.
Behavior Loop
- Start: tap Start Game on the menu; score and state reset, and the
core/runnermodule starts. - Auto-move: speed starts at 10 units/s and ramps +0.5 per second linearly; score and distance accumulate together (1 unit = 1 point).
- Read the obstacle: obstacles spawn randomly — 50% ground, 50% air.
- React: SLIDE for ground obstacles, JUMP for air ones; each action opens a 0.6-second dodge window of the matching type.
- Take the consequence: failing to dodge inside the window costs 1 of 3 lives and broadcasts a
runner.crashevent. - Settle: the third crash triggers the result state, showing the final score and updating the high score; Play Again starts a new run immediately.
Measured hands-off, one run lasts about 6 seconds and scores around 65 — a textbook "ten seconds per round" ultra-short loop. Try it yourself in the playable demo at the end of this article.
Primitive Annotations
AutoMove(speed(t))– continuous forward motion with a linearly ramping speedSpawnObstacle(interval(t))– obstacle spawns on a shrinking intervalJump(window)/Slide(window)– open a 0.6s type-matched dodge windowCrash(type, lives)– failed dodge: lose a life and broadcast the eventFinish(reason)– settle the run when lives deplete (or the time cap is reached)UpdateHighScore()– compare and write back the high score at settlement
Parameter Cheat Sheet
| Parameter | Meaning | Effective Value | Unit | |-----------|---------|-----------------|------| | runnerSpeed | Base speed | 10 | units/s | | Speed ramp | Acceleration per second | +0.5 | units/s² | | obstacleInterval | Initial spawn interval | 2 | s | | Interval shrink | Reduction per second | 0.05 | s | | Interval floor | Minimum spawn interval | 0.8 | s | | jumpDuration / slideDuration | Dodge window | 0.6 | s | | runnerLives | Lives | 3 | count | | gameDuration | Time cap (fallback; rarely reached) | 60 | s |
Design Trade-offs
- The collection economy is cut entirely: the original game is a Three.js 3D stream-rolling ball with two tiers of collectibles (droplets +10, stars +50). The rebuild removes all of it and keeps only dodging. The payoff: score equals distance with zero explanation, and the speed curve becomes the single difficulty knob — tune one number, tune the whole game.
- A binary decision mapping: ground → SLIDE, air → JUMP. Every obstacle maps to exactly one button, and the button label is the action name. No tutorial is needed, and failure attribution is unambiguous — a crash means you pressed wrong or didn't press, so players own their mistakes.
- Window 0.6s < interval floor 0.8s: even at maximum speed and obstacle density, only one obstacle is ever in play at a time. There is no such thing as an unfair barrage — a hard guarantee of readability at high speed.
- Two stacked curves: the linear speed ramp (+0.5/s) shrinks the reaction-time budget while the shrinking spawn interval (-0.05/s, floored at 0.8s) raises action density. Together they tighten pressure, but both slopes are gentle enough that the first few seconds play like a tutorial.
- Three lives: the first two crashes are free lessons; only the third settles the run. Failure tolerance manufactures the "so close" regret that fuels one more run.
Implementation Notes
- Module split:
common-uibinds and forwards skin button events;core/runnercarries all gameplay logic (movement / spawning / collision / scoring), exposing five actions — start, jump, slide, setSpeed, reset — plus therunner.crashandrunner.finishevents. - LogicGraph orchestration: on entering gameplay, data is zeroed and
runner.startis called;player.jump/player.slideevents forward directly to module actions;runner.finishwrites back score/distance, sets gameOver, and emitsgame.overto transition into the result state. - Time-unit conversion: v2-kernel's update callback passes milliseconds; the module divides by 1000 and integrates speed and distance in seconds.
- Fully data-key driven parameters: runnerSpeed / obstacleInterval / jumpDuration / slideDuration / gameDuration / runnerLives are all read via
ctx.data, so live tuning never touches code. - Presentation layer: a 750×1334 portrait canvas with PIXI rendering the skin layer; a one-line HUD of three text slots (Score / Dist / Lives) on top, and two large 320×100 buttons at the bottom inside the thumb zone.
- Known limitations of this build: skin text
{{key}}placeholders render literally (HUD numbers do not tick; data binding awaits kernel support), and initial values declared in the blueprint dataGraph (e.g. runnerSpeed=30) are not injected at runtime — the module fallbacks (10) are what actually take effect.
These trade-offs make Stream Runner a minimal specimen for studying "speed curve + readability" design: the mechanics fit in a single table, yet every parameter directly shapes game feel. Tap Start Game in the demo below and see how far three lives take you.
함께 볼 만한 글
120 seconds, 3 lives, 323 dots: eat and dodge through a block-city maze, where the 8-second power-pellet reversal is the only rhythm switch that matters.

This article dissects the core mechanics of roguelike slaying games, breaking down the construction of enemy density curves to create dynamic combat pacing. It explains how director systems use calculation to control enemy distribution for screen readability, analyzes hard rules limiting special effects to ensure visual clarity, and reveals the satisfaction derived from linear enemy growth paired with exponential player damage. The guide provides actionable advice for developers to balance numerical values with player experience.

A deep dive into the tap dodge mechanic, analyzing hitbox tolerance settings and near-miss feedback to balance failure risk. We also explore constructing bullet speed curves to achieve smooth difficulty progression, providing core design strategies for developers to ensure both responsive controls and long-term player retention.