Codieren eines benutzerdefinierten Sequenzgenerators zum Rendern einer Sternenlandschaft
German (Deutsch) translation by Valentina (you can also view the original English article)
In meinem vorherigen Artikel habe ich den Unterschied zwischen einem Pseudozufallszahlengenerator und einem Sequenzgenerator erläutert und die Vorteile eines Sequenzgenerators gegenüber einem PRNG untersucht. In diesem Tutorial werden wir einen ziemlich einfachen Sequenzgenerator codieren. Es generiert eine Folge von Zahlen, manipuliert und interpretiert diese Sequenz und zeichnet damit eine sehr einfache Sternenlandschaft.
Hinweis: Obwohl dieses Tutorial mit Java geschrieben wurde, sollten Sie in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte verwenden können.
Erstellen und Initialisieren des Images
Als erstes müssen wir das Bild erstellen. Für diesen Sequenzgenerator erstellen wir ein 1000 × 1000px-Bild, um die Zahlengenerierung so einfach wie möglich zu halten. Verschiedene Sprachen tun dies unterschiedlich. Verwenden Sie daher den erforderlichen Code für Ihre Entwicklungsplattform.
Wenn Sie das Bild erfolgreich erstellt haben, ist es Zeit, ihm eine Hintergrundfarbe zu geben. Da es sich um einen Sternenhimmel handelt, ist es sinnvoller, mit einem schwarzen Hintergrund (#000000) zu beginnen und dann die weißen Sterne hinzuzufügen, anstatt umgekehrt.
Erstellen eines Sternprofils und eines Sternfelds
Bevor wir mit der Arbeit am Sequenzgenerator beginnen, sollten Sie herausfinden, wohin Sie damit wollen. Dies bedeutet zu wissen, was Sie erstellen möchten und wie unterschiedliche Samen und Zahlen variieren, was Sie erstellen möchten - in diesem Fall die Sterne.
Dazu müssen wir ein Beispielsternprofil erstellen, das Klassenvariablen enthält, die einige der Eigenschaften der Sterne angeben. Um die Dinge einfach zu halten, beginnen wir mit nur drei Attributen:
- x-Koordinate
- y-Koordinate
- Größe
Jedes der drei Attribute hat Werte zwischen 0 und 999, was bedeutet, dass jedem Attribut drei Ziffern zugewiesen sind. All dies wird in einer Star-Klasse gespeichert.
Zwei wichtige Methoden in der Star-Klasse sind getSize() und getRadiusPx(). Die Methode getSize() gibt die Größe des Sterns zurück, die auf eine Dezimalzahl zwischen Null und Eins verkleinert ist, und die Methode getRadiusPx() gibt zurück, wie groß der Radius des Sterns im endgültigen Bild sein soll.
Ich habe festgestellt, dass 4 Pixel einen guten maximalen Radius in meiner Demo ergeben, daher gibt getRadiusPx() einfach den Wert von getSize() multipliziert mit vier zurück. Wenn die Methode getSize() beispielsweise einen Radius von 0,4 zurückgibt, gibt die Methode getRadiusPx() einen Radius von 1,6 Pixel an.
1 |
|
2 |
//Star class
|
3 |
|
4 |
private int s_x, s_y, s_size; |
5 |
|
6 |
public Star(int x, int y, int size){ |
7 |
//Constructor which sets initial attributes
|
8 |
s_x = x; |
9 |
s_y = y; |
10 |
s_size = size; |
11 |
}
|
12 |
|
13 |
public int getX(){ |
14 |
//Returns the x-co-ordinate of the star
|
15 |
return s_x; |
16 |
}
|
17 |
|
18 |
public int getY(){ |
19 |
//Returns the y-co-ordinate of the star
|
20 |
return s_y; |
21 |
}
|
22 |
|
23 |
public double getSize(){ |
24 |
//Returns the star's radius as a decimal number between 0 and 1
|
25 |
return (double) (s_size/1000); |
26 |
}
|
27 |
|
28 |
public double getRadiusPx(){ |
29 |
//Returns the star's radius in pixels
|
30 |
return (double) 4*getSize(); //4px is the biggest radius a star can have |
31 |
}
|
Wir sollten auch eine sehr einfache Klasse bilden, deren Aufgabe es ist, alle Sterne in jeder Folge von Sternen im Auge zu behalten. Die Starfield-Klasse besteht nur aus Methoden, mit denen Sterne zu einer ArrayList hinzugefügt, entfernt oder abgerufen werden. Es sollte auch in der Lage sein, die ArrayList zurückzugeben.
1 |
|
2 |
//Starfield class
|
3 |
|
4 |
private ArrayList s_stars = new ArrayList(); |
5 |
|
6 |
public void addStar(Star s){ |
7 |
//A method which adds a star to an ArrayList
|
8 |
s_stars.add(s); |
9 |
}
|
10 |
|
11 |
public void removeStar(Star s){ |
12 |
//A method which removes a star from an ArrayList
|
13 |
s_stars.remove(s); |
14 |
}
|
15 |
|
16 |
public Star getStar(int i){ |
17 |
//A method which retrieves a star with index i from an ArrayList
|
18 |
return (Star) getStarfield().get(i); |
19 |
}
|
20 |
|
21 |
public ArrayList getStarfield(){ |
22 |
//A method which returns the ArrayList storing all the stars
|
23 |
return s_stars; |
24 |
}
|
Planen des Sequenzgenerators
Nachdem wir das Sternprofil fertiggestellt und das Bild initialisiert haben, kennen wir einige wichtige Punkte über den Sequenzgenerator, den wir erstellen möchten.
Zunächst wissen wir, dass die Breite und Höhe des Bildes 1000px beträgt. Dies bedeutet, dass der Bereich der x- und y-Koordinaten in den Bereich 0-999 fallen muss, um die vorhandenen Ressourcen zu nutzen. Da zwei der erforderlichen Zahlen in den gleichen Bereich fallen, können wir den gleichen Bereich auf die Größe des Sterns anwenden, um die Gleichmäßigkeit zu gewährleisten. Die Größe wird später verkleinert, wenn wir die Zahlenreihe interpretieren.
Wir werden eine Reihe von Klassenvariablen verwenden. Dazu gehören: s_seed, eine einzelne Ganzzahl, die die gesamte Sequenz definiert; s_start und s_end, zwei Ganzzahlen, die durch Aufteilen des Startwerts in zwei generiert werden; und s_current, eine Ganzzahl, die die zuletzt generierte Zahl in der Sequenz enthält.



s_start und s_end.random(). Das bedeutet, dass derselbe Samen immer die gleiche Sternenlandschaft erzeugt.Wir werden auch s_sequence verwenden, einen String, der die Gesamtsequenz enthält. Die letzten beiden Klassenvariablen sind s_image (vom Typ Image - eine Klasse, die wir später erstellen werden) und s_starfield (vom Typ Starfield, die Klasse, die wir gerade erstellt haben). Der erste speichert das Bild, während der zweite das Sternenfeld enthält.
Der Weg, den wir zur Erstellung dieses Generators einschlagen werden, ist recht einfach. Zuerst müssen wir einen Konstruktor erstellen, der einen Startwert akzeptiert. Wenn dies erledigt ist, müssen wir eine Methode erstellen, die eine Ganzzahl akzeptiert, die die Anzahl der Sterne darstellt, die sie erstellen muss. Diese Methode sollte dann den eigentlichen Generator aufrufen, um die Zahlen zu ermitteln. Und jetzt beginnt die eigentliche Arbeit... die Erstellung des Sequenzgenerators.
Codierung des Sequenzgenerators
Das erste, was ein Sequenzgenerator tun muss, ist, einen Startwert zu akzeptieren. Wie bereits erwähnt, werden wir den Samen in zwei Teile aufteilen: die ersten beiden Ziffern und die letzten beiden Ziffern. Aus diesem Grund müssen wir prüfen, ob der Startwert vierstellig ist, und ihn mit Nullen auffüllen, wenn dies nicht der Fall ist. Wenn dies erledigt ist, können wir die Startzeichenfolge in zwei Variablen aufteilen: s_start und s_end. (Beachten Sie, dass die Samen selbst nicht Teil der tatsächlichen Sequenz sind.)
1 |
|
2 |
//StarfieldSequence class
|
3 |
|
4 |
public StarfieldSequence (int seed){ |
5 |
//A constructor which accepts a seed, and splits it into two
|
6 |
String s_seedTemp; |
7 |
s_starfield = new Starfield(); //Initialize the Starfield |
8 |
s_seed = seed; //Store the seed in a string so that we can easily split it |
9 |
|
10 |
//Add zeros to the string if the seed doesn't have four digits
|
11 |
if (seed < 10){ |
12 |
s_seedTemp = "000"; |
13 |
s_seedTemp = s_seedTemp.concat(Integer.toString(seed)); |
14 |
} else if (seed < 100){ |
15 |
s_seedTemp = "00"; |
16 |
s_seedTemp = s_seedTemp.concat(Integer.toString(seed)); |
17 |
} else if (seed < 1000){ |
18 |
s_seedTemp = "0"; |
19 |
s_seedTemp = s_seedTemp.concat(Integer.toString(seed)); |
20 |
} else { |
21 |
s_seedTemp = Integer.toString(seed); |
22 |
}
|
23 |
|
24 |
//Split the seed into two - the first two digits are stored in s_start, while the last two are stored in s_end
|
25 |
s_start = Integer.parseInt(s_seedTemp.substring(0, 2)); |
26 |
s_end = Integer.parseInt(s_seedTemp.substring(2, 4)); |
27 |
}
|
Damit:
-
seed=1234bedeutets_start=12unds_end=34 -
seed=7bedeutets_start=00unds_end=07 -
seed=303bedeutets_start=03unds_end=03
Weiter in der Zeile: Erstellen Sie eine andere Methode, die anhand der beiden Zahlen die nächste Zahl in der Sequenz generiert.
Die richtige Formel zu finden ist ein müder Prozess. Dies bedeutet normalerweise stundenlanges Ausprobieren, um eine Sequenz zu finden, die nicht zu viele Muster im resultierenden Bild enthält. Daher wäre es klüger, die beste Formel zu finden, wenn wir das Bild tatsächlich sehen können, als jetzt. Was uns gerade interessiert, ist die Suche nach einer Formel, die eine mehr oder weniger zufällige Sequenz erzeugt. Aus diesem Grund verwenden wir dieselbe Formel wie in der Fibonacci-Sequenz: Addition der beiden Zahlen.
1 |
|
2 |
//StarfieldSequence class
|
3 |
|
4 |
private int getNext(){ |
5 |
//A method that returns the next number in the sequence
|
6 |
return (s_start + s_end); |
7 |
}
|
Wenn dies erledigt ist, können wir nun fortfahren und mit der Erstellung der Sequenz beginnen. Bei einer anderen Methode manipulieren wir den anfänglichen Startwer, um einen ganzen Strom von Zahlen zu generieren, die dann als Attribute des Sternprofils interpretiert werden können.
Wir wissen, dass wir für einen bestimmten Stern neun Ziffern benötigen: Die ersten drei definieren die x-Koordinate, die mittleren drei definieren die y-Koordinate und die letzten drei definieren die Größe. Wie bei der Fütterung des Samens ist es daher wichtig, dass jede generierte Zahl dreistellig ist, um die Gleichmäßigkeit zu gewährleisten. In diesem Fall müssen wir die Zahl auch abschneiden, wenn sie größer als 999 ist.
Dies ist ziemlich ähnlich zu dem, was wir zuvor gemacht haben. Wir müssen nur die Nummer in einem temporären String speichern, temp, und dann die erste Ziffer entsorgen. Wenn die Zahl keine drei Ziffern hat, sollten wir sie wie zuvor mit Nullen auffüllen.
1 |
|
2 |
//StarfieldSequence class
|
3 |
|
4 |
private void fixDigits(){ |
5 |
String temp = ""; |
6 |
//If the newly-generated number has more than three digits, only take the last three
|
7 |
if (s_current > 999){ |
8 |
temp = Integer.toString(s_current); |
9 |
s_current = Integer.parseInt(temp.substring(1, 4)); |
10 |
}
|
11 |
//If the newly-generated number has less than three digits, add zeros to the beginning
|
12 |
if (s_current < 10){ |
13 |
s_sequence += "00"; |
14 |
} else if (s_current < 100){ |
15 |
s_sequence += "0"; |
16 |
}
|
17 |
}
|
Nachdem dies abgeschlossen ist, sollten wir jetzt eine andere Methode erstellen, die jedes Mal, wenn wir drei Zahlen generieren, ein Sternprofil erstellt und zurückgibt. Mit dieser Methode können wir den Stern dann zur ArrayList of Stars hinzufügen.
1 |
|
2 |
//StarfieldSequence class
|
3 |
|
4 |
private Star getStar(int i){ |
5 |
//A method which accepts an integer (the size of the sequence) and returns the Star
|
6 |
|
7 |
//Split the last nine digits in the sequence into three (the three attributes of the star)
|
8 |
Star star = new Star(Integer.parseInt(s_sequence.substring(i-9, i-6)), |
9 |
Integer.parseInt(s_sequence.substring(i-6, i-3)), |
10 |
Integer.parseInt(s_sequence.substring(i-3, i))); |
11 |
return star; |
12 |
}
|
Alles zusammenfügen
Nachdem dies abgeschlossen ist, können wir den Generator zusammenbauen. Es sollte die Anzahl der Sterne akzeptieren, die es erzeugen muss.
Wir wissen bereits, dass wir für einen Stern neun Ziffern benötigen, daher muss dieser Generator die Anzahl der Zeichen in den Zeichenfolgen zählen. Der Zähler s_counter speichert die maximale Länge der Sequenz. Daher multiplizieren wir die Anzahl der Sterne mit neun und entfernen eins, da ein String mit dem Index Null beginnt.
Wir müssen auch die Anzahl der Zeichen zählen, die wir seit der letzten Generierung eines Sterns erstellt haben. Für diese Aufgabe verwenden wir s_starcounter. In einer for-Schleife, die wiederholt wird, bis die Länge der Serie s_counter entspricht, können wir jetzt die bisher erstellten Methoden aufrufen.
s_start und s_end zu ersetzen, sonst generieren wir immer wieder dieselbe Nummer!1 |
|
2 |
//StarfieldSequence class
|
3 |
|
4 |
public void generate(int starnumber){ |
5 |
//Generates a number of stars as indicated by the integer starnumber
|
6 |
int s_counter = 9 * starnumber; //s_counter keeps track of the number of characters the String must have to generate the designated number of stars |
7 |
s_counter -= 1; //Remove one since a String starts from index 0 |
8 |
int s_starcounter = 0; //s_starcounter keeps track of the number of numbers generated |
9 |
|
10 |
for (int i = 1; s_sequence.length() <= s_counter; i++){ |
11 |
s_current = getNext(); //Generate the next number in the sequence |
12 |
fixDigits(); //Make sure the number has three digits |
13 |
s_sequence += s_current; //Add the new number to the sequence |
14 |
s_starcounter++; |
15 |
|
16 |
if (s_starcounter >= 3 && s_starcounter % 3 == 0){ |
17 |
//If three numbers have been generated since the last star was created, create another one
|
18 |
s_starfield.addStar(getStar(s_sequence.length())); |
19 |
}
|
20 |
|
21 |
//Replace s_start and s_end, or else you will keep on generating the same number over and over again!
|
22 |
s_start = s_end; |
23 |
s_end = s_current; |
24 |
}
|
25 |
}
|
Sterne zeichnen
Jetzt, da der schwierige Teil vorbei ist, ist es endlich Zeit, zur Image-Klasse überzugehen und mit dem Zeichnen von Sternen zu beginnen.
Bei einer Methode, die ein Starfield akzeptiert, erstellen wir zuerst eine Instanz Color und rufen dann die Anzahl der Sterne ab, die wir zeichnen müssen. In einer for-Schleife zeichnen wir alle Sterne. Nachdem Sie eine Kopie des aktuellen Sterns erstellt haben, ist es wichtig, dass wir den Radius des Sterns abrufen. Da die Anzahl der Pixel eine Ganzzahl ist, sollten wir den Radius addieren, um ihn zu einer Ganzzahl zu machen.
Um den Stern zu zeichnen, verwenden wir einen radialen Gradienten.



Die Opazität eines radialen Gradienten hängt vom Abstand eines Pixels von der Mitte ab. Der Mittelpunkt des Kreises hat Koordinaten (0,0). Unter Verwendung der gebräuchlichsten Konvention hat jedes Pixel links von der Mitte eine negative x-Koordinate, und jedes Pixel darunter hat eine negative y-Koordinate.
Aus diesem Grund beginnen die verschachtelten for-Schleifen mit einer negativen Zahl. Mit dem Satz von Pythagoras berechnen wir den Abstand vom Mittelpunkt des Kreises und verwenden ihn, um die Deckkraft abzurufen. Bei Sternen mit dem kleinstmöglichen Radius (1 Pixel) hängt ihre Deckkraft ausschließlich von ihrer Größe ab.
1 |
|
2 |
//Image class
|
3 |
|
4 |
public void draw(Starfield starfield){ |
5 |
Color color; |
6 |
for (int i = 0; i < starfield.getStarfield().size(); i++){ //Repeat for every star |
7 |
Star s = starfield.getStar(i); |
8 |
int f = (int) Math.ceil(s.getRadiusPx()); //We need an integer, so we ceil the star's radius |
9 |
for (int x = -1*f; x <= f; x++){ |
10 |
for (int y = -1*f; y <= f; y++){ |
11 |
//Calculate the distance of the current pixel from the star's center
|
12 |
double d = Math.abs(Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); |
13 |
if (d < s.getRadiusPx()){ //Only draw pixel if it falls within radius |
14 |
if (f == 1){ |
15 |
//If the star's radius is just one, the opacity depends on the star's size
|
16 |
color = new Color(0.85f, 0.95f, 1, (float) s.getSize()); |
17 |
} else { |
18 |
//The opacity here depends on the distance of the pixel from the center
|
19 |
color = new Color(0.85f, 0.95f, 1, (float) ((s.getRadiusPx() - d)/s.getRadiusPx())); |
20 |
}
|
21 |
graphics.setColor(color); //Assign a color for the next pixel |
22 |
graphics.fillRect(s.getX()+x, s.getY()+y, 1, 1); //Fill the pixel |
23 |
}
|
24 |
}
|
25 |
}
|
26 |
}
|
27 |
}
|
Zum Abschluss müssen wir eine Methode erstellen, die einen String akzeptiert und damit das Bild unter diesem Dateinamen speichert. Im Generator sollten wir zuerst das Bild erstellen. Dann sollten wir diese beiden letzten Methoden vom Sequenzgenerator aufrufen.
1 |
|
2 |
//StarfieldSequence class
|
3 |
|
4 |
public void generate(int starnumber){ |
5 |
s_image.createImage(); //Create the image |
6 |
int s_counter = 9*starnumber; |
7 |
s_counter -= 1; |
8 |
int s_starcounter = 0; |
9 |
for (int i = 1; s_sequence.length() <= s_counter; i++){ |
10 |
s_current = getNext(); |
11 |
fixDigits(); |
12 |
s_sequence += s_current; |
13 |
s_starcounter++; |
14 |
if (s_starcounter >= 3 && s_starcounter % 3 == 0){ |
15 |
s_starfield.addStar(getStar(s_sequence.length())); |
16 |
}
|
17 |
s_start = s_end; |
18 |
s_end = s_current; |
19 |
}
|
20 |
s_image.draw(s_starfield); //Draw the starfield |
21 |
s_image.save("starfield"); //Save the image with the name 'starfield' |
22 |
}
|
In der Main-Klasse sollten wir eine Instanz des Sequenzgenerators erstellen, ihm einen Startwert zuweisen und eine gute Anzahl von Sternen erhalten (400 sollten ausreichen). Versuchen Sie, das Programm auszuführen, Fehler zu beheben und den Zielpfad zu überprüfen, um festzustellen, welches Image erstellt wurde.



Verbesserungen
Es gibt noch einige Änderungen, die wir vornehmen können. Das erste, was Sie zum Beispiel bemerkt hätten, ist, dass die Sterne in der Mitte gruppiert sind. Um dies zu beheben, müssten Sie eine gute Formel entwickeln, die alle Muster beseitigt. Alternativ können Sie eine Reihe von Formeln erstellen und mithilfe eines Zählers zwischen diesen wechseln. Die Formeln, die wir verwendeten, waren diese:
1 |
|
2 |
//StarfieldSequence class
|
3 |
|
4 |
private int getNext(){ |
5 |
if (count == 0){ |
6 |
if (s_start > 0 && s_end > 0){ |
7 |
count++; |
8 |
return (int) (Math.pow(s_start*s_end, 2) / (Math.pow(s_start, 1)+s_end) |
9 |
+ Math.round(Math.abs(Math.cos(0.0175f * s_end)))); |
10 |
} else { |
11 |
count++; |
12 |
return (int) (Math.pow((s_end + s_start), 4) / |
13 |
Math.pow((s_end + s_start), 2) + |
14 |
Math.round(Math.abs(Math.cos(0.0175f * s_end))) + |
15 |
Math.cos(s_end) + Math.cos(s_start)); |
16 |
}
|
17 |
} else { |
18 |
if (s_start > 0 && s_end > 0){ |
19 |
count--; |
20 |
return (int) (Math.pow((s_end + s_start), 2) + |
21 |
Math.round(Math.abs(Math.cos(0.0175f * s_end)))); |
22 |
} else { |
23 |
count--; |
24 |
return (int) (Math.pow((s_end + s_start), 2) + |
25 |
Math.round(Math.abs(Math.cos(0.0175f * s_end))) + |
26 |
Math.cos(s_end) + Math.cos(s_start)); |
27 |
}
|
28 |
}
|
29 |
}
|
Es gibt noch eine einfache Verbesserung, die wir implementieren können. Wenn Sie in den Himmel schauen, sehen Sie ein paar große und viele weitere kleine Sterne. In unserem Fall entspricht die Anzahl der kleinen Sterne jedoch ungefähr der Anzahl der großen Sterne. Um dies zu beheben, müssen wir nur zur Methode getSize() in der Star-Klasse zurückkehren. Nachdem wir die Größe auf einen Bruchteil von eins gesetzt haben, müssen wir diese Zahl auf die Potenz einer ganzen Zahl erhöhen - zum Beispiel vier oder fünf.
1 |
|
2 |
//Star class
|
3 |
|
4 |
public double getSize(){ |
5 |
return (double) (Math.pow((double)s_size/1000, 4)); |
6 |
}
|
Wenn Sie das Programm ein letztes Mal ausführen, erhalten Sie ein zufriedenstellendes Ergebnis.



Abschluss
In diesem Fall haben wir einen Sequenzgenerator verwendet, um prozedural einen Hintergrund zu generieren. Ein Sequenzgenerator wie dieser könnte viel mehr Verwendungszwecke haben - zum Beispiel könnte dem Stern eine Z-Koordinate hinzugefügt werden, sodass er anstelle des Zeichnens eines Bildes Sterne als Objekte in einer 3D-Umgebung erzeugen könnte.



