Codage d'un générateur de séquence personnalisé pour restituer un paysage Starscape

Dans mon article précédent, j'avais expliqué la différence entre un générateur de nombres pseudo-aléatoires et un générateur de séquence et examiné les avantages d'un générateur de séquence par rapport à un PRNG. Dans ce tutoriel, nous allons coder un générateur de séquence assez simple. Il génère une chaîne de nombres, manipule et interprète cette séquence, puis l'utilise pour dessiner un paysage d'étoiles très simple..

Remarque: Bien que ce tutoriel ait été écrit en Java, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


Création et initialisation de l'image

La première chose à faire est de créer l'image. Pour ce générateur de séquence, nous allons créer une image de 1 000 × 1 000 pixels pour que la génération de nombres soit aussi simple que possible. Différentes langues font cela différemment, utilisez donc le code nécessaire pour votre plateforme de développement..

Une fois l’image créée, il est temps de lui attribuer une couleur de fond. Puisque nous parlons d’un ciel étoilé, il serait plus judicieux de commencer par un noir (# 000000) en arrière-plan et ensuite ajouter les étoiles blanches, plutôt que l'inverse.


Faire un profil et un champ d'étoiles

Avant de commencer à travailler sur le générateur de séquence, vous devez déterminer où vous voulez vous diriger. Cela signifie que vous devez savoir ce que vous voulez créer, et en quoi différentes graines et nombres varient en fonction de ce que vous voulez créer - dans ce cas, les étoiles.

Pour ce faire, nous devons créer un exemple de profil d’étoile qui contiendra des variables de classe indiquant certaines propriétés des étoiles. Pour garder les choses simples, nous allons commencer avec seulement trois attributs:

  • coordonnée x
  • y-coordonnée
  • Taille

Chacun des trois attributs aura des valeurs allant de 0 à 999, ce qui signifie que chaque attribut aura trois chiffres qui lui sont attribués. Tout cela sera stocké dans un Étoile classe.

Deux méthodes importantes dans le Étoile la classe sont getSize () et getRadiusPx (). le getSize () La méthode retourne la taille de l'étoile, réduite à un nombre décimal compris entre zéro et un, et le getRadiusPx () la méthode retourne la taille du rayon de l'étoile dans l'image finale.

J'ai trouvé que 4 pixels permettaient un bon rayon maximum dans ma démo, donc getRadiusPx () retournera simplement la valeur de getSize () multiplié par quatre. Par exemple, si le getSize () la méthode renvoie un rayon de 0,4, le getRadiusPx () la méthode donnerait un rayon de 1.6px.

 // Star class private int s_x, s_y, s_size; public Star (int x, int y, int taille) // Constructeur qui définit les attributs initiaux s_x = x; s_y = y; s_size = taille;  public int getX () // Retourne la coordonnée x de l'étoile return s_x;  public int getY () // Retourne la coordonnée y de l'étoile return s_y;  public double getSize () // Renvoie le rayon de l'étoile sous forme de nombre décimal compris entre 0 et 1 return (double) (s_size / 1000);  public double getRadiusPx () // Retourne le rayon de l'étoile en pixels return (double) 4 * getSize (); // 4px est le plus grand rayon qu'une étoile puisse avoir

Nous devrions également faire une classe très simple dont le travail est de garder une trace de toutes les étoiles dans chaque séquence d'étoiles. le Starfield classe consiste simplement en des méthodes qui ajoutent, suppriment ou récupèrent des étoiles d’un ArrayList. Il devrait également être en mesure de retourner le ArrayList.

 // Classe Starfield private ArrayList s_stars = new ArrayList (); public void addStar (Star s) // Une méthode qui ajoute une étoile à un ArrayList s_stars.add (s);  public void removeStar (Star s) // Une méthode qui supprime une étoile d'un ArrayList s_stars.remove (s);  public Star getStar (int i) // Méthode permettant de récupérer une étoile d'indice i à partir d'un retour ArrayList (Star) getStarfield (). get (i);  public ArrayList getStarfield () // Une méthode qui retourne l'ArrayList stockant toutes les étoiles return s_stars; 

Planification du générateur de séquence

Maintenant que nous avons terminé le profil d'étoile et initialisé l'image, nous connaissons certains points importants concernant le générateur de séquence que nous souhaitons créer..

Tout d’abord, nous savons que la largeur et la hauteur de l’image sont de 1000 pixels. Cela signifie que, pour exploiter les ressources disponibles, la plage des coordonnées x et y doit être comprise entre 0 et 999. Étant donné que deux des nombres requis se situent dans la même plage, nous pouvons appliquer la même plage à la taille de l'étoile pour conserver l'uniformité. La taille sera ensuite réduite plus tard lorsque nous interprétons la série de nombres.

Nous allons utiliser un certain nombre de variables de classe. Ceux-ci inclus: s_seed, un entier unique qui définit la séquence entière; s_start et envoyer, deux entiers générés en scindant la graine en deux; et s_current, un entier contenant le dernier numéro généré dans la séquence.


Voir cette image de mon article précédent. 1234 est la graine, et 12 et 34 sont les valeurs initiales de s_start et envoyer. Pointe: Notez que chaque nombre généré provient de la graine; il n'y a pas d'appel à au hasard(). Cela signifie que la même graine générera toujours le même ciel étoilé.

Nous allons également utiliser s_sequence, une Chaîne qui tiendra la séquence globale. Les deux dernières variables de classe sont s_image (de type Image - une classe que nous créerons plus tard) et s_starfield (de type Starfield, la classe que nous venons de créer). Le premier stocke l'image, tandis que le second contient le champ étoile.

Le chemin que nous allons prendre pour créer ce générateur est assez simple. Premièrement, nous devons créer un constructeur qui accepte une graine. Lorsque cela est fait, nous devons créer une méthode qui accepte un entier représentant le nombre d’étoiles qu’elle doit créer. Cette méthode devrait alors appeler le générateur actuel pour obtenir les chiffres. Et commence maintenant le vrai travail… créer le générateur de séquence.


Codage du générateur de séquence

La première chose qu’un générateur de séquence doit faire est d’accepter une graine. Comme mentionné, nous allons diviser la graine en deux: les deux premiers chiffres et les deux derniers chiffres. Pour cette raison, nous devons vérifier si la graine a quatre chiffres et la compléter avec des zéros si ce n’est pas le cas. Lorsque cela est fait, nous pouvons alors scinder la chaîne de base en deux variables: s_start et envoyer. (Notez que les graines elles-mêmes ne feront pas partie de la séquence réelle.)

 // StarfieldSequence class public StarfieldSequence (int seed) // Un constructeur qui accepte une graine et la scinde en deux String s_seedTemp; s_starfield = new Starfield (); // Initialise le Starfield s_seed = seed; // Stocke la graine dans une chaîne pour pouvoir la scinder facilement // Ajoute des zéros à la chaîne si la graine n'a pas quatre chiffres if (seed < 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)); 

Alors:

  • graine = 1234 veux dire s_start = 12 et s_end = 34
  • graine = 7 veux dire s_start = 00 et s_end = 07
  • graine = 303 veux dire s_start = 03 et s_end = 03

Next in line: crée une autre méthode qui, à partir des deux nombres, génère le prochain numéro de la séquence..

Trouver la bonne formule est un processus fatigant. Cela signifie généralement des heures d'essais et d'essais pour trouver une séquence qui n'implique pas trop de motifs dans l'image résultante. Par conséquent, il serait plus sage de trouver la meilleure formule une fois que nous pourrons réellement voir l'image, plutôt que maintenant. Ce qui nous intéresse actuellement, c’est de trouver une formule qui génère une séquence plus ou moins aléatoire. Pour cette raison, nous allons utiliser la même formule que celle utilisée dans la séquence de Fibonacci: addition des deux nombres.

 // Classe StarfieldSequence private int getNext () // Une méthode qui retourne le prochain numéro de la séquence return (s_start + s_end); 

Ceci fait, nous pouvons maintenant passer à la création de la séquence. Dans une autre méthode, nous allons manipuler le germe initial pour générer tout un flux de nombres, qui peuvent ensuite être interprétés comme les attributs du profil d'étoile..

Nous savons que pour une étoile donnée, nous avons besoin de neuf chiffres: les trois premiers définissent la coordonnée x, les trois du milieu définissent la coordonnée y et les trois derniers définissent la taille. Par conséquent, comme dans le cas de l’alimentation de la graine, il est important de veiller à ce que chaque numéro généré comporte trois chiffres. Dans ce cas, nous devons également tronquer le nombre s'il est supérieur à 999..

Ceci est assez similaire à ce que nous avons fait auparavant. Nous avons juste besoin de stocker le numéro dans une chaîne temporaire, temp, puis disposer du premier chiffre. Si le nombre n'a pas trois chiffres, nous devrions le compléter avec des zéros comme nous l'avons fait précédemment.

 // Void privé de la classe StarfieldSequence fixDigits () String temp = ""; // Si le numéro nouvellement généré comporte plus de trois chiffres, ne prenez que les trois derniers si (s_current> 999) temp = Integer.toString (s_current); s_current = Integer.parseInt (temp.sous chaîne (1, 4));  // Si le numéro nouvellement généré comporte moins de trois chiffres, ajoutez des zéros au début de (if < 10) s_sequence += "00";  else if (s_current < 100) s_sequence += "0";  

Cela étant fait, nous devrions maintenant créer une autre méthode qui crée et renvoie un profil en étoile chaque fois que nous générons trois nombres. En utilisant cette méthode, on peut ensuite ajouter l’étoile à la ArrayList des étoiles.

 // StarfieldSequence class private Star getStar (int i) // Une méthode qui accepte un entier (la taille de la séquence) et retourne l'étoile // divise les neuf derniers chiffres de la séquence en trois (les trois attributs de l'étoile ) Star Star = nouvelle 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))); étoile de retour; 

Mettre tous ensemble

Après cela, nous pouvons assembler le générateur. Il devrait accepter le nombre d'étoiles qu'il doit générer.

Nous savons déjà que pour une étoile, nous avons besoin de neuf chiffres. Ce générateur doit donc compter le nombre de caractères dans les chaînes. Le compteur, s_counter, stockera la longueur maximale de la séquence. Par conséquent, nous multiplions le nombre d'étoiles par neuf et en retirons une depuis un Chaîne commence à partir de zéro.

Nous devons également compter le nombre de caractères que nous avons créés depuis notre dernière génération. Pour cette tâche, nous allons utiliser s_starcounter. Dans un pour boucle, qui se répète jusqu'à ce que la longueur de la série soit égale à s_counter, nous pouvons maintenant appeler les méthodes que nous avons créées jusqu'à présent.

Pointe: Nous ne devons pas oublier de remplacer s_start et envoyer, ou bien nous continuerons à générer le même nombre encore et encore!
 // Classe StarfieldSequence public void generate (int starnumber) // Génère un nombre d'étoiles comme indiqué par le nombre entier starnumber int s_counter = 9 * starnumber; // s_counter enregistre le nombre de caractères que la chaîne doit avoir pour générer le nombre d'étoiles désigné s_counter - = 1; // Supprimer un depuis qu'une chaîne commence à partir de l'index 0 int s_starcounter = 0; // s_starcounter enregistre le nombre de nombres générés pour (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) // Si trois nombres ont été générés depuis la création de la dernière étoile, créez-en un autre s_starfield.addStar (getStar (s_sequence.length ()));  // Remplacez s_start et s_end, sinon vous continuerez à générer le même nombre encore et encore! s_start = s_end; s_end = s_current; 

Dessin d'étoiles

Maintenant que la partie difficile est terminée, il est enfin temps de passer à la Image classe et commence à dessiner des étoiles.

Dans une méthode qui accepte un Starfield, nous créons d'abord une instance d'un Couleur, récupérez ensuite le nombre d'étoiles que nous devons dessiner. Dans un pour boucle, nous allons dessiner toutes les étoiles. Après avoir copié l'étoile actuelle, il est important de récupérer le rayon de l'étoile. Étant donné que le nombre de pixels est un entier, il faut ajouter le rayon pour en faire un entier..

Pour dessiner l'étoile, nous allons utiliser un dégradé radial.

Un exemple de dégradé radial

L'opacité d'un dégradé radial dépend de la distance d'un pixel au centre. Le centre du cercle aura des coordonnées (0,0). En utilisant la convention la plus courante, tout pixel situé à gauche du centre a une coordonnée x négative et tout pixel situé en dessous a une coordonnée y négative..

Pour cette raison, le niché pour Les boucles commencent par un nombre négatif. En utilisant le théorème de Pythagore, nous calculons la distance par rapport au centre du cercle et l'utilisons pour récupérer l'opacité. Pour les étoiles ayant le plus petit rayon possible (1px), leur opacité dépend uniquement de leur taille.

 // Image publique public nul draw (Starfield starfield) Color color; pour (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     

Pour conclure, nous devons créer une méthode qui accepte une Chaîne et l'utilise pour enregistrer l'image avec ce nom de fichier. Dans le générateur, nous devrions d'abord créer l'image. Ensuite, nous devrions appeler ces deux dernières méthodes à partir du générateur de séquence.

 // classe StarfieldSequence public void generer (int nombre-étoile) s_image.createImage (); // Crée l'image int s_counter = 9 * starnumber; s_counter - = 1; int s_starcounter = 0; pour (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); // Dessine le starfield s_image.save ("starfield"); // Enregistrer l'image avec le nom 'starfield'

dans le Principale classe, nous devrions créer une instance du générateur de séquence, lui attribuer un germe et obtenir un bon nombre d’étoiles (400 devraient suffire). Essayez d'exécuter le programme, de réparer les erreurs et de vérifier le chemin de destination pour voir quelle image a été créée..

L'image résultante avec une graine de 1234

Améliorations

Nous pouvons encore apporter certains changements. Par exemple, la première chose que vous auriez remarquée, c’est que les étoiles sont regroupées au centre. Pour résoudre ce problème, vous devez trouver une bonne formule qui élimine tous les schémas. Vous pouvez également créer un certain nombre de formules et les utiliser à l’aide d’un compteur. Les formules que nous avons utilisées étaient les suivantes:

 // StarfieldSequence class private int getNext () if (nombre == 0) if (s_start> 0 && s_end> 0) nombre ++; 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 )); 

Il y a une autre amélioration simple que nous pouvons mettre en œuvre. Si vous regardez le ciel, vous verrez quelques grandes étoiles et beaucoup d'autres petites. Cependant, dans notre cas, le nombre de petites étoiles est à peu près le même que le nombre de grandes étoiles. Pour résoudre ce problème, il suffit de revenir à la getSize () méthode dans le Étoile classe. Après avoir fait la taille d'une fraction de un, nous devons augmenter ce nombre à la puissance d'un entier - par exemple quatre ou cinq.

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

Exécuter le programme une dernière fois devrait vous donner un résultat satisfaisant.

Le résultat final - un paysage étoilé entier généré de manière procédurale par notre générateur de séquence!

Conclusion

Dans ce cas, nous avons utilisé un générateur de séquence pour générer de manière procédurale un arrière-plan. Un générateur de séquence comme celui-ci pourrait avoir de nombreuses autres utilisations. Par exemple, une coordonnée z pourrait être ajoutée à l'étoile de telle sorte qu'au lieu de dessiner une image, il pourrait générer des étoiles sous forme d'objets dans un environnement 3D..