I min forrige artikkel forklarte jeg forskjellen mellom en pseudorandom-tall generator og en sekvensgenerator, og undersøkte fordelene en sekvensgenerator har over en PRNG. I denne opplæringen vil vi kode en ganske enkel sekvensgenerator. Den genererer en rekke tall, manipulerer og tolker denne sekvensen, og bruker den til å tegne en veldig enkel starscape.
Merk: Selv om denne opplæringen er skrevet ved hjelp av Java, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.
Det første vi må gjøre er å lage bildet. For denne sekvensgeneratoren skal vi lage et 1000 × 1000 px bilde for å holde nummergenerering så enkelt som mulig. Ulike språk gjør dette annerledes, så bruk den nødvendige koden for din dev-plattform.
Når du har opprettet bildet, er det på tide å gi det en bakgrunnsfarve. Siden vi snakker om en stjerneklar himmel, ville det være mer fornuftig å starte med en svart (# 000000) bakgrunn og legg deretter til de hvite stjernene, snarere enn omvendt.
Før vi begynner å jobbe med sekvensgeneratoren, bør du finne ut hvor du vil ha med den. Dette betyr å vite hva du vil opprette, og hvordan forskjellige frø og tall varierer fra det du vil lage - i dette tilfellet stjernene.
For å gjøre dette må vi opprette en sample stjerneprofil som vil inneholde klassevariabler som indikerer noen av stjernens egenskaper. For å holde ting enkelt, skal vi begynne med bare tre attributter:
Hver av de tre attributter vil ha verdier fra 0 til 999, noe som betyr at hver attributt vil ha tre siffer allokert til den. Alt dette vil bli lagret i en Stjerne
klasse.
To viktige metoder i Stjerne
klassen er getSize ()
og getRadiusPx ()
. De getSize ()
Metoden returnerer stjernens størrelse, skaleres ned til et desimalnummer mellom null og en, og getRadiusPx ()
Metoden returnerer hvor stor stjernens radius skal være i det endelige bildet.
Jeg har funnet ut at 4 piksler gir en god maksimal radius i min demo, så getRadiusPx ()
vil ganske enkelt returnere verdien av getSize ()
multiplisert med fire. For eksempel, hvis getSize ()
Metoden returnerer en radius på 0,4, getRadiusPx ()
Metoden ville gi en radius på 1,6px.
// Star class private int s_x, s_y, s_size; offentlig stjerne (int x, int y, int størrelse) // konstruktor som setter innledende attributter s_x = x; s_y = y; s_size = size; offentlig int getX () // Returnerer x-koordinaten til stjernens retur s_x; offentlig int getY () // Returnerer y-koordinaten til stjernen retur s_y; offentlig dobbelt getSize () // Returnerer stjernens radius som desimaltall mellom 0 og 1 retur (dobbelt) (s_size / 1000); offentlig dobbelt getRadiusPx () // Returnerer stjernens radius i piksler retur (dobbelt) 4 * getSize (); // 4px er den største radiusen en stjerne kan ha
Vi bør også lage en veldig enkel klasse hvis jobb er å holde styr på alle stjernene i hver sekvens av stjerner. De Star
klassen består bare av metoder som legger til, fjerner eller henter stjerner fra en Arraylist
. Det skal også kunne returnere Arraylist
.
// Starfield klasse privat ArrayList s_stars = new ArrayList (); public void addStar (Star s) // En metode som legger til en stjerne i en ArrayList s_stars.add (s); public void removeStar (Star s) // En metode som fjerner en stjerne fra en ArrayList s_stars.remove (s); offentlig Star getStar (int i) // En metode som henter en stjerne med indeks i fra en ArrayList retur (Star) getStarfield (). get (i); offentlig ArrayList getStarfield () // En metode som returnerer ArrayList lagrer alle stjernene returnerer s_stars;
Nå som vi har fullført stjerneprofilen og initialisert bildet, vet vi noen viktige punkter om sekvensgeneratoren vi vil lage.
Først av alt, vet vi at bredden og høyden på bildet er 1000px. Dette betyr at rekkevidden av x- og y-koordinater skal falle i området 0-999 for å utnytte ressursene ved hånden. Siden to av de nødvendige tallene faller i samme område, kan vi bruke samme rekkevidde til stjernens størrelse for å holde likhet. Størrelsen vil da bli skalert ned senere når vi tolker serienummeret.
Vi skal bruke en rekke klassevariabler. Disse inkluderer: s_seed
, et enkelt heltall som definerer hele sekvensen; s_start
og sende
, to heltall som genereres ved å dele frøet i to; og s_current
, et heltall som inneholder det sist genererte nummeret i sekvensen.
s_start
og sende
. Tips: Merk at hvert generert tall stammer fra frøet; det er ingen anrop til tilfeldig()
. Dette betyr at det samme frøet alltid vil generere den samme stjernekilden. Vi bruker også s_sequence
, en string
som vil holde den generelle sekvensen. De to siste klassevariablene er s_image
(av type Bilde
- en klasse vi lager senere) og s_starfield
(av type Star
, klassen vi nettopp har opprettet). Den første lagrer bildet, mens den andre inneholder stjernen.
Ruten vi skal ta for å lage denne generatoren er ganske enkel. Først må vi lage en konstruktør som aksepterer et frø. Når dette er gjort, må vi lage en metode som aksepterer et heltall som representerer antall stjerner den må opprette. Denne metoden bør da ringe til selve generatoren for å komme opp med tallene. Og nå begynner det virkelige arbeidet ... å skape sekvensgeneratoren.
Det første som en sekvensgenerator må gjøre er å akseptere et frø. Som nevnt vil vi splitte frøet i to: de to første sifrene og de to siste sifrene. Av denne grunn må vi sjekke om frøet har fire sifre, og legg det med nuller hvis det ikke gjør det. Når dette er gjort, kan vi deretter dele frøstrengen i to variabler: s_start
og sende
. (Merk at frøene selv ikke vil være en del av den aktuelle sekvensen.)
// StarfieldSequence klasse offentlig StarfieldSequence (int seed) // En konstruktør som aksepterer et frø, og splitt det i to strenger s_seedTemp; s_starfield = new Starfield (); // Initialiser Starfield s_seed = frø; // Oppbevar frøet i en streng slik at vi enkelt kan dele det // Legg til nuller på strengen hvis frøet ikke har fire sifre hvis (frø < 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));
Så:
frø = 1234
midler s_start = 12
og s_end = 34
frø = 7
midler s_start = 00
og s_end = 07
frø = 303
midler s_start = 03
og s_end = 03
Neste i kø: Lag en annen metode som, gitt de to tallene, genererer det neste nummeret i sekvensen.
Å finne den rette formelen er en trøtt prosess. Det betyr vanligvis timer med prøve-og-feil-arbeid som prøver å finne en sekvens som ikke involverer for mange mønstre i det resulterende bildet. Derfor ville det være klokere å finne den beste formelen når vi faktisk kan se bildet, snarere enn nå. Det vi er interessert i akkurat nå, er å finne en formel som genererer en sekvens som er mer eller mindre tilfeldig. Av denne grunn vil vi bruke samme formel som brukes i Fibonacci-sekvensen: tillegg av de to tallene.
// StarfieldSequence klasse privat int getNext () // En metode som returnerer neste nummer i sekvensavkastningen (s_start + s_end);
Når dette er gjort, kan vi nå fortsette og begynne å lage sekvensen. I en annen metode vil vi manipulere det opprinnelige frøet for å generere en hel strøm av tall, som da kan tolkes som stjerneprofilens egenskaper.
Vi vet at for en gitt stjerne trenger vi ni sifre: de tre første definerer x-koordinaten, de midterste tre definerer y-koordinaten, og de tre siste definerer størrelsen. Derfor, som det var tilfelle når fôret frøene, for å holde likhet i hele det er viktig å sørge for at hvert generert nummer har tre sifre. I dette tilfellet må vi også kutte nummeret hvis det er større enn 999.
Dette er ganske lik det vi gjorde før. Vi trenger bare å lagre nummeret i en midlertidig streng, temp
, Deretter kastes det første sifferet. Hvis tallet ikke har tre siffer, bør vi deretter legge det med nuller som vi gjorde tidligere.
// StarfieldSequence klasse private void fixDigits () String temp = ""; // Hvis det nylig genererte nummeret har mer enn tre sifre, bare ta de siste tre hvis (s_current> 999) temp = Integer.toString (s_current); s_current = Integer.parseInt (temp.substring (1, 4)); // Hvis det nylig genererte nummeret har mindre enn tre sifre, legg nuller til begynnelsen hvis (s_current < 10) s_sequence += "00"; else if (s_current < 100) s_sequence += "0";
Med det innpakket, bør vi nå lage en annen metode som skaper og returnerer en stjerneprofil hver gang vi genererer tre tall. Ved hjelp av denne metoden kan vi deretter legge stjernen til Arraylist
av stjerner.
// StarfieldSequence klasse privat Star getStar (int i) // En metode som aksepterer et heltall (størrelsen på sekvensen) og returnerer stjernen // Del de siste ni sifrene i sekvensen i tre (de tre attributene til stjernen ) Star star = new Star (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))); returnere stjerne;
Etter at dette er ferdig, kan vi montere generatoren. Det bør akseptere antall stjerner den har å generere.
Vi vet allerede at for en stjerne trenger vi ni sifre, så denne generatoren trenger å fortsette å telle antall tegn i strengene. Disken, s_counter
, vil lagre maksimal lengde på sekvensen. Derfor multipliserer vi antall stjerner med ni og fjerner en siden a string
starter fra indeks null.
Vi må også fortsette å telle antall tegn vi har opprettet siden vi sist genererte en stjerne. For denne oppgaven skal vi bruke s_starcounter
. I en til
loop, som vil gjenta til serie lengde er like s_counter
, Vi kan nå kalle metodene vi har laget så langt.
s_start
og sende
, Ellers vil vi fortsette å generere det samme nummeret igjen og igjen! // StarfieldSequence klasse offentlig tomrom generere (int starnumber) // Genererer et antall stjerner som angitt av heltall starnumber int s_counter = 9 * starnumber; // s_counter holder styr på antall tegn som Strenge må generere det angitte antall stjerner s_counter - = 1; // Fjern en siden String starter fra indeks 0 int s_starcounter = 0; // s_starcounter holder oversikt over antall tall som genereres for (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) // Hvis tre tall har blitt generert siden den siste stjernen ble opprettet, lag en annen en s_starfield.addStar (getStar (s_sequence.length ())); // Erstatt s_start og s_end, ellers fortsetter du å generere det samme nummeret igjen og igjen! s_start = s_end; s_end = s_current;
Nå som den vanskelige delen er over, er det endelig tid til å flytte over til Bilde
klassen, og begynn å tegne stjerner.
I en metode som aksepterer a Star
, vi lager først en forekomst av a Farge
, deretter hente antall stjerner vi må tegne. I en til
loop, vi skal tegne alle stjernene. Etter å ha kopiert den nåværende stjernen, er det viktig at vi henter stjernens radius. Gitt at antall piksler er et heltall, bør vi legge til radius for å gjøre det til et heltall.
For å tegne stjernen vil vi bruke en radial gradient.
Et eksempel på en radial gradientEn radiell gradientens opasitet avhenger av avstanden til en piksel fra midten. Senterets sirkel vil ha koordinater (0,0)
. Ved hjelp av den vanligste konvensjonen har en piksel til venstre for senteret en negativ x-koordinat, og noen piksel under har en negativ y-koordinat.
På grunn av dette, den nestede til
sløyfer starter med et negativt tall. Ved å bruke Pythagoras teorem beregner vi avstanden fra sirkelens senter og bruker den til å hente opaciteten. For stjerner som har den minste mulige radius (1px), er deres ugjennomhetelse avhengig av deres størrelse.
// Image class offentlig ugyldig tegning (Starfield starfield) Color farge; for (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
For å pakke opp ting må vi lage en metode som aksepterer a string
og bruker den til å lagre bildet med det filnavnet. I generatoren bør vi først lage bildet. Da bør vi ringe disse to siste metodene fra sekvensgeneratoren.
// StarfieldSequence klasse offentlig tomrom generere (int starnumber) s_image.createImage (); // Opprett bildet int s_counter = 9 * starnumber; s_counter - = 1; int s_starcounter = 0; for (int i = 1; s_sequence.length () <= s_counter; i++) s_current = getNext(); fixDigits(); s_sequence += s_current; s_starcounter++; if (s_starcounter >= 3 && s sstarcounter% 3 == 0) s_starfield.addStar (getStar (s_sequence.length ())); s_start = s_end; s_end = s_current; s_image.draw (s_starfield); // Tegn stjernen s_image.save ("starfield"); // Lagre bildet med navnet 'starfield'
I Hoved
klasse, bør vi lage en forekomst av sekvensgeneratoren, tildele den et frø og få et godt antall stjerner (400 skal være nok). Prøv å kjøre programmet, fikse eventuelle feil, og kontroller destinasjonsbanen for å se hvilket bilde som ble opprettet.
Det er fortsatt noen endringer vi kan gjøre. For eksempel, det første du ville ha lagt merke til er at stjernene er gruppert i sentrum. For å fikse det, må du komme med en god formel som eliminerer noen mønstre. Alternativt kan du opprette en rekke formler og alternere mellom dem ved hjelp av en teller. Formlene vi brukte var disse:
// StarfieldSequence klasse privat int getNext () if (count == 0) hvis (s_start> 0 && s_end> 0) count ++; returnere (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)))); ellers teller ++; returnere (int) (Math.pow ((s_end + s_start), 4) / Math.pow ((s_end + s_start), 2) + Math.round (Math.abs (Math.cos (0.0175f * s_end))) + Math.cos (s_end) + Math.cos (s_start)); annet hvis (s_start> 0 && s_end> 0) count--; returnere (int) (Math.pow ((s_end + s_start), 2) + Math.round (Math.abs (Math.cos (0.0175f * s_end)))); ellers count--; returnere (int) (Math.pow ((s_end + s_start), 2) + Math.round (Math.abs (Math.cos (0.0175f * s_end))) + Math.cos (s_end) + Math.cos (s_start ));
Det er en enklere forbedring vi kan implementere. Hvis du ser opp på himmelen, vil du se noen store stjerner og mange flere små. Men i vårt tilfelle er antall små stjerner omtrent det samme som antall store stjerner. For å fikse dette, må vi bare gå tilbake til getSize ()
metode i Stjerne
klasse. Etter å ha størrelsen en brøkdel av en, må vi heve dette tallet til kraften til et heltall - for eksempel fire eller fem.
// Star class offentlig dobbel getSize () return (double) (Math.pow ((double) s_size / 1000, 4));
Kjører programmet en siste gang, bør gi deg et tilfredsstillende resultat.
Det endelige resultatet - et helt stjerneskape som genereres generelt av vår Sequence Generator!I dette tilfellet brukte vi en sekvensgenerator til å generere en prosessorisk prosess. En sekvensgenerator som dette kunne ha mange flere bruksområder. For eksempel kan en z-koordinat legges til stjernen, slik at i stedet for å tegne et bilde, kunne kunne generere stjerner som objekter i et 3D-miljø.