La 5 luni după ce am început primul meu joc, mi-am dat seama că va trebui să refac jumătate din el. Am învățat mai multe despre arhitectura software în acea perioadă decât în toți anii mei anteriori de facultate.
Mulți oameni din IT se joacă jocuri video ca hobby, și mulți dintre ei s-ar fi gândit chiar să folosească abilitățile lor de programare pentru a-și crea jocul visurilor lor, poate chiar să-l și lanseze public, dar puțini chiar fac asta. Chiar e atât de greu să faci un joc?
Să zicem că avem deja o idee pentru un joc video și am făcut deja un document de design al jocului. Ca să oferim câteva idei despre ce fel de joc vrem să facem, ne dorim ceva cu multe date, cu multiple sisteme care depind de date diferite și cu interacțiuni intensive între sisteme. În acest scop, jocurile din genurile roguelike/roguelite și Real-Time Strategy (RTS) sunt destul de bune, asta dacă nu vrem să gândim un joc și mecanici întregi de la zero. Dacă vrem mai multă "libertate", ar trebui să facem un Role-Playing Game (RPG). În final, putem crea orice vrem, în orice gen, în orice fel vrem, deoarece vom învăța din toate, dar acestea sunt cel mai probabil cele mai eficiente pentru scopul învățării.
Următorul pas este construirea de Software Requirements Specification (SRS). Prima întrebare la care ar trebui să răspundem este ce motor de joc vom folosi și de ce îl alegem. Deși acesta este un subiect foarte dezbătut, pentru scopul acestui articol, care este cel de învățare, sugerez Godot și GDScript. Unii ar putea să se gândească la Unity sau Unreal Engine 5 (UE5), pentru că sunt software-uri mai mature, dar exact asta le dezavantajează. Au atât de multe funcții încorporate încât sunt mai greu de navigat și, în loc să folosim timpul pentru dezvoltare și cercetare, îl petrecem căutând prin documentații scrise prost și învechite. Godot este mult mai modern, fiind un motor de jocuri gratuit și open-source (FOSS). Primește actualizări regulate și are o documentație bine scrisă, adusă la zi. Îmi place din prea multe motive ca să le listez aici, dar unul dintre principalele motive pentru care îl recomand este că are câteva funcționalități lipsă. Deoarece scopul principal al acestui articol este să învățăm, voi menționa că micile funcții lipsă ne dau ocazia să le creăm noi înșine, pentru proiect și nevoile lui, ca să înțelegem cu adevărat cum funcționează în culise. Pentru sisteme mai complexe, asta ne obligă să fim creativi cu soluțiile noastre.
Am putea crede că avem suficientă experiență pentru a nu fi deranjați să scriem un SRS sau să facem un document detaliat de dezvoltare a jocului și, în funcție de complexitatea și noutatea jocului pe care îl facem, această presupunere ar putea fi corectă. Chiar și așa, fără experiență anterioară în realizarea jocurilor, este foarte probabil să cădem în unele capcane comune. Cea mai comună capcană este să implementăm un joc ca pe un software normal. Spre deosebire de software-ul normal, la un moment dat, ne vom dori să schimbăm aproape fiecare lucru pe care l-am implementat, situațiile rare fiind cele în care vom spune "Acest lucru nu se va schimba niciodată pentru acest joc, va fi întotdeauna adevărat" sau "Voi folosi/voi avea nevoie întotdeauna de acest lucru așa / să se comporte astfel". Ca exemplu, am creat un sistem de hărți generate aleatoriu pentru genul roguelike menționat anterior. Multe roguelike-uri sunt construite pe matrice de dimensiune MxM, în loc de matrice de dimensiune MxN. Astfel de simplificări ne permit de obicei să generalizăm codul, dar dacă am decide mai târziu că vrem libertatea și caracterul aleatoriu al unui MxN, ar trebui să refactorizăm mult cod și să verificăm din nou multe dintre cazurile limită pe care un utilizator le-ar putea întâlni. Aceasta este natura generării aleatorii, ea încetinind procesul de dezvoltare. Și exact asta mi s-a întâmplat mie.
Când ne gândim la dezvoltarea de jocuri, majoritatea oamenilor se gândesc la gameplay și grafică, nu la meniuri, setări și framework-uri de testare, dar acestea sunt cu adevărat importante. Dacă vrem să creăm mai multe jocuri în viitor, este obligatoriu să stăpânim aceste sisteme. Fă-o o dată și fă-o bine! Crearea sistemului de setări mai întâi permite o personalizare ușoară pe termen lung, fără a pierde timp urmărind toate cazurile în care ar trebui să înlocuim codul ca să ne asigurăm că setările sunt citite corect. Aceasta este o capcană comună în care cad chiar și dezvoltatorii experimentați, deoarece există multe jocuri cu patch notes care includ setări care nu sunt citite corect. Pentru modelul de date al "setărilor", trebuie avut în vedere că acesta va trebui să ajungă într-un meniu, deci ne vom asigura că avem multiple modalități de a-l afișa, în funcție de tipul setării, de numărul de opțiuni etc. Deoarece am început să vorbim despre meniuri, stabilirea UI/UX-ului pentru joc va fi importantă și în etapele ulterioare, cum ar fi dezvoltarea unui Heads-Up Display (HUD) sau a altor sisteme de gameplay care au nevoie de un UI pentru interacțiune. Setarea timpurie a așteptărilor privind UI/UX asigură, de asemenea, o abordare consecventă pe parcursul dezvoltării. În cele din urmă, un framework de testare este o bună practică și, dacă nu vrem să petrecem timp căutând ceva creat de alții pe GitHub și să-l învățăm, trebuie să-l facem noi înșine. Indiferent dacă cineva vrea să creeze ceva nou fără să cerceteze subiectul înainte, sau să copieze framework-ul lor preferat de testare, toate abordările au avantajele lor.
Să definim ce presupun legarea și integrarea în contextul creării de sisteme:
singular - un scop foarte specific,
modular - ușor de extins, de integrat și de folosit în alte proiecte,
Cineva ar putea crede că "singular" și "general" se contrazic, așa că haideți să discutăm un exemplu concret. Gândește-te la un generator de inamici. Are un scop foarte specific, dar dacă putem genera doar un singur tip de inamic, nu este un sistem grozav (sau general). Totuși, ar trebui să privim tot ce scriem prin acest tip de lentilă, fiind, totodată, conștienți că aproape fiecare regulă are excepțiile ei. Vor exista jocuri în care avem nevoie de ceva foarte specific, care probabil nu va mai fi folosit niciodată, iar identificare acestor excepții îți poate economisi mult timp. Trebuie să fim atenți să ne gândim la timpul și efortul necesare, respectiv dacă merită cu adevărat să creăm un sistem general, reutilizabil, în loc de unul specific proiectului. Modul în care a fost descris acest aspect, într-un videoclip intitulat "Best Code Architecture For Indie Games" în jurul minutului 3 (totuși, recomand să vizionați tot videoclipul, este grozav), creat de dezvoltatorul expert în jocuri Jonas Tyroller (@JonasTyroller pe YouTube), este că ar trebui ca 70% din cod să fie reutilizabil sub forma unui simplu folder de tip drop-in din jocuri anterioare, iar restul de 30% ar trebui să fie cod specific, de integrare. Iată o propunere ușor diferită: 60% ar trebui să fie sisteme reutilizabile, 10% ar trebui să fie sisteme noi și 30% integrare. Motivul pentru care este nevoie de 10% de sisteme noi este că, chiar și într-o fază mai târzie a dezvoltării jocului, ne va forța fie să examinăm codul și să găsim sisteme reutilizabile posibile, fie să venim cu idei noi pentru jocul la care lucrăm sau pentru unul nou.
Refactorizările majore sunt ceea ce sfaturile din acest articol încearcă să prevină, dar, chiar și așa, tot ne vom lovi de situații în care refactorizările sunt necesare. Aceasta este o oportunitate excelentă de a lua o pauză și de a analiza ce s-ar fi putut face pentru a evita o astfel de situație, și să vedem ce a funcționat și ce nu. Poate că exista o modalitate mai bună de a implementa ceva încă de la început.
Merită investigate următoarele concepte utile: delta time, interpolare, _physics_process versus _process in Godot (majoritatea motoarelor de jocuri au acest concept, doar cu nume diferite), shader-e. De asemenea, un alt video excelent pentru a face dezvoltarea de jocuri mai ușoară este "Gyms, Zoos, Museums: Your documentation should be in-game" de Robin-Yann Storm (@rystorm- pe YouTube).
În era LLM-urile și a AI-ului, tranziționăm de la a fi doar dezvoltatori de software care descifrează tichetele Jira, care înțeleg proiectul atât din punct de vedere business, cât și tehnic, care creează și refactorizează arhitecturi, care revizuiesc și scriu cod curat, la tot ce am discutat mai devreme, dar amplificat, scriind mai puțin cod manual. Momentan, LLM-urile nu sunt cele mai bune la arhitectura de proiect. Un motiv important pentru acest lucru este că, pentru multe proiecte, LLM-urile pur și simplu nu au suficient context și înțelegere a business-ului. Poate că lipsește documentația, poate că documentația este împrăștiată și/sau învechită. Să presupunem că reușim să actualizăm documentația în întregime și că o punem într-un singur loc la care AI-ul poate avea acces. Câteva întrebări de luat în considerare sunt: Cât de mult vrem cu adevărat să documentăm și cât vrem să dăm în final LLM-ului? Dacă i-am oferi doar date tehnice LLM-ului, acesta nu ar reuși să creeze arhitectură bună, deoarece nu ar ști nimic legat de companie sau de proiect, aspecte care să îi permit să ia decizii corecte. Dacă i-am oferi documentația create de noi, ne punem întrebarea dacă o corporație dorește ca întreaga sa documentație tehnică și de business să ajungă la o entitate terță. O alternativă este realizarea unui AI in-house, dar acest lucru este foarte scump, necesită întreținere permanentă și ar presupune să folosim modele open source. Sau am putea să ne cream modelele noastre, ceea ce este foarte scump și foarte riscant. Am putea considera că nu merită efortul, costurile și timpul investit.
Există 3 moduri principale de a folosi LLM-urile eficient, pentru a crea arhitectura unui proiect software:
prompt simplu, de genul "Generează arhitectura proiectului pentru …(descrierea generală a proiectului și a business-ului)…" - potrivit ca punct de plecare pentru proiecte mai simple
creează documentația de business și o descriere detaliată a proiectului, și apoi lasă agenții să parcurgă fișierele cu un prompt mai specific - potrivit și pentru proiecte de dimensiuni medii, dar există riscul de business menționat mai sus
Legat de punctul 3, dezvoltarea jocurilor ajută la acumularea de experiență și abilități. Când dezvoltăm jocuri, arhitectura este foarte importantă, pentru că gestionăm o mulțime de sisteme, obiecte, fișiere și resurse, iar performanța este o provocare permanentă. Prin urmare, dacă arhitectura e deficitară, acest lucru devine evident destul de repede. Va fi nevoie să refactorizăm, să reconsiderăm și să analizăm din nou ce nu a funcționat, dar și cum am fi putut să proiectăm soluția mai bine de la început, ceea ce oferă un antrenament grozav.
De la Vibe Coding la Production Engineering
Marți, 30 iunie, ora 18:00
Cognizant (Timișoara)
Facebook Meetup StreamEvent YouTubede Alex Popescu
de Luiza Mihu
de Cosmin Sandu
de Andreea Roșu