Codarea unui generator de secvențe personalizate pentru a realiza un Starscape

În articolul meu precedent, i-am explicat diferența dintre un generator de numere pseudorandom și un generator de secvențe și am examinat avantajele pe care le are un generator de secvențe asupra unui PRNG. În acest tutorial vom codifica un generator de secvențe destul de simplu. Acesta generează un șir de numere, manipulează și interpretează această secvență și apoi o folosește pentru a desena un star de mare foarte simplu.

Notă: Deși acest tutorial este scris folosind Java, ar trebui să puteți folosi aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocului.


Crearea și inițializarea imaginii

Primul lucru pe care trebuie să-l facem este să creezi imaginea. Pentru acest generator de secvențe, vom crea o imagine de 1000 × 1000 pixeli pentru a păstra generarea de numere cât mai simplă posibil. Diferitele limbi fac acest lucru diferit, deci folosiți codul necesar pentru platforma dvs. dev.

Când ați creat cu succes imaginea, este timpul să îi dați o culoare de fundal. De vreme ce vorbim despre un cer înstelat, ar fi mai sensibil să începem cu un negru (# 000000) și apoi adăugați stelele albe, mai degrabă decât invers.


Efectuarea unui profil Star și a unui câmp Star

Înainte de a începe să lucrăm la generatorul de secvențe, ar trebui să dați seama de unde doriți să mergeți cu el. Aceasta înseamnă că știi ce vrei să creezi și cum diferite semințe și numere variază ceea ce vrei să creezi - în acest caz stelele.

Pentru a face acest lucru, trebuie să creați un exemplu de profil stea care va conține variabile de clasă care indică unele dintre proprietățile stelelor. Pentru a păstra lucrurile simple, vom începe cu doar trei atribute:

  • x-coordonate
  • y-coordonate
  • mărimea

Fiecare dintre cele trei atribute va avea valori cuprinse între 0 și 999, ceea ce înseamnă că fiecare atribut va avea trei cifre alocate. Toate acestea vor fi stocate într-un Stea clasă.

Două metode importante în Stea clasa sunt getSize () și getRadiusPx (). getSize () metoda returnează dimensiunea stelei, redusă la un număr zecimal între zero și unu, și getRadiusPx () metoda returnează cât de mare ar trebui să fie raza stelei în imaginea finală.

Am descoperit că 4 pixeli fac o rază maximă bună în demo-ul meu, deci getRadiusPx () va reveni pur și simplu valoarea getSize () înmulțit cu patru. De exemplu, dacă getSize () metoda returnează o rază de 0,4, getRadiusPx () metoda ar da o rază de 1,6px.

 // Clasa privată int s_x, s_y, s_size; public Star (int x int, int dimensiune) // Constructor care stabilește atribute inițiale s_x = x; s_y = y; s_size = dimensiune;  public int getX () // Returnează coordonatul x al returului s_x;  public int getY () // Returnează coordonatul y al întoarcerii starului s_y;  public double getSize () // Returnează raza stelei ca număr zecimal între 0 și 1 retur (dublu) (s_size / 1000);  public dublu getRadiusPx () // Returnează raza stelei în pixeli retur (dublu) 4 * getSize (); // 4px este cea mai mare rază pe care o stea o poate avea

De asemenea, trebuie să facem o clasă foarte simplă, a cărei misiune este de a urmări toate stelele din fiecare secvență de stele. Starfield clasa constă doar din metode care adaugă, elimină sau recuperează stele dintr-un ArrayList. De asemenea, ar trebui să poată să returneze ArrayList.

 // Clasa Starfield privată ArrayList s_stars = ArrayList () nou; public void addStar (Star s) // O metoda care adauga o stea intr-un ArrayList s_stars.add (s);  public void removeStar (Star s) // O metodă care elimină o stea de pe un ArrayList s_stars.remove (s);  public Star getStar (int i) // O metodă care preia o stea cu index i dintr-o întoarcere ArrayList (Star) getStarfield () get (i);  public ArrayList getStarfield () // O metodă care returnează ArrayList stocând toate stelele returnează s_stars; 

Planificarea generatorului de secvențe

Acum că am terminat profilul stea și am inițializat imaginea, știm câteva puncte importante despre generatorul de secvențe pe care dorim să-l creăm.

În primul rând, știm că lățimea și înălțimea imaginii sunt de 1000 pixeli. Aceasta înseamnă că, pentru a exploata resursele la îndemână, intervalul coordonatelor x și y trebuie să se încadreze în intervalul 0-999. Din moment ce două dintre numerele cerute se încadrează în același interval, putem aplica aceeași dimensiune dimensiunii stelei pentru a păstra uniformitatea. Marimea va fi scazuta ulterior cand vom interpreta seriile de numere.

Vom folosi o serie de variabile de clasă. Acestea includ: s_seed, un întreg întreg care definește întreaga secvență; s_start și trimite, două numere întregi care sunt generate prin împărțirea semințelor în două; și s_current, un număr întreg care conține cel mai recent generat număr din secvență.


Vedeți această imagine din articolul meu precedent. 1234 este sămânța, iar 12 și 34 sunt valorile inițiale ale lui s_start și trimite. Bacsis: Rețineți că fiecare număr generat provine din semințe; nu mai este niciun apel întâmplător(). Aceasta înseamnă că aceeași sămânță va genera întotdeauna același star-stele.

Vom folosi de asemenea s_sequence, A Şir care va păstra secvența globală. Ultimele două variabile de clasă sunt s_image (de tipul Imagine - o clasă pe care o vom crea ulterior) și s_starfield (de tipul Starfield, clasa pe care tocmai am creat-o). Primul stochează imaginea, în timp ce cel de-al doilea conține câmpul cu stea.

Traseul pe care îl vom lua pentru a crea acest generator este destul de simplu. În primul rând, trebuie să facem un constructor care să accepte o sămânță. Când se face acest lucru, trebuie să creați o metodă care acceptă un număr întreg reprezentând numărul de stele pe care trebuie să le creeze. Această metodă trebuie să apeleze apoi la generatorul real pentru a veni cu numerele. Și începe acum lucrarea reală ... creând generatorul de secvențe.


Codificarea generatorului de secvențe

Primul lucru pe care un generator de secvențe trebuie să îl facă este să accepte o sămânță. Așa cum am menționat, vom împărți semințele în două: primele două cifre și ultimele două cifre. Din acest motiv, trebuie să verificăm dacă sămânța are patru cifre și să o introduceți cu zerouri dacă nu. Când se face acest lucru, putem împărți șirul de semințe în două variabile: s_start și trimite. (Rețineți că semințele în sine nu vor face parte din secvența reală.)

 // StarfieldSequence class public StarfieldSequence (int seed) // Un constructor care acceptă o sămânță și o împarte în două String s_seedTemp; s_starfield = noul Starfield (); // Initializeaza Starfield s_seed = seed; // Păstrați semințele într-un șir astfel încât să le putem împărți cu ușurință // Adăugați nuli la șir dacă semințele nu au patru cifre dacă (semințe < 10) s_seedTemp = "000"; s_seedTemp = s_seedTemp.concat(Integer.toString(seed));  else if (seed < 100) s_seedTemp = "00"; s_seedTemp = s_seedTemp.concat(Integer.toString(seed));  else if (seed < 1000) s_seedTemp = "0"; s_seedTemp = s_seedTemp.concat(Integer.toString(seed));  else  s_seedTemp = Integer.toString(seed);  //Split the seed into two - the first two digits are stored in s_start, while the last two are stored in s_end s_start = Integer.parseInt(s_seedTemp.substring(0, 2)); s_end = Integer.parseInt(s_seedTemp.substring(2, 4)); 

Asa de:

  • semințe = 1234 mijloace s_start = 12 și s_end = 34
  • semințe = 7 mijloace s_start = 00 și s_end = 07
  • semințe = 303 mijloace s_start = 03 și s_end = 03

Următorul rând: creați o altă metodă care, având în vedere cele două numere, generează următorul număr din secvență.

Găsirea formulei potrivite este un proces obosit. De obicei, înseamnă ore de încercare și de eroare în încercarea de a găsi o secvență care nu implică prea multe modele în imaginea rezultată. Prin urmare, ar fi mai înțelept să găsim cea mai bună formulă odată ce putem vedea imaginea, mai degrabă decât acum. Ceea ce ne interesează acum este găsirea unei formule care generează o secvență care este mai mult sau mai puțin aleatoare. Din acest motiv, vom folosi aceeași formulă folosită în secvența Fibonacci: adăugarea celor două numere.

 // Clasa StarfieldSequence private int getNext () // O metoda care returneaza urmatorul numar in returul secventa (s_start + s_end); 

Când se face acest lucru, putem trece acum și începem să creăm secvența. Într-o altă metodă, vom manipula semințele inițiale pentru a genera un întreg flux de numere, care pot fi apoi interpretate ca atributele profilului stelei.

Știm că pentru o anumită stea avem nevoie de nouă cifre: primele trei definesc coordonatele x, iar cele trei de mijloc definesc coordonatele y, iar ultimele trei definesc dimensiunea. Prin urmare, ca și în cazul hrănirii semințelor, pentru a păstra uniformitatea pe tot parcursul procesului este important să vă asigurați că fiecare număr generat are trei cifre. În acest caz, trebuie să trunchăm și numărul dacă este mai mare de 999.

Acest lucru este destul de similar cu ceea ce am făcut înainte. Trebuie doar să stocăm numărul într-un string String temporar, temp, apoi aruncați prima cifră. În cazul în care numărul nu are trei cifre, ar trebui să îl adăugăm cu zerouri, așa cum am făcut mai devreme.

 // Clasa StarfieldSequence private void fixDigits () String temp = ""; // Dacă numărul nou generat are mai mult de trei cifre, luați ultimele trei dacă (s_current> 999) temp = Integer.toString (s_current); s_current = Integer.parseInt (temp.substring (1, 4));  // Dacă numărul nou generat are mai puțin de trei cifre, adăugați zerouri la început dacă (s_current < 10) s_sequence += "00";  else if (s_current < 100) s_sequence += "0";  

Cu acest lucru înfășurat, ar trebui să facem acum o altă metodă care creează și returnează un profil stea de fiecare dată când generăm trei numere. Folosind această metodă, putem adăuga apoi steaua la ArrayList de stele.

 / / StarfieldSequence clasa privata Star getStar (int i) // O metoda care accepta un intreg (marimea secventei) si returneaza steaua // Separa ultimele noua cifre din secventa in trei (cele trei atribute ale stelei ) Star Star = noua stea (Integer.parseInt (s_sequence.substring (i-9, i-6)), Integer.parseInt (s_sequence.substring (i-6, i-3)), Integer.parseInt (s_sequence.substring (i-3, i))); star retur; 

Punându-le pe toți împreună

După ce se termină, putem asambla generatorul. Ar trebui să accepte numărul de stele pe care trebuie să le genereze.

Știm deja că pentru o stea, avem nevoie de nouă cifre, deci acest generator trebuie să țină cont de numărul de caractere din șiruri. Contorul, s_counter, va stoca lungimea maximă a secvenței. Prin urmare, înmulțim numărul de stele cu nouă și eliminăm unul de la a Şir pornește de la index zero.

De asemenea, trebuie să ținem cont de numărul de caractere pe care le-am creat de când am generat ultima o stea. Pentru această sarcină, vom folosi s_starcounter. În a pentru buclă, care se va repeta până când lungimea seriei va fi egală s_counter, acum putem numi metodele pe care le-am creat până acum.

Bacsis: Nu trebuie să uităm să o înlocuim s_start și trimite, sau altfel vom continua să generăm același număr din nou și din nou!
 // clasa StarfieldSequence public void genera (int starnumber) // genereaza un numar de stele asa cum este indicat de intregul starnumber int s_counter = 9 * starnumber; // s_counter urmărește numărul de caractere pe care String-ul trebuie să le genereze pentru a genera numărul de stele desemnate s_counter - = 1; // Eliminați unul de când un String pornește de la indexul 0 int s_starcounter = 0; // s_starcounter ține evidența numărului de numere generate pentru (int i = 1; s_sequence.length () <= s_counter; i++) s_current = getNext(); //Generate the next number in the sequence fixDigits(); //Make sure the number has three digits s_sequence += s_current; //Add the new number to the sequence s_starcounter++; if (s_starcounter >= 3 && s_starcounter% 3 == 0) // Dacă au fost generate trei numere de la crearea ultimei stele, creați un alt s_starfield.addStar (getStar (s_sequence.length ());  / / Înlocuiți s_start și s_end, altfel veți continua să generați același număr din nou și din nou! s_start = s_end; s_end = s_current; 

Desenarea stelelor

Acum că partea dificilă sa terminat, este în cele din urmă timpul să trecem la Imagine clasă și începe să desenezi stele.

Într-o metodă care acceptă a Starfield, mai întâi creează o instanță a Culoare, apoi recuperați numărul de stele pe care trebuie să le desenezi. În a pentru bucla, vom atrage toate stelele. După ce ați făcut o copie a starului curent, este important să regăsim raza stelei. Dat fiind faptul că numărul de pixeli este un număr întreg, ar trebui să adăugăm raza pentru a face ca aceasta să fie un număr întreg.

Pentru a desena steaua, vom folosi un gradient radial.

Un exemplu de gradient radial

Opacitatea unui gradient radial depinde de distanța unui pixel din centru. Centrul cercului va avea coordonate (0,0). Folosind convenția cea mai comună, orice pixel din stânga centrului are o coordonată x negativă, iar orice pixel de mai jos are o coordonată y negativă.

Din cauza asta, imbricate pentru buclele încep cu un număr negativ. Folosind teorema lui Pythagoras, calculăm distanța de la centrul cercului și îl folosim pentru a recupera opacitatea. Pentru stele care au cea mai mică rază posibilă (1px), opacitatea lor depinde doar de mărimea lor.

 // Clasa de imagine void draw public (Starfield fieldfield) Culoare culoare; pentru (int i = 0; i < starfield.getStarfield().size(); i++) //Repeat for every star Star s = starfield.getStar(i); int f = (int) Math.ceil(s.getRadiusPx()); //We need an integer, so we ceil the star's radius for (int x = -1*f; x <= f; x++) for (int y = -1*f; y <= f; y++) //Calculate the distance of the current pixel from the star's center double d = Math.abs(Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); if (d < s.getRadiusPx()) //Only draw pixel if it falls within radius if (f == 1) //If the star's radius is just one, the opacity depends on the star's size color = new Color(0.85f, 0.95f, 1, (float) s.getSize());  else  //The opacity here depends on the distance of the pixel from the center color = new Color(0.85f, 0.95f, 1, (float) ((s.getRadiusPx() - d)/s.getRadiusPx()));  graphics.setColor(color); //Assign a color for the next pixel graphics.fillRect(s.getX()+x, s.getY()+y, 1, 1); //Fill the pixel     

Pentru a încheia lucrurile, trebuie să facem o metodă care acceptă a Şir și o folosește pentru a salva imaginea cu acel nume de fișier. În generatorul, ar trebui să creăm prima imagine. Apoi, ar trebui să sunăm ultimele două metode din generatorul de secvențe.

 // clasa StarfieldSequence public void genera (int starnumber) s_image.createImage (); // Creați imaginea int s_counter = 9 * starnumber; s_counter - = 1; int s_starcounter = 0; pentru (int i = 1; s_sequence.length () <= s_counter; i++) s_current = getNext(); fixDigits(); s_sequence += s_current; s_starcounter++; if (s_starcounter >= 3 && s_starcounter% 3 == 0) s_starfield.addStar (getStar (s_sequence.length ()));  s_start = s_end; s_end = s_current;  s_image.draw (s_starfield); // Desenați câmpul cu stea s_image.save ("starfield"); // Salvați imaginea cu numele "starfield"

În Principal ar trebui să creăm o instanță a generatorului de secvențe, să îi atribuim o sămânță și să obținem un număr bun de stele (400 ar trebui să fie suficiente). Încercați să rulați programul, să remediați eventualele erori și să verificați calea de destinație pentru a vedea ce imagine a fost creată.

Imaginea rezultată cu o sămânță de 1234

îmbunătăţiri

Există încă unele schimbări pe care le putem face. De exemplu, primul lucru pe care l-ați observa este că stelele sunt grupate în centru. Pentru a rezolva asta, va trebui să vină cu o formulă bună care elimină orice tipar. Alternativ, puteți crea un număr de formule și alternați între ele folosind un contor. Formulele pe care le-am folosit au fost următoarele:

 // clasa StarfieldSequence private int getNext () if (count == 0) if (s_start> 0 && s_end> 0) numarul ++; retur (int) (Math.pow (s_start * s_end, 2) / (Math.pow (s_start, 1) + s_end) + Math.round (Math.abs (Math.cos (0.0175f * s_end)));  altceva count ++; retur (int) (Math.pow ((s_end + s_start), 4) / Math.pow ((s_end + s_start), 2) + Math.round (Math.abs (0.0175f * s_end) Math.cos (s_end) + Math.cos (s_start));  altceva if (s_start> 0 && s_end> 0) count--; retur (int) (Math.pow ((s_end + s_start), 2) + Math.round (Math.abs (Math.cos (0.0175f * s_end))));  altceva count--; retur (int) (Math.pow ((s_end + s_start), 2) + Math.round (Math.abs (Math.cos (0.0175f * s_end))) + Math.cos (s_end) )); 

Există încă o simplă îmbunătățire pe care o putem implementa. Dacă priviți în sus la cer, veți vedea câteva stele mari și multe altele mai mici. Cu toate acestea, în cazul nostru numărul de stele mici este aproximativ același cu numărul de stele mari. Pentru a rezolva acest lucru, trebuie să ne întoarcem la getSize () metodă în Stea clasă. După ce dimensiunea este o fracțiune de unu, trebuie să ridicăm acest număr la puterea unui întreg - de exemplu, patru sau cinci.

 // Clasa publica dubla getSize () retur (dublu) (Math.pow ((dublu) s_size / 1000, 4)); 

Rularea programului pentru ultima oară ar trebui să vă ofere un rezultat satisfăcător.

Rezultatul final - o vedetă de vederi întregi generată procedural de generatorul nostru de secvențe!

Concluzie

În acest caz, am folosit un generator de secvențe pentru a genera un fundal procedural. Un generator de secvențe ca acesta ar putea avea mult mai multe utilizări - de exemplu, ar putea fi adăugată o coordonată z în stea, astfel încât, în loc să deseneze o imagine, ar putea genera stele ca obiecte într-un mediu 3D.