HashCoreAI
← Blog
Game dev

Cum proiectăm un endless runner corect: nivele procedurale mereu rezolvabile (Planet Surfer)

Designing a Fair Endless Runner: Procedural Levels That Are Always Survivable (Planet Surfer)

Publicat 10 aprilie 2026 · 8 min citire

La HashCoreAI, Planet Surfer este endless runner-ul nostru neon prin spațiu: pilotezi o navă pe trei benzi printr-un cosmos synthwave, eviți piloni de cristal, treci pe sub porți de energie, sari peste asteroizi și aduni cristale. Nu există nivele desenate de mână. Pista este generată procedural, rând cu rând, în timp ce alergi. Asta creează o problemă de design care pare simplă, dar e perfidă: dacă lași la voia întâmplării, mai devreme sau mai târziu generatorul va plasa un obstacol în fiecare bandă în același timp și jucătorul moare fără nicio vină. Într-un joc unde scopul e „încă o tură”, o moarte nedreaptă strică tot.

Acest DEVLOG povestește cum am rezolvat asta: cum garantăm o cale de scăpare în fiecare rând, cum urcăm dificultatea fără să creăm vreodată un gol imposibil, cum dozăm momentele de respiro și cum am acordat scorul ca să se simtă corect. Cifrele de mai jos sunt valorile reale din codul jocului.

Contractul de bază: fiecare rând are cel puțin o bandă liberă

Regula fundamentală a generatorului nostru e formulată ca un contract, scris chiar în comentariul din cod: fiecare rând lasă cel puțin o bandă complet goală, astfel încât pista să fie mereu rezolvabilă doar prin schimbări de bandă. Săritura și plonjonul rămân opționale — abilitate pură pentru a sta într-o bandă cu cristale — dar nu sunt niciodată obligatorii pentru supraviețuire într-un rând generat aleator.

Avem trei benzi, la pozițiile X de -2,8, 0 și 2,8. Când generăm un rând, decidem mai întâi câte benzi blocăm (0, 1 sau 2), apoi alegem aleator care benzi. Niciodată toate trei. Asta singur elimină categoria de „moarte imposibilă” fără ca jucătorul să observe vreo schemă.

O cale de scăpare garantată nu înseamnă un joc ușor. Înseamnă un joc unde fiecare moarte e vina jucătorului — și exact asta îl face să apese „restart”.

Curba de dificultate: un singur parametru, normalizat

Toată dificultatea pornește dintr-o singură valoare normalizată. Luăm distanța parcursă și o împărțim la 5000, apoi o limităm între 0 și 1. Numim asta d. La începutul turei d e aproape 0; după ~5 km de joc devine 1 și rămâne acolo. Tot ce ține de dificultate citește din d:

Viteza nu crește liniar

Viteza navei pleacă de la 25 de unități/secundă și urcă spre maximul de 62. Crucial, nu folosim o rampă liniară — folosim o curbă de saturație exponențială: BASE + (MAX − BASE) × (1 − e^(−distanță / 4200)). Practic, asta înseamnă o accelerare rapidă și satisfăcătoare la început, apoi o apropiere tot mai lentă de plafon. Jocul se simte mereu mai rapid, dar nu ajunge niciodată într-o zonă în care reflexele umane sunt pur și simplu depășite.

De ce distanțăm rândurile în timp, nu în spațiu

Aceasta e probabil cea mai importantă decizie subtilă din tot generatorul. Dacă ai distanța fixă între rânduri în unități de lume, atunci pe măsură ce viteza crește, rândurile vin tot mai des în timp real — iar fereastra de reacție a jucătorului se prăbușește. La viteză maximă, jocul devine injust nu pentru că obstacolele sunt grele, ci pentru că nu mai ai timp fizic să reacționezi.

Soluția noastră: spațierea dintre rânduri e calculată din timp, nu din distanță. Înmulțim viteza curentă cu un interval țintă (0,52 s) și obținem o spațiere în unități de lume, limitată între 13 și 38. Cu cât mergi mai repede, cu atât rândurile sunt mai depărtate fizic — exact cât trebuie ca fereastra de reacție în secunde să rămână aproximativ constantă pe toată tura.

ParametruValoareRol
Viteză de bază → maximă25 → 62 u/sRampă exponențială, nu liniară
Distanța rampei4200Mai mare = accelerare mai lentă
Interval-țintă pe rând0,52 sFereastra de reacție rămâne corectă
Spațiere min/max13 / 38 uLimite de siguranță
Praguri de scor2000 / 5000 / 10000Recompense unice de progres

Rânduri forțate: provocare reală, dar mereu rezolvabilă

Doar schimbarea de bandă ar deveni plictisitoare. Așa că injectăm ocazional rânduri „forțate” care cer o acțiune verticală — dar le proiectăm să fie imposibil de greșit. Cu o șansă de 20% (și niciodată după 0,06 din curba de dificultate, ca să nu surprindem jucătorul în primele secunde), zidim solid două benzi cu obstacole „înalte” și lăsăm a treia bandă cu un singur obstacol jos (sari) sau o poartă de sus (plonjează).

Două garanții fac acest moment corect: este mereu rezolvabil cu o singură acțiune, iar o regulă explicită interzice două rânduri forțate consecutive. Nu poți fi prins niciodată într-o combinație sări-apoi-imediat-plonjează imposibilă. Iar în banda de scăpare punem un șir de cristale poziționate exact pentru acțiunea cerută (mai sus pentru săritură), astfel încât mișcarea corectă să fie și cea recompensată.

Momentele de respiro: pauzele care fac ritmul

Tensiunea continuă obosește. Un endless runner bun respiră. Aproximativ 1 din 8 rânduri (12% șansă) este un „breather”: zero obstacole, plus cristale în plus pe mai multe benzi. Sunt secundele în care jucătorul își recompune poziția, ochii se relaxează și apare senzația de zbor pur.

Mai facem un truc la pornirea fiecărei ture: funcția de „priming” umple coridorul vizibil cu rânduri înainte ca jucătorul să apese start, ca tura să nu înceapă într-un gol pustiu — dar cel mai apropiat rând e garantat un breather. Primele secunde sunt mereu calme. Nimeni nu moare în prima secundă în Planet Surfer.

Și inamicii respectă contractul

Navele inamice nu apar oriunde. Le generăm doar când cel puțin două benzi sunt libere, astfel încât să rămână întotdeauna măcar o bandă de pură evitare, fără combat. Bolturile lor roșii se mișcă lent (48 u/s, spre jucător) tocmai ca să poată fi evitate. Iar la fiecare ~1800 m apare un boss într-o arenă curată: oprim temporar generarea de rânduri noi, ca lupta cu boss-ul să fie un moment dedicat, nu un haos suprapus.

Scorul și senzația de „încă o tură”

Scorul curge din mai multe surse, acordate ca progresul să se simtă constant, dar abilitatea să fie răsplătită:

Peste asta am pus praguri de scor la 2000, 5000 și 10000. Sunt repere clare care dau fiecărei ture o țintă tangibilă chiar dacă recordul personal e mult mai mare — „mai am puțin până la 5000” e exact gândul care declanșează un restart. Pragurile deblochează recompense unice (monede virtuale din economia jocului; doar valută din joc, fără valoare reală), iar revendicarea e idempotentă, deci nu pot fi luate de două ori.

Senzația de corectitudine vine și din cum mor jucătorii: avem o coliziune „swept” (verificăm dacă obstacolul a traversat fereastra de coliziune între două cadre, ca nimic să nu treacă prin nava la viteză mare) și 1,3 secunde de invulnerabilitate după fiecare lovitură. Trei lovituri, nu una. Toate aceste decizii spun același lucru: dacă ai murit, ai fi putut evita. Iar asta e combustibilul pentru „încă o tură”.

Cum am acordat totul

Nu am nimerit aceste valori din prima. Procesul a fost: centralizăm absolut fiecare număr într-un singur fișier de constante, jucăm sute de ture, și ajustăm un singur parametru pe iterație. Distanța rampei a fost prea agresivă inițial (jocul devenea de neevitat); am crescut-o. Intervalul pe rând a fost cândva fix în spațiu, până am observat că viteza maximă era injustă; l-am mutat pe timp. Un test de fum automat pornește jocul în Chrome și verifică ciclul boot → joc → pauză → coliziune → restart, fără erori în consolă, ca o ajustare de echilibru să nu spargă altceva.

Concluzia de design: un endless runner „corect” nu e unul ușor. E unul în care aleatorul e îngrădit de garanții stricte — o cale de scăpare mereu prezentă, o fereastră de reacție constantă, momente de respiro ritmate — astfel încât singura variabilă rămasă să fie abilitatea jucătorului.

Vrei să vezi curba în acțiune? Joacă Planet Surfer și restul colecției Neon Arcade gratuit, direct în browser, la games.hashcoreai.com. Iar dacă ai nevoie de un joc, un prototip interactiv sau o aplicație construită cu aceeași grijă pentru detaliu, scrie-ne la contact@hashcoreai.com — ne plac problemele de design care par simple până le deschizi.

Citește mai departe