Neon Arcade este colecția noastră de jocuri gratuite în browser, găzduită la games.hashcoreai.com. În momentul scrierii acestui devlog conține trei jocuri 3D originale, toate construite în casă la HashCoreAI:
- Tidefall — un platformer într-o mlaștină bioluminescentă, combinat cu un colony builder: ataci, construiești, aperi.
- Planet Surfer — un endless runner spațial neon: navighezi prin cosmos, eviți obstacolele, supraviețuiești.
- Barrel Brigade — un shooter arcade pe trei benzi: conduci echipa, spargi butoaiele.
Toate trei rulează în orice browser modern și, în același timp, în aplicațiile native pentru iOS și Android — din exact același cod. Acest articol explică deciziile de arhitectură care fac posibil acest lucru, inclusiv compromisurile pe care nu le-am evitat.
Stack-ul: Three.js + Vite + TypeScript
Fiecare joc este o aplicație web independentă: Three.js pentru randarea 3D pe WebGL, Vite pentru bundling și dev server, TypeScript pentru tot codul de joc. Hub-ul arcade — grila de carduri pe care alegi un joc — este o a patra aplicație Vite + TypeScript, separată de jocuri.
Le ținem deliberat separate. Fiecare joc are propriul package.json, propriul vite.config.ts și propriul build. Asta înseamnă că un bug de dependență din Tidefall nu poate dărâma Planet Surfer, iar fiecare joc poate evolua în ritmul lui. La build, un script de assembly (scripts/assemble.mjs) copiază hub-ul construit în rădăcină și fiecare joc construit sub propriul sub-path, producând un singur folder public/ pe care îl deployăm pe Firebase Hosting.
| Componentă | Servită la | Build |
|---|---|---|
| Hub arcade | games.hashcoreai.com/ | Vite + TS + Capacitor |
| Planet Surfer | /planet-surfer/ | Three.js + Vite |
| Barrel Brigade | /barrel-brigade/ | Three.js + Vite |
| Tidefall | /tidefall/ | Three.js + Vite |
Un detaliu mic dar important: fiecare joc se construiește cu base: './' în Vite. Asta face ca toate path-urile către asset-uri să fie relative, deci același build rulează corect la /tidefall/ pe hub, dar și dintr-un wrapper file:// sau dintr-un sub-path complet diferit. Nu trebuie să recompilăm un joc doar pentru că s-a schimbat originea care îl servește.
Decizia cheie: în aplicația nativă, jocurile se încarcă din web-ul live
Aceasta este decizia de arhitectură care definește întregul proiect. Aplicațiile native sunt construite cu Capacitor, care împachetează un build web într-un shell nativ iOS/Android cu o punte (bridge) către API-uri native.
Abordarea evidentă ar fi să împachetezi toate jocurile în interiorul aplicației. Noi am ales contrariul. Doar hub-ul este împachetat în aplicație; jocurile se încarcă din originea web live, https://games.hashcoreai.com/<id>/, chiar și înăuntrul aplicației native. În capacitor.config.ts permitem navigarea către acel domeniu, iar codul nostru de platformă decide URL-ul:
Pe web folosim path-uri relative same-origin. În aplicația nativă, jocurile se încarcă din aceeași origine web live. Câștigul: a adăuga un joc = deploy pe Firebase și apare instant în aplicație. Fără bundling, fără update în magazin, niciodată.
Concret, fluxul de adăugare a unui joc nou arată așa:
- Pui jocul în
games/<id>/și îl adaugi în scriptul de assembly. - Îl adaugi în lista de jocuri (
games.json, servit de pe domeniul web). - Rulezi
npm run deploy.
Atât. Web-ul și aplicațiile native deja instalate pe telefoanele oamenilor îl preiau — fără update în App Store sau Google Play. Hub-ul nu are jocurile în cod la momentul build-ului: la pornire face fetch la lista live de jocuri și o afișează. Lista împachetată în aplicație este doar un fallback offline.
De ce o singură origine web partajată
O singură origine ne dă trei lucruri gratis. Un singur cont Firebase și o singură sesiune de autentificare funcționează în toate jocurile, fiindcă rulează pe același domeniu. Un singur portofel de tokenuri este consistent peste tot. Și o singură conductă de deploy livrează simultan pe toate platformele. Costul de mentenanță al unui joc nou se apropie de zero din punct de vedere al distribuției.
Compromisul WebView vs. nativ
Această abordare nu este gratuită. Iată ce am cântărit, sincer:
| Aspect | Jocuri în web live (alegerea noastră) | Jocuri împachetate nativ |
|---|---|---|
| Joc nou | Deploy web, apare instant | Update în magazin (zile de review) |
| Offline | Necesită conexiune (mitigat prin cache) | Funcționează offline |
| Performanță | WebGL în WebView | WebGL în WebView (identic) |
| Mărime aplicație | Mică (doar shell-ul) | Crește cu fiecare joc |
| Risc de respingere în magazin | Necesită valoare nativă reală | Mai puțin risc |
Cel mai important compromis: o aplicație care e „doar un site împachetat” riscă respingerea pe motiv de „minimum functionality”, mai ales pe App Store. De aceea am adăugat valoare nativă reală în shell, activată doar pe native și încărcată dinamic ca să nu afecteze build-ul web: bara de stare colorată în tema neon, butonul hardware de back pe Android care merge prin istoricul in-app (nu lasă utilizatorul într-un dead-end), blocarea orientării per joc (Tidefall pornește mereu landscape, restul portrait), feedback haptic la atingeri și o notificare locală de re-engagement programată local, fără server.
Performanța pe telefoane Android ieftine
WebGL prin WebView rulează la fel ca un joc web obișnuit — nu primești magie nativă gratis. Pe dispozitive de gamă joasă, GPU-ul și lățimea de bandă a memoriei sunt constrângerea reală, nu JavaScript-ul. Lecțiile noastre practice:
- Buget de draw calls, nu doar de poligoane. Pe GPU-uri mobile ieftine, numărul de draw calls și schimbările de stare dor mai mult decât numărul brut de triunghiuri. Comasăm geometriile și instanțiem obiectele repetate.
- Țintă es2022 la build. Compilăm pentru un baseline modern, ca să nu plătim transpilare inutilă, păstrând în același timp compatibilitatea cu WebView-urile actuale.
- Particule și efecte neon ieftine. Estetica neon vine din shadere și bloom controlat, nu din geometrie densă. Arată scump, costă puțin.
- Degradare elegantă. Mai bine un framerate stabil cu efecte reduse decât stuttering la efecte maxime.
Strategia de încărcare și de asset-uri
Pentru că jocurile se încarcă din rețea, cache-ul HTTP este critic. Configurăm header-ele pe Firebase Hosting astfel:
- HTML-ul se servește cu
no-cache, deci un deploy nou se vede imediat. - Fișierele cu hash din
assets/și modelele.glb, plus fonturilewoff/woff2, primescmax-age=31536000, immutable— un an, cacheabile pentru totdeauna.
Combinația dă ce ne dorim: actualizările apar instant, dar asset-urile grele 3D se descarcă o singură dată și apoi vin din cache. Hub-ul deține și PWA-ul (service worker-ul); jocurile nu înregistrează service workere proprii, ca să nu se lupte pe scope. La nivel de cod, SDK-urile grele (RevenueCat, de exemplu) sunt importate dinamic abia când sunt necesare, ca să țină pornirea ușoară.
Un cont, un portofel — server-authoritative
Un singur proiect Firebase deservește tot. Utilizatorii se autentifică o singură dată cu Google, Apple sau email și au același cont pe web, Android și iOS. Pe native, signInWithPopup nu funcționează în WebView, așa că rulăm fluxul nativ de provider și apoi semnăm credențialul în SDK-ul Firebase JS — astfel întreaga aplicație împarte o singură sesiune de autentificare.
Peste cont stă un portofel de tokenuri partajat între jocuri, stocat în Firestore. Punctul cheie de design este că este server-authoritative: regulile noastre Firestore permit clientului doar să citească propriul portofel; orice scriere a soldului este interzisă din client și se face exclusiv din Cloud Functions cu Admin SDK. Clientul se abonează la soldul live și reflectă schimbările în UI, dar nu poate inventa tokenuri.
Notă: tokenurile sunt valută pur virtuală în jocuri. Nu sunt bani reali, nu pot fi „câștigați” ca venit și nu există nicio componentă de investiție — sunt doar design de joc, ca monedele din orice joc clasic.
Ce am învăța
Separarea jocurilor de shell și încărcarea lor din web-ul live a fost decizia care a plătit cel mai mult. Eliberează echipa de ciclul de review din magazine pentru orice e conținut, păstrând în același timp un strat nativ subțire pentru lucrurile care chiar au nevoie de el. Compromisul — dependența de rețea — îl gestionăm cu o politică de cache disciplinată. Pentru un studio mic care publică multe jocuri mici, este exact echilibrul corect.
Vrei să le încerci? Toate jocurile sunt gratuite la games.hashcoreai.com. Iar dacă plănuiești o aplicație web sau mobilă care trebuie să livreze rapid pe mai multe platforme dintr-un singur codebase, scrie-ne la contact@hashcoreai.com — exact genul de arhitectură pe care o construim pentru clienți.