Codering van een Custom Sequence Generator om een ​​Starscape te renderen

In mijn vorige artikel legde ik het verschil uit tussen een pseudowillekeurige nummergenerator en een sequentiegenerator en onderzocht ik de voordelen die een sequentiegenerator heeft ten opzichte van een PRNG. In deze tutorial coderen we een vrij eenvoudige sequentiegenerator. Het genereert een reeks getallen, manipuleert en interpreteert deze reeks en gebruikt het vervolgens om een ​​heel eenvoudige sterrencape te tekenen.

Notitie: Hoewel deze tutorial geschreven is met behulp van Java, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Maken en initialiseren van de afbeelding

Het eerste dat we moeten doen is de afbeelding maken. Voor deze sequentiegenerator gaan we een 1000 × 1000 px-afbeelding maken om het genereren van nummers zo eenvoudig mogelijk te houden. Verschillende talen doen dit anders, dus gebruik de nodige code voor uw dev-platform.

Als je de afbeelding hebt gemaakt, is het tijd om hem een ​​achtergrondkleur te geven. Omdat we het hebben over een sterrenhemel, is het verstandiger om te beginnen met een zwarte (# 000000) achtergrond en voeg dan de witte sterren toe, in plaats van andersom.


Een sterrenprofiel en een sterrenveld maken

Voordat we aan de sequentiegenerator beginnen te werken, moet je erachter komen waar je mee wilt beginnen. Dit betekent weten wat je wilt maken en hoe verschillende zaden en nummers variëren wat je wilt creëren - in dit geval de sterren.

Om dit te doen, moeten we een voorbeeldsterprofiel maken dat klassevariabelen bevat die enkele eigenschappen van de sterren aangeven. Om het simpel te houden, beginnen we met slechts drie attributen:

  • x-coördinaat
  • y-coördinaat
  • grootte

Elk van de drie attributen heeft waarden van 0 tot 999, wat betekent dat aan elk attribuut drie cijfers zijn toegewezen. Dit alles wordt opgeslagen in een Ster klasse.

Twee belangrijke methoden in de Ster klasse zijn getSize () en getRadiusPx (). De getSize () methode retourneert de stergrootte, verkleind tot een decimaal getal tussen nul en één, en de getRadiusPx () methode geeft aan hoe groot de straal van de ster in de uiteindelijke afbeelding moet zijn.

Ik heb gemerkt dat 4 pixels zorgt voor een goede maximale radius in mijn demo, dus getRadiusPx () zal gewoon de waarde van retourneren getSize () vermenigvuldigd met vier. Bijvoorbeeld, als de getSize () methode geeft een straal van 0.4, de getRadiusPx () methode zou een straal van 1.6px geven.

 // Star class private int s_x, s_y, s_size; public Star (int x, int y, int size) // Constructor die beginkenmerken instelt s_x = x; s_y = y; s_size = grootte;  public int getX () // Retourneert de x-coördinaat van de sterretour s_x;  public int getY () // Retourneert de y-coördinaat van de sterretour s_y;  public double getSize () // Retourneert de straal van de ster als een decimaal getal tussen 0 en 1 retour (dubbel) (s_size / 1000);  public double getRadiusPx () // Geeft de straal van de ster in pixels terug (dubbel) 4 * getSize (); // 4px is de grootste straal die een ster kan hebben

We moeten ook een heel eenvoudige klas maken wiens taak erin bestaat om alle sterren in elke reeks sterren bij te houden. De starfield klasse bestaat alleen uit methoden die sterren toevoegen, verwijderen of ophalen van een ArrayList. Het moet ook in staat zijn om de ArrayList.

 // Starfield class private ArrayList s_stars = new ArrayList (); public void addStar (Star s) // Een methode die een ster toevoegt aan een ArrayList s_stars.add (s);  public void removeStar (Star s) // Een methode die een ster verwijdert uit een ArrayList s_stars.remove (s);  public Star getStar (int i) // Een methode die een ster ophaalt met index i van een ArrayList-return (Star) getStarfield (). get (i);  public ArrayList getStarfield () // Een methode die ervoor zorgt dat de ArrayList alle sterren opslaat en s_stars retourneert; 

Planning van de sequentiegenerator

Nu we het sterprofiel hebben voltooid en de afbeelding hebben geïnitialiseerd, weten we enkele belangrijke opmerkingen over de sequentiegenerator die we willen maken.

Ten eerste weten we dat de breedte en hoogte van de afbeelding 1000 px is. Dit betekent dat, om de beschikbare bronnen te exploiteren, het bereik van x- en y-coördinaten moet vallen in het bereik 0-999. Aangezien twee van de vereiste getallen in hetzelfde bereik vallen, kunnen we hetzelfde bereik toepassen op de grootte van de ster om uniformiteit te behouden. De grootte zal later worden verkleind wanneer we de reeks getallen interpreteren.

We gaan een aantal klassenvariabelen gebruiken. Waaronder: s_seed, een enkel geheel getal dat de hele reeks definieert; s_start en sturen, twee gehele getallen die worden gegenereerd door het zaad in tweeën te splitsen; en s_current, een geheel getal dat het meest recent gegenereerde nummer in de reeks bevat.


Zie deze afbeelding van mijn vorige artikel. 1234 is het zaad en 12 en 34 zijn de beginwaarden van s_start en sturen. Tip: Merk op dat elk gegenereerd nummer afkomstig is van het zaad; er is geen oproep voor willekeurig(). Dit betekent dat hetzelfde zaad altijd hetzelfde sterrenbeeld zal genereren.

We zullen ook gebruiken s_sequence, een Draad welke de algehele reeks zal bevatten. De laatste twee klassenvariabelen zijn s_image (van type Beeld - een les die we later zullen maken) en s_starfield (van type starfield, de klas die we zojuist hebben gemaakt). De eerste slaat de afbeelding op, terwijl de tweede het starfield bevat.

De route die we gaan volgen om deze generator te maken is vrij eenvoudig. Eerst moeten we een constructeur maken die een zaadje accepteert. Wanneer dit is gebeurd, moeten we een methode maken die een geheel getal accepteert dat het aantal sterren vertegenwoordigt dat het moet maken. Deze methode zou dan de eigenlijke generator moeten bellen om met de cijfers te komen. En nu begint het echte werk ... het creëren van de sequentiegenerator.


Codering van de sequentiegenerator

Het eerste dat een sequentiegenerator hoeft te doen, is een zaad accepteren. Zoals gezegd, splitsen we het zaad in tweeën: de eerste twee cijfers en de laatste twee cijfers. Om deze reden moeten we controleren of het zaad vier cijfers heeft en het vullen met nullen als dit niet het geval is. Wanneer dit is gebeurd, kunnen we de seed-string in twee variabelen splitsen: s_start en sturen. (Merk op dat de zaadjes zelf geen deel uitmaken van de feitelijke volgorde.)

 // StarfieldSequence class public StarfieldSequence (int seed) // Een constructor die een seed accepteert en deze in twee String s_seedTemp splitst; s_starfield = new Starfield (); // Initialiseer het Starfield s_seed = seed; // Bewaar het zaad in een string zodat we het gemakkelijk kunnen splitsen // Nullen aan de string toevoegen als het zaadje geen vier cijfers heeft als < 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)); 

Zo:

  • zaad = 1234 middelen s_start = 12 en s_end = 34
  • zaad = 7 middelen s_start = 00 en s_end = 07
  • zaad = 303 middelen s_start = 03 en s_end = 03

Next in line: maak een andere methode die, gegeven de twee nummers, het volgende nummer in de reeks genereert.

Het vinden van de juiste formule is een moe proces. Het betekent meestal urenlang proberen om een ​​reeks te vinden die niet te veel patronen in de resulterende afbeelding bevat. Daarom zou het verstandiger zijn om de beste formule te vinden zodra we het beeld daadwerkelijk kunnen zien, in plaats van nu. Waar we op dit moment in geïnteresseerd zijn, is het vinden van een formule die een reeks genereert die min of meer willekeurig is. Om deze reden zullen we dezelfde formule gebruiken die in de Fibonacci-reeks wordt gebruikt: toevoeging van de twee getallen.

 // StarfieldSequence class private int getNext () // Een methode die het volgende getal in de reeksterugloop retourneert (s_start + s_end); 

Wanneer dit is gebeurd, kunnen we nu verder gaan en beginnen met het maken van de reeks. In een andere methode zullen we het initiële zaad manipuleren om een ​​hele stroom getallen te genereren, die dan geïnterpreteerd kunnen worden als de attributen van het sterprofiel..

We weten dat voor een bepaalde ster we negen cijfers nodig hebben: de eerste drie definiëren de x-coördinaat, de middelste drie definiëren de y-coördinaat en de laatste drie bepalen de grootte. Daarom is het, net als bij het voeren van het zaad, belangrijk om uniformiteit te behouden om ervoor te zorgen dat elk gegenereerd getal uit drie cijfers bestaat. In dit geval moeten we het nummer ook inkorten als het groter is dan 999.

Dit is vrij gelijkaardig aan wat we eerder deden. We moeten het nummer opslaan in een tijdelijke string, temp, gooi dan het eerste cijfer weg. Als het nummer geen drie cijfers heeft, moeten we het pad opvullen met nullen zoals we eerder deden.

 // StarfieldSequence class private void fixDigits () String temp = ""; // Als het nieuw gegenereerde nummer uit meer dan drie cijfers bestaat, neemt u alleen de laatste drie als (s_current> 999) temp = Integer.toString (s_current); s_current = Integer.parseInt (temp.substring (1, 4));  // Als het nieuw gegenereerde nummer minder dan drie cijfers heeft, voegt u nullen toe aan het begin als (s_current < 10) s_sequence += "00";  else if (s_current < 100) s_sequence += "0";  

Met dat ingepakt, zouden we nu een andere methode moeten maken die een sterprofiel creëert en retourneert elke keer dat we drie getallen genereren. Met behulp van deze methode kunnen we vervolgens de ster toevoegen aan de ArrayList van sterren.

 // StarfieldSequence class private Star getStar (int i) // Een methode die een geheel getal (de grootte van de reeks) accepteert en de ster teruggeeft // Split de laatste negen cijfers in de reeks in drie (de drie kenmerken van de ster ) Sterster = nieuwe ster (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))); ster terugkeren; 

Alles samenvoegen

Nadat dit is voltooid, kunnen we de generator assembleren. Het zou het aantal sterren moeten accepteren dat het moet genereren.

We weten al dat voor één ster we negen cijfers nodig hebben, dus deze generator moet het aantal tekens in de strings bijhouden. De teller, s_counter, slaat de maximale lengte van de reeks op. Daarom vermenigvuldigen we het aantal sterren met negen en verwijderen we een sinds a Draad start vanaf index nul.

We moeten ook het aantal tekens bijhouden dat we hebben gemaakt sinds we voor het laatst een ster hebben gegenereerd. Voor deze taak gaan we gebruiken s_starcounter. In een voor loop, die zal herhalen totdat de lengte van de reeks gelijk is s_counter, we kunnen nu de methoden bellen die we tot nu toe hebben gemaakt.

Tip: We moeten niet vergeten te vervangen s_start en sturen, anders blijven we steeds hetzelfde nummer genereren!
 // StarfieldSequence class public void genereren (int starnumber) // Genereert een aantal sterren zoals aangegeven door het integer-starnumber int s_counter = 9 * starnumber; // s_counter houdt bij hoeveel tekens de String moet hebben om het aangewezen aantal sterren s_counter - = 1 te genereren; // Verwijder een omdat een reeks begint met index 0 int s_starcounter = 0; // s_starcounter houdt het aantal getallen bij dat wordt gegenereerd voor (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) // Als er drie nummers zijn gegenereerd sinds de laatste ster is gemaakt, maak dan een nieuwe s_starfield.addStar (getStar (s_sequence.length ()));  // Vervang s_start en s_end, anders blijft u hetzelfde nummer steeds opnieuw genereren! s_start = s_end; s_end = s_current; 

Sterren tekenen

Nu het moeilijke gedeelte voorbij is, is het eindelijk tijd om over te stappen naar de Beeld klasse en begin sterren te tekenen.

In een methode die a accepteert starfield, we maken eerst een instantie van a Kleur, haal dan het aantal sterren op dat we moeten tekenen. In een voor lus, we gaan alle sterren tekenen. Na het maken van een kopie van de huidige ster, is het belangrijk dat we de straal van de ster ophalen. Aangezien het aantal pixels een geheel getal is, moeten we het toevoegen aan de straal om er een geheel getal van te maken.

Om de ster te tekenen, gebruiken we een radiaal verloop.

Een voorbeeld van een radiaal verloop

De dekking van een radiale gradient hangt af van de afstand van een pixel tot het midden. Het midden van de cirkel heeft coördinaten (0,0). Met behulp van de meest gebruikelijke conventie heeft elke pixel links van het midden een negatieve x-coördinaat, en elke pixel hieronder heeft een negatieve y-coördinaat.

Vanwege dit, de geneste voor loops beginnen met een negatief getal. Met behulp van de stelling van Pythagoras berekenen we de afstand vanaf het middelpunt van de cirkel en gebruiken deze om de dekking op te halen. Voor sterren met de kleinst mogelijke straal (1px), hangt hun dekking alleen af ​​van hun grootte.

 // Openbare afbeelding ongeldige afbeelding classificeren (Starfield starfield) Kleur kleur; voor (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     

Om zaken af ​​te ronden, moeten we een methode maken die a accepteert Draad en gebruikt het om de afbeelding met die bestandsnaam op te slaan. In de generator moeten we eerst de afbeelding maken. Dan zouden we deze laatste twee methoden uit de sequentiegenerator moeten noemen.

 // StarfieldSequence class public void generate (int starnumber) s_image.createImage (); // Maak de afbeelding 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_starcounter% 3 == 0) s_starfield.addStar (getStar (s_sequence.length ()));  s_start = s_end; s_end = s_current;  s_image.draw (s_starfield); // Teken het starfield s_image.save ("starfield"); // Sla de afbeelding op met de naam 'starfield'

In de Hoofd klasse, moeten we een instantie van de sequentiegenerator maken, een seed toewijzen en een goed aantal sterren krijgen (400 zou voldoende moeten zijn). Probeer het programma uit te voeren, eventuele fouten op te lossen en controleer het doelpad om te zien welke afbeelding is gemaakt.

Het resulterende beeld met een zaadje van 1234

verbeteringen

Er zijn nog steeds enkele wijzigingen die we kunnen aanbrengen. Het eerste dat u bijvoorbeeld zou hebben opgemerkt, is dat de sterren in het midden geclusterd zijn. Om dat te verhelpen, zou je een goede formule moeten verzinnen die patronen elimineert. U kunt ook een aantal formules maken en deze afwisselen met een teller. De formules die we gebruikten waren deze:

 // StarfieldSequence class private int getNext () if (count == 0) if (s_start> 0 && s_end> 0) count ++; return (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))));  else count ++; return (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));  else if (s_start> 0 && s_end> 0) count--; return (int) (Math.pow ((s_end + s_start), 2) + Math.round (Math.abs (Math.cos (0.0175f * s_end))));  else count--; return (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 )); 

Er is nog een eenvoudige verbetering die we kunnen implementeren. Als je naar de lucht kijkt, zie je een paar grote sterren en nog veel meer kleine sterren. In ons geval is het aantal kleine sterren echter ongeveer hetzelfde als het aantal grote sterren. Om dit op te lossen, moeten we gewoon terugkeren naar de getSize () methode in de Ster klasse. Nadat we de grootte een fractie van één hebben gemaakt, moeten we dit getal verhogen tot een geheel getal - bijvoorbeeld vier of vijf.

 // Star class public double getSize () return (double) (Math.pow ((double) s_size / 1000, 4)); 

Als u het programma voor de laatste keer uitvoert, krijgt u een bevredigend resultaat.

Het eindresultaat - een volledig sterrenbeeld dat procedureel is gegenereerd door onze sequentiegenerator!

Conclusie

In dit geval hebben we een sequencegenerator gebruikt om procedureel een achtergrond te genereren. Een Sequence Generator zoals deze kan nog veel meer gebruiken - bijvoorbeeld, een z-coördinaat kan aan de ster worden toegevoegd, zodat in plaats van een afbeelding te tekenen, de zou sterren zouden kunnen genereren als objecten in een 3D-omgeving.