Créer la vie le jeu de la vie de Conway

Parfois, même un simple ensemble de règles de base peut vous donner des résultats très intéressants. Dans ce didacticiel, nous allons construire le moteur de base du jeu de la vie de Conway..

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


introduction

Le jeu de la vie de Conway est un automate cellulaire mis au point dans les années 1970 par un mathématicien britannique nommé John Conway..

Avec une grille bidimensionnelle de cellules, avec certaines "allumées" ou "vivantes" et d'autres "éteintes" ou "mortes", et un ensemble de règles qui régissent la manière dont elles s'animent ou meurent, nous pouvons avoir une "forme de vie intéressante" "se dérouler juste devant nous. Ainsi, en dessinant simplement quelques motifs sur notre grille, puis en démarrant la simulation, nous pouvons voir les formes de vie de base évoluer, se répandre, disparaître et finalement se stabiliser. Téléchargez les fichiers sources finaux ou consultez la démo ci-dessous:

Or, ce "jeu de la vie" n’est pas strictement un "jeu" - c’est plus une machine, principalement parce qu’il n’ya ni joueur ni but, il évolue simplement en fonction de ses conditions initiales. Néanmoins, il est très amusant de jouer avec, et il existe de nombreux principes de conception de jeu qui peuvent être appliqués à sa création. Alors, sans plus tarder, commençons!

Pour ce tutoriel, j’ai tout construit en XNA car c’est ce qui me convient le mieux. (Si cela vous intéresse, il existe un guide pour vous familiariser avec XNA.) Cependant, vous devriez pouvoir suivre n'importe quel environnement de développement de jeux en 2D que vous connaissez bien..


Créer les cellules

L’élément le plus fondamental du jeu de la vie de Conway est le cellules, qui sont les "formes de vie" qui forment la base de la simulation entière. Chaque cellule peut être dans l'un des deux états: "vivant" ou "mort". Par souci de cohérence, nous nous en tiendrons à ces deux noms pour les états des cellules pour la suite de ce didacticiel..

Les cellules ne bougent pas, elles affectent simplement leurs voisins en fonction de leur état actuel.

Maintenant, en termes de programmation de leurs fonctionnalités, il y a trois comportements que nous devons leur donner:

  1. Ils doivent suivre leur position, leurs limites et leur état pour pouvoir être cliqués et dessinés correctement..
  2. Ils doivent basculer entre vivants et morts lorsque l'utilisateur clique dessus, ce qui permet à l'utilisateur de réaliser des choses intéressantes..
  3. Ils doivent être dessinés en blanc ou en noir s'ils sont morts ou vivants, respectivement.

Tout ce qui précède peut être accompli en créant un Cellule classe, qui contiendra le code ci-dessous:

classe Cell public Point Position get; ensemble privé;  public Rectangle Bounds get; ensemble privé;  public bool IsAlive get; ensemble;  public Cell (Position du point) Position = position; Bounds = new Rectangle (Position.X * Game1.CellSize, Position.Y * Game1.CellSize, Game1.CellSize, Game1.CellSize); IsAlive = false;  public void Update (MouseState mouseState) if (Bounds.Contains (nouveau Point (mouseState.X, mouseState.Y)))) // Donne vie aux cellules avec un clic gauche ou tue-les avec un clic droit. if (mouseState.LeftButton == ButtonState.Pressed) IsAlive = true; else if (mouseState.RightButton == ButtonState.Pressed) IsAlive = false;  public void Draw (SpriteBatch spriteBatch) if (IsAlive) spriteBatch.Draw (Game1.Pixel, Bounds, Color.Black); // Ne dessine rien s'il est mort, car la couleur d'arrière-plan par défaut est le blanc. 

La grille et ses règles

Maintenant que chaque cellule va se comporter correctement, nous devons créer une grille qui les tiendra toutes et mettre en œuvre la logique qui indique à chacune si elle doit devenir vivante, rester en vie, mourir ou rester morte (pas de zombies!)..

Les règles sont assez simples:

  1. Toute cellule vivante avec moins de deux voisins vivants décède, comme si elle était causée par une sous-population.
  2. Toute cellule vivante avec deux ou trois voisins vivants vit dans la génération suivante.
  3. Toute cellule vivante ayant plus de trois voisins vivants décède, comme si elle était surpeuplée.
  4. Toute cellule morte ayant exactement trois voisins vivants devient une cellule vivante, comme par reproduction.

Voici un guide visuel rapide de ces règles dans l'image ci-dessous. Chaque cellule mise en évidence par une flèche bleue sera affectée par la règle numérotée correspondante ci-dessus. En d'autres termes, la cellule 1 mourra, la cellule 2 restera en vie, la cellule 3 mourra et la cellule 4 s'animera..

Ainsi, alors que la simulation de jeu exécute une mise à jour à des intervalles de temps constants, la grille vérifie chacune de ces règles pour toutes les cellules de la grille. Cela peut être accompli en mettant le code suivant dans une nouvelle classe que j'appellerai la grille:

class Grid taille de point publique get; ensemble privé;  cellules privées [,] cellules; grille publique () Size = new Point (Game1.CellsX, Game1.CellsY); cells = new Cell [Size.X, Size.Y]; pour (int i = 0; i < Size.X; i++) for (int j = 0; j < Size.Y; j++) cells[i, j] = new Cell(new Point(i, j));  public void Update(GameTime gameTime)  (… ) // Loop through every cell on the grid. for (int i = 0; i < Size.X; i++)  for (int j = 0; j < Size.Y; j++)  // Check the cell's current state, and count its living neighbors. bool living = cells[i, j].IsAlive; int count = GetLivingNeighbors(i, j); bool result = false; // Apply the rules and set the next state. if (living && count < 2) result = false; if (living && (count == 2 || count == 3)) result = true; if (living && count > 3) résultat = faux; if (! living && count == 3) resultat = true; cellules [i, j] .IsAlive = résultat;  (…)

La seule chose qui nous manque ici est la magie GetLivingNeothers méthode, qui compte simplement combien de voisins de la cellule actuelle sont actuellement en vie. Alors, ajoutons cette méthode à notre la grille classe:

public int GetLivingNeothers (int x, int y) int count = 0; // Vérifier la cellule à droite. if (x! = Size.X - 1) if (cellules [x + 1, y] .IsAlive) comptent ++; // Vérifier la cellule en bas à droite. if (x! = Size.X - 1 && y! = Size.Y - 1) if (cellules [x + 1, y + 1] .IsAlive) comptent ++; // Vérifier la cellule en bas. if (y! = Size.Y - 1) if (cellules [x, y + 1] .IsAlive) comptent ++; // Vérifier la cellule en bas à gauche. if (x! = 0 && y! = Size.Y - 1) if (cellules [x - 1, y + 1] .IsAlive) comptent ++; // Vérifier la cellule à gauche. if (x! = 0) if (cellules [x - 1, y] .IsAlive) comptent ++; // Vérifier la cellule en haut à gauche. if (x! = 0 && y! = 0) if (cellules [x - 1, y - 1] .IsAlive) comptent ++; // Vérifier la cellule en haut. if (y! = 0) if (cellules [x, y - 1] .IsAlive) comptent ++; // Vérifier la cellule en haut à droite. if (x! = Size.X - 1 && y! = 0) if (cellules [x + 1, y - 1] .IsAlive) comptent ++; compte de retour; 

Notez que dans le code ci-dessus, le premier si La déclaration de chaque paire vérifie simplement que nous ne sommes pas au bord de la grille. Si nous n'avions pas cette vérification, nous aurions plusieurs exceptions pour dépasser les limites du tableau. En outre, puisque cela conduira à compter ne jamais être incrémentés lorsque nous vérifions au-delà des bords, cela signifie que le jeu "suppose" que les bords sont morts, c'est donc équivalent à avoir une bordure permanente de cellules blanches et mortes autour de nos fenêtres de jeu.


Mise à jour de la grille par intervalles de temps discrets

Jusqu'à présent, toute la logique que nous avons mise en œuvre est solide, mais elle ne se comportera pas correctement si nous ne veillons pas à ce que notre simulation s'exécute par incréments de temps distincts. Ceci est juste une façon élégante de dire que toutes nos cellules seront mises à jour exactement au même moment, dans un souci de cohérence. Si nous n'implémentions pas cela, nous aurions un comportement étrange car l'ordre dans lequel les cellules seraient vérifiées importerait. Ainsi, les règles strictes que nous venons de définir s'effondreraient et un mini-chaos s'ensuivrait..

Par exemple, notre boucle ci-dessus vérifie toutes les cellules de gauche à droite, donc si la cellule de gauche que nous venons de vérifier s'animait, cela modifierait le nombre de cellules au milieu que nous vérifions et pourrait le faire apparaître. . Mais si nous vérifiions de droite à gauche, la cellule de droite pourrait être morte et la cellule de gauche n'est pas encore vivante. Notre cellule du milieu resterait donc morte. C'est mauvais parce que c'est incohérent! Nous devrions pouvoir vérifier les cellules dans n'importe quel ordre aléatoire (comme une spirale!) Et la prochaine étape doit toujours être identique.

Heureusement, c'est très facile à implémenter en code. Tout ce dont nous avons besoin est d’avoir une deuxième grille de cellules en mémoire pour le prochain état de notre système. Chaque fois que nous déterminons le prochain état d'une cellule, nous l'enregistrons dans notre deuxième grille pour l'état suivant de l'ensemble du système. Ensuite, lorsque nous avons trouvé le prochain état de chaque cellule, nous les appliquons tous en même temps. Nous pouvons donc ajouter un tableau 2D de booléens nextCellStates en tant que variable privée, puis ajoutez cette méthode à la la grille classe:

public void SetNextState () pour (int i = 0; i < Size.X; i++) for (int j = 0; j < Size.Y; j++) cells[i, j].IsAlive = nextCellStates[i, j]; 

Enfin, n'oubliez pas de réparer votre Mettre à jour méthode ci-dessus pour assigner le résultat à l'état suivant plutôt qu'à l'état actuel, puis appeler SetNextState à la toute fin du Mettre à jour méthode, juste après la fin des boucles.


Dessiner la grille

Maintenant que nous avons terminé les parties les plus délicates de la logique de la grille, nous devons pouvoir la dessiner à l'écran. La grille dessinera chaque cellule en appelant ses méthodes de dessin une à la fois, de sorte que toutes les cellules vivantes soient noires et les cellules mortes en blanc..

La grille actuelle ne avoir besoin dessiner quoi que ce soit, mais il est beaucoup plus clair du point de vue de l'utilisateur si nous ajoutons des lignes de grille. Cela permet à l’utilisateur de voir plus facilement les limites des cellules et de communiquer une impression d’échelle; créons donc un Dessiner méthode comme suit:

public void Draw (SpriteBatch spriteBatch) foreach (Cellule dans les cellules) cell.Draw (spriteBatch); // Dessine des lignes de grille verticales. pour (int i = 0; i < Size.X; i++) spriteBatch.Draw(Game1.Pixel, new Rectangle(i * Game1.CellSize - 1, 0, 1, Size.Y * Game1.CellSize), Color.DarkGray); // Draw horizontal gridlines. for (int j = 0; j < Size.Y; j++) spriteBatch.Draw(Game1.Pixel, new Rectangle(0, j * Game1.CellSize - 1, Size.X * Game1.CellSize, 1), Color.DarkGray); 

Notez que dans le code ci-dessus, nous prenons un seul pixel et l'étendons pour créer une ligne très longue et fine. Votre moteur de jeu particulier pourrait fournir un simple Dessiner une ligne méthode où vous pouvez spécifier deux points et avoir une ligne en tirant entre eux, ce qui le rendrait encore plus facile que ce qui précède.


Ajout d'une logique de jeu de haut niveau

À ce stade, nous avons tous les éléments de base dont nous avons besoin pour faire fonctionner le jeu, nous devons simplement les réunir. Donc, pour commencer, dans la classe principale de votre jeu (celle qui commence tout), nous devons ajouter quelques constantes telles que les dimensions de la grille et le nombre d'images par seconde (la rapidité avec laquelle elle sera mise à jour), ainsi que toutes les autres choses dont nous avons besoin, comme l'image de pixel unique, la taille de l'écran, etc..

Nous devons également initialiser bon nombre de ces tâches, comme créer la grille, définir la taille de la fenêtre du jeu et s’assurer que la souris est visible pour pouvoir cliquer sur les cellules. Mais toutes ces choses sont spécifiques à un moteur et pas très intéressantes, nous allons donc passer directement à autre chose et passer aux choses intéressantes. (Bien sûr, si vous suivez XNA, vous pouvez télécharger le code source pour obtenir tous les détails.)

Maintenant que tout est configuré et prêt à fonctionner, nous devrions pouvoir lancer le jeu! Mais pas si vite, car il y a un problème: nous ne pouvons rien faire car le jeu est toujours en cours d'exécution. Il est fondamentalement impossible de dessiner des formes spécifiques car elles se briseront à mesure que vous les dessinez. Nous devons donc pouvoir mettre le jeu en pause. Ce serait également bien si nous pouvions nettoyer la grille si elle devenait un gâchis, car nos créations deviendraient souvent incontrôlables et laisseraient un désordre derrière nous..

Alors, ajoutons du code pour mettre le jeu en pause chaque fois que la barre d'espace est enfoncée et effaçons l'écran si vous appuyez sur la touche Retour arrière:

protégé annule la mise à jour (GameTime gameTime) keyboardState = Keyboard.GetState (); if (GamePad.GetState (PlayerIndex.One) .Buttons.Back == ButtonState.Pressed) this.Exit (); // Basculer la pause lorsque la barre d'espace est enfoncée. if (keyboardState.IsKeyDown (Keys.Space) && lastKeyboardState.IsKeyUp (Keys.Space)) Paused =! Paused; // Efface l'écran si le retour arrière est activé. if (keyboardState.IsKeyDown (Keys.Back) && lastKeyboardState.IsKeyUp (Keys.Back)) grid.Clear (); base.Update (gameTime); grid.Update (gameTime); lastKeyboardState = keyboardState; 

Cela aiderait également si nous indiquions très clairement que le jeu était en pause, alors que nous écrivons notre Dessiner méthode, ajoutons du code pour que l'arrière-plan devienne rouge et écrivons "en pause" à l'arrière-plan:

protégé annulation void Draw (GameTime gameTime) if (En pause) GraphicsDevice.Clear (Color.Red); else GraphicsDevice.Clear (Color.White); spriteBatch.Begin (); if (en pause) chaîne en pause = "en pause"; spriteBatch.DrawString (Police, en pause, ScreenSize / 2, Color.Gray, 0f, Font.MeasureString (en pause) / 2, 1f, SpriteEffects.None, 0f);  grid.Draw (spriteBatch); spriteBatch.End (); base.Draw (gameTime); 

C'est tout! Tout devrait maintenant fonctionner, vous pouvez donc y faire un tourbillon, dessiner des formes de vie et voir ce qui se passe! Allez explorer des modèles intéressants que vous pouvez créer en vous référant à nouveau à la page Wikipedia. Vous pouvez également jouer avec le nombre d'images par seconde, la taille de la cellule et les dimensions de la grille pour le modifier à votre guise..


Ajout d'améliorations

À ce stade, le jeu est entièrement fonctionnel et il n’ya pas de honte à l’appeler un jour. Cependant, un inconvénient que vous avez peut-être remarqué est que les clics de votre souris ne s'enregistrent pas toujours lorsque vous essayez de mettre à jour une cellule. Par conséquent, lorsque vous cliquez et faites glisser votre souris sur la grille, une ligne en pointillé reste derrière elle un. Cela est dû au fait que le taux d'actualisation des cellules correspond également au taux de vérification de la souris et qu'il est beaucoup trop lent. Nous devons donc simplement découpler la vitesse à laquelle le jeu se met à jour et la vitesse à laquelle les lectures sont lues..

Commencez par définir le taux de mise à jour et le framerate séparément dans la classe principale:

public const int UPS = 20; // Mises à jour par seconde public const int FPS = 60;

Maintenant, lors de l’initialisation du jeu, utilisez le framerate (FPS) pour définir la rapidité avec laquelle il lit et dessine la souris, ce qui devrait être au moins 60 FPS:

IsFixedTimeStep = true; TargetElapsedTime = TimeSpan.FromSeconds (1.0 / FPS);

Ensuite, ajoutez une minuterie à votre la grille classe, de sorte qu'elle ne se mettra à jour que quand il le faudra, indépendamment du nombre d'images par seconde:

Mise à jour publique vide (GameTime gameTime) (…) updateTimer + = gameTime.ElapsedGameTime; if (updateTimer.TotalMilliseconds> 1000f / Game1.UPS) updateTimer = TimeSpan.Zero; (…) // Met à jour les cellules et applique les règles. 

Maintenant, vous devriez être capable de lancer le jeu à la vitesse que vous souhaitez, même avec une mise à jour très lente de 5 mises à jour, pour pouvoir regarder attentivement votre simulation se dérouler tout en continuant à tracer de jolies lignes bien lisses à un débit d'image stable..


Conclusion

Vous avez maintenant un jeu de vie fluide et fonctionnel entre vos mains, mais si vous souhaitez l'explorer plus avant, vous pouvez toujours y ajouter plus de modifications. Par exemple, la grille suppose actuellement qu’au-delà de ses contours, tout est mort. Vous pouvez le modifier de manière à ce que la grille s'enroule de manière à ce qu'un planeur vole à jamais! Les variantes de ce jeu populaire ne manquent pas, alors laissez libre cours à votre imagination..

Merci de votre lecture et j'espère que vous avez appris des choses utiles aujourd'hui!