Codifica di un generatore di sequenze personalizzato per il rendering di Starscape

Nel mio precedente articolo ho spiegato la differenza tra un generatore di numeri pseudocasuali e un generatore di sequenze e ho esaminato i vantaggi che un generatore di sequenze ha su un PRNG. In questo tutorial codificheremo un generatore di sequenze abbastanza semplice. Genera una stringa di numeri, manipola e interpreta questa sequenza e quindi la usa per disegnare un semplice starscape.

Nota: Sebbene questo tutorial sia scritto usando Java, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Creazione e inizializzazione dell'immagine

La prima cosa che dobbiamo fare è creare l'immagine. Per questo generatore di sequenze, creeremo un'immagine 1000 × 1000px per mantenere la generazione dei numeri il più semplice possibile. Diverse lingue lo fanno in modo diverso, quindi utilizza il codice necessario per la tua piattaforma di sviluppo.

Quando hai creato correttamente l'immagine, è il momento di dargli un colore di sfondo. Dato che stiamo parlando di un cielo stellato, sarebbe più sensato iniziare con un nero (# 000000) sfondo e quindi aggiungere le stelle bianche, piuttosto che il contrario.


Creare un profilo stellare e un campo stellare

Prima di iniziare a lavorare sul generatore di sequenze, dovresti capire dove vuoi andare. Ciò significa sapere cosa si desidera creare e come diversi semi e numeri possono variare ciò che si desidera creare, in questo caso le stelle.

Per fare ciò abbiamo bisogno di creare un profilo stellare campione che conterrà variabili di classe che indicano alcune proprietà delle stelle. Per semplificare le cose, inizieremo con solo tre attributi:

  • ascissa
  • coordinata y
  • taglia

Ciascuno dei tre attributi avrà valori compresi tra 0 e 999, il che significa che a ciascun attributo saranno assegnate tre cifre. Tutto questo verrà memorizzato in a Stella classe.

Due importanti metodi nel Stella classe sono getSize () e getRadiusPx (). Il getSize () metodo restituisce le dimensioni della stella, ridotte a un numero decimale compreso tra zero e uno, e il valore getRadiusPx () metodo restituisce quanto grande deve essere il raggio della stella nell'immagine finale.

Ho trovato che 4 pixel fanno un buon raggio massimo nella mia demo, quindi getRadiusPx () restituirà semplicemente il valore di getSize () moltiplicato per quattro. Ad esempio, se il getSize () metodo restituisce un raggio di 0.4, il getRadiusPx () il metodo darebbe un raggio di 1.6px.

 // Star classe private int s_x, s_y, s_size; public Star (int x, int y, int size) // Costruttore che imposta gli attributi iniziali s_x = x; s_y = y; s_size = size;  public int getX () // Restituisce la coordinata x della stella return s_x;  public int getY () // Restituisce la coordinata y della stella return s_y;  public double getSize () // Restituisce il raggio della stella come numero decimale tra 0 e 1 return (double) (s_size / 1000);  public double getRadiusPx () // Restituisce il raggio della stella in pixel return (double) 4 * getSize (); // 4px è il raggio più grande che una stella può avere

Dovremmo anche fare una classe molto semplice il cui compito è quello di tenere traccia di tutte le stelle in ogni sequenza di stelle. Il starfield la classe consiste solo di metodi che aggiungono, rimuovono o recuperano le stelle da un Lista di array. Dovrebbe anche essere in grado di restituire il Lista di array.

 // ArrayList classe privata Starfield s_stars = new ArrayList (); public void addStar (Star s) // Un metodo che aggiunge una stella a un ArrayList s_stars.add (s);  public void removeStar (Star s) // Un metodo che rimuove una stella da un ArrayList s_stars.remove (s);  public Star getStar (int i) // Un metodo che recupera una stella con indice i da ArrayList return (Star) getStarfield (). get (i);  public ArrayList getStarfield () // Un metodo che restituisce ArrayList che memorizza tutte le stelle restituiscono s_stars; 

Pianificazione del generatore di sequenze

Ora che abbiamo completato il profilo stella e inizializzato l'immagine, conosciamo alcuni punti importanti sul generatore di sequenze che vogliamo creare.

Prima di tutto, sappiamo che la larghezza e l'altezza dell'immagine sono 1000 px. Ciò significa che, per sfruttare le risorse disponibili, l'intervallo di coordinate x e y deve essere compreso nell'intervallo 0-999. Poiché due dei numeri richiesti rientrano nello stesso intervallo, possiamo applicare lo stesso intervallo alle dimensioni della stella per mantenere l'uniformità. La dimensione verrà quindi ridimensionata più tardi quando interpretiamo la serie di numeri.

Utilizzeremo un certo numero di variabili di classe. Questi includono: s_seed, un singolo intero che definisce l'intera sequenza; s_start e inviare, due numeri interi generati dividendo il seme in due; e s_current, un numero intero che contiene il numero generato più di recente nella sequenza.


Vedi questa immagine dal mio precedente articolo. 1234 è il seme, e 12 e 34 sono i valori iniziali di s_start e inviare. Mancia: Si noti che ogni numero generato proviene dal seme; non c'è nessuna chiamata a casuale(). Ciò significa che lo stesso seme genererà sempre lo stesso panorama.

Useremo anche s_sequence, un Stringa che manterrà la sequenza generale. Le ultime due variabili di classe sono s_image (di tipo Immagine - una lezione che creeremo in seguito) e s_starfield (di tipo starfield, la classe che abbiamo appena creato). Il primo memorizza l'immagine, mentre il secondo contiene lo starfield.

Il percorso che prenderemo per creare questo generatore è abbastanza semplice. Per prima cosa, dobbiamo creare un costruttore che accetti un seme. Quando ciò è fatto, dobbiamo creare un metodo che accetti un numero intero che rappresenta il numero di stelle che deve creare. Questo metodo dovrebbe quindi chiamare il generatore effettivo per ottenere i numeri. E ora inizia il vero lavoro ... creando il generatore di sequenze.


Coding the Sequence Generator

La prima cosa che un generatore di sequenze deve fare è accettare un seme. Come accennato, divideremo il seme in due: le prime due cifre e le ultime due cifre. Per questo motivo, dobbiamo verificare se il seme ha quattro cifre e riempirlo con zeri se non lo fa. Fatto questo, possiamo dividere la stringa seme in due variabili: s_start e inviare. (Nota che i semi stessi non faranno parte della sequenza effettiva.)

 // StarfieldSequence class public StarfieldSequence (int seed) // Un costruttore che accetta un seed e lo divide in due String s_seedTemp; s_starfield = new Starfield (); // Inizializza Starfield s_seed = seed; // Memorizza il seme in una stringa in modo da poterlo dividere facilmente // Aggiungi zeri alla stringa se il seme non ha quattro cifre se (seme < 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)); 

Così:

  • seme = 1234 si intende s_start = 12 e s_end = 34
  • seme = 7 si intende s_start = 00 e s_end = 07
  • seme = 303 si intende s_start = 03 e s_end = 03

Next in line: crea un altro metodo che, dati i due numeri, genera il numero successivo nella sequenza.

Trovare la formula giusta è un processo stanco. Di solito significa ore di lavoro di prova ed errore che cercano di trovare una sequenza che non coinvolga troppi modelli nell'immagine risultante. Quindi, sarebbe più saggio trovare la migliore formula una volta che possiamo effettivamente vedere l'immagine, piuttosto che ora. Ciò che ci interessa in questo momento è trovare una formula che generi una sequenza più o meno casuale. Per questo motivo, useremo la stessa formula usata nella sequenza di Fibonacci: aggiunta dei due numeri.

 // StarfieldSequence class private int getNext () // Un metodo che restituisce il numero successivo nella sequenza return (s_start + s_end); 

Quando questo è fatto, possiamo ora andare avanti e iniziare a creare la sequenza. In un altro metodo, manipoleremo il seed iniziale per generare un intero flusso di numeri, che può quindi essere interpretato come gli attributi del profilo stella.

Sappiamo che per una data stella abbiamo bisogno di nove cifre: le prime tre definiscono la coordinata x, la terza tre definiscono la coordinata y e le ultime tre definiscono la dimensione. Pertanto, come nel caso dell'alimentazione del seme, per mantenere l'uniformità è importante assicurarsi che ogni numero generato abbia tre cifre. In questo caso, dobbiamo anche troncare il numero se è maggiore di 999.

Questo è abbastanza simile a quello che abbiamo fatto prima. Abbiamo solo bisogno di memorizzare il numero in una stringa temporanea, Temp, quindi smaltire la prima cifra. Se il numero non ha tre cifre, dovremmo quindi riempirlo con zeri come abbiamo fatto in precedenza.

 // StarfieldSequence class private void fixDigits () String temp = ""; // Se il numero appena generato ha più di tre cifre, prendi solo gli ultimi tre se (s_corrente> 999) temp = Integer.toString (s_current); s_current = Integer.parseInt (temp.substring (1, 4));  // Se il numero appena generato ha meno di tre cifre, aggiungere zeri all'inizio se (s_current < 10) s_sequence += "00";  else if (s_current < 100) s_sequence += "0";  

Con questo, dovremmo ora creare un altro metodo che crea e restituisce un profilo stella ogni volta che generiamo tre numeri. Usando questo metodo, possiamo quindi aggiungere la stella al Lista di array di stelle.

 // StarfieldSequence class private Star getStar (int i) // Un metodo che accetta un intero (la dimensione della sequenza) e restituisce la stella // divide le ultime nove cifre della sequenza in tre (i tre attributi della stella ) 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))); ritorno stella; 

Mettere tutto insieme

Dopo che questo è finito, possiamo assemblare il generatore. Dovrebbe accettare il numero di stelle che deve generare.

Sappiamo già che per una stella abbiamo bisogno di nove cifre, quindi questo generatore deve mantenere il conteggio del numero di caratteri nelle stringhe. Il contatore, s_counter, memorizzerà la lunghezza massima della sequenza. Pertanto moltiplichiamo il numero di stelle per nove e ne rimuoviamo uno poiché a Stringa inizia da zero dell'indice.

Dobbiamo anche tenere il conto del numero di personaggi che abbiamo creato da quando abbiamo generato una stella. Per questo compito, useremo s_starcounter. In un per loop, che si ripeterà fino a quando la lunghezza della serie sarà uguale s_counter, ora possiamo chiamare i metodi che abbiamo creato finora.

Mancia: Non dobbiamo dimenticare di sostituire s_start e inviare, altrimenti continueremo a generare lo stesso numero più e più volte!
 // StarfieldSequence class public void generate (int starnumber) // Genera un numero di stelle come indicato dal numero intero star int s_counter = 9 * starnumber; // s_counter tiene traccia del numero di caratteri che la String deve avere per generare il numero designato di stelle s_counter - = 1; // Rimuove uno poiché inizia una stringa dall'indice 0 int s_starcounter = 0; // s_starcounter tiene traccia del numero di numeri generati per (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) // Se sono stati generati tre numeri dall'ultima stella creata, creane un altro s_starfield.addStar (getStar (s_sequence.length ()));  // Sostituisci s_start e s_end, altrimenti continuerai a generare lo stesso numero più e più volte! s_start = s_end; s_end = s_current; 

Disegno di stelle

Ora che la parte difficile è finita, è finalmente il momento di passare al Immagine classe, e inizia a disegnare stelle.

In un metodo che accetta a starfield, per prima cosa creiamo un'istanza di a Colore, quindi recupera il numero di stelle che dobbiamo disegnare. In un per loop, disegneremo tutte le stelle. Dopo aver fatto una copia della stella attuale, è importante recuperare il raggio della stella. Dato che il numero di pixel è un numero intero, dovremmo aggiungere al raggio per renderlo un numero intero.

Per disegnare la stella, useremo un gradiente radiale.

Un esempio di un gradiente radiale

L'opacità di un gradiente radiale dipende dalla distanza di un pixel dal centro. Il centro del cerchio avrà le coordinate (0,0). Usando la convenzione più comune, qualsiasi pixel a sinistra del centro ha una coordinata x negativa, e qualsiasi pixel sotto ha una coordinata y negativa.

Per questo motivo, l'annidato per i cicli iniziano con un numero negativo. Usando il teorema di Pitagora calcoliamo la distanza dal centro del cerchio e la usiamo per recuperare l'opacità. Per le stelle che hanno il raggio più piccolo possibile (1px), la loro opacità dipende esclusivamente dalla loro dimensione.

 // Classe pubblica public draw (Starfield starfield) Colore colore; per (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     

Per concludere, dobbiamo creare un metodo che accetti un Stringa e lo usa per salvare l'immagine con quel nome di file. Nel generatore, dovremmo creare prima l'immagine. Quindi, dovremmo chiamare questi ultimi due metodi dal generatore di sequenze.

 // StarfieldSequence class public void generate (int starnumber) s_image.createImage (); // Crea l'immagine 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); // Disegna lo starfield s_image.save ("starfield"); // Salva l'immagine con il nome 'starfield'

Nel Principale classe, dovremmo creare un'istanza del generatore di sequenze, assegnargli un seme e ottenere un buon numero di stelle (400 dovrebbe essere sufficiente). Prova a eseguire il programma, correggi eventuali errori e controlla il percorso di destinazione per vedere quale immagine è stata creata.

L'immagine risultante con un seme di 1234

miglioramenti

Ci sono ancora alcuni cambiamenti che possiamo apportare. Ad esempio, la prima cosa che avresti notato è che le stelle sono raggruppate al centro. Per risolvere il problema, dovresti trovare una buona formula che elimina qualsiasi schema. In alternativa è possibile creare un numero di formule e alternare tra loro utilizzando un contatore. Le formule che abbiamo usato erano queste:

 // 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 )); 

C'è un altro semplice miglioramento che possiamo implementare. Se guardi il cielo, vedrai alcune grandi stelle e molte più piccole. Tuttavia, nel nostro caso il numero di piccole stelle è all'incirca uguale al numero di grandi stelle. Per risolvere questo problema, dobbiamo solo tornare al getSize () metodo nel Stella classe. Dopo aver ridotto la dimensione di una unità, dobbiamo aumentare questo numero alla potenza di un numero intero, ad esempio quattro o cinque.

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

Eseguire il programma un'ultima volta dovrebbe dare un risultato soddisfacente.

Il risultato finale: un intero scenario stellare generato dal nostro Sequence Generator!

Conclusione

In questo caso, abbiamo usato un generatore di sequenze per generare in modo procedurale uno sfondo. Un generatore di sequenze come questo potrebbe avere molti altri usi - ad esempio, una coordinata z potrebbe essere aggiunta alla stella in modo che invece di disegnare un'immagine, si potrebbero generare stelle come oggetti in un ambiente 3D.