Crearea vieții Jocul vieții lui Conway

Uneori, chiar și un set simplu de reguli de bază vă poate oferi rezultate foarte interesante. În acest tutorial vom construi motorul de bază al jocului de viață al lui Conway de la început.

Notă: Deși acest tutorial este scris folosind C # și XNA, ar trebui să puteți utiliza aceleași tehnici și concepte în aproape orice mediu de dezvoltare a jocurilor 2D.


Introducere

Jocul de viață al lui Conway este un automat celular, conceput în anii 1970 de un matematician britanic numit, bine, John Conway.

Având în vedere o retea bidimensionala a celulelor, cu unii "pe" sau "in viata" si altii "off" sau "morti" si un set de reguli care guverneaza modul in care traiesc sau mor, putem avea o viata interesanta "Desfășurați-ne chiar în fața noastră. Deci, prin simpla desenare a unor modele pe grilă și apoi prin simularea, putem urmări evoluția formelor de viață de bază, răspândirea, moartea și eventual stabilizarea. Descărcați fișierele sursă finale sau verificați demo-ul de mai jos:

Acum, acest "Joc al Vieții" nu este strict un "joc" - este mai mult o mașină, în principal pentru că nu există un jucător și nici un scop, ci pur și simplu evoluează pe baza condițiilor sale inițiale. Cu toate acestea, este o mulțime de distracție de a juca cu, și există multe principii de design de joc care se pot aplica la crearea sa. Deci, fără să mai vorbim, să începem!

Pentru acest tutorial am mers înainte și am construit totul în XNA, pentru că asta mă simt cel mai bine. (Există un ghid pentru a începe cu XNA aici, dacă sunteți interesat). Cu toate acestea, ar trebui să puteți urmări împreună cu orice mediu de dezvoltare a jocurilor 2D pe care îl cunoașteți.


Crearea celulelor

Cel mai elementar element al jocului vieții lui Conway este celulele, care sunt "formele de viață" care formează baza întregii simulări. Fiecare celulă poate fi în una din cele două stări: "viu" sau "mort". Din motive de coerență, vom rămâne la cele două nume pentru stările celulare pentru restul tutorialului.

Celulele nu se mișcă, ci afectează pur și simplu vecinii în funcție de starea lor actuală.

Acum, în ceea ce privește programarea funcționalității lor, există cele trei comportamente pe care trebuie să le oferim:

  1. Ei trebuie să țină evidența poziției, limitelor și stării lor, astfel încât să poată fi dați clic și trasați corect.
  2. Ei trebuie să treacă între cei vii și cei morți atunci când au dat clic, ceea ce permite utilizatorului să facă efectiv lucruri interesante.
  3. Ei trebuie să fie desenați ca alb sau negru dacă sunt morți sau vii, respectiv.

Toate cele de mai sus pot fi realizate prin crearea unui celulă clasa, care va conține codul de mai jos:

clasa Cell public Point Position get; set privat;  Public Rectangle Bounds get; set privat;  bool public IsAlive get; a stabilit;  public Cell (Poziția punctului) Position = position; Bounds = dreptunghi nou (Position.X * Game1.CellSize, Position.Y * Game1.CellSize, Game1.CellSize, Game1.CellSize); IsAlive = false;  public void Update (MouseState mouseState) if (Bounds.Contains (new Point (mouseState.X, mouseState.Y))) // Asigurați-vă ca celulele să fie în viață cu clic stânga sau să le ucizi cu click-dreapta. dacă (mouseState.LeftButton == ButtonState.Pressed) IsAlive = true; altceva dacă (mouse.State.RightButton == ButtonState.Pressed) IsAlive = false;  void public Draw (SpriteBatch spriteBatch) dacă (IsAlive) spriteBatch.Draw (Game1.Pixel, Bounds, Color.Black); // Nu trageți nimic dacă este mort, deoarece culoarea de fundal implicită este albă. 

Grila și regulile sale

Acum că fiecare celulă se va comporta corect, trebuie să creăm o rețea care să îi țină pe toate și să implementeze logica care spune fiecăruia dacă ar trebui să vină în viață, să rămână în viață, să moară sau să rămână mort (fără zombi!).

Regulile sunt destul de simple:

  1. Orice celula viu cu mai putin de doi vecini vii moare, ca si cum ar fi cauzata de sub-populatie.
  2. Orice celulă viu cu doi sau trei vecini trăiește în următoarea generație.
  3. Orice celula viu cu mai mult de trei vecini vii moare, ca si cum ar fi prin supraaglomerare.
  4. Orice celulă moartă cu exact trei vecini vii devine o celulă vie, ca și cum ar fi prin reproducere.

Iată un ghid vizual rapid pentru aceste reguli din imaginea de mai jos. Fiecare celulă evidențiată de o săgeată albastră va fi afectată de regula corespunzătoare numerotată de mai sus. Cu alte cuvinte, celula 1 va muri, celula 2 va rămâne în viață, celula 3 va muri și celula 4 va veni în viață.

Deci, deoarece simularea jocului rulează o actualizare la intervale de timp constante, grilă va verifica fiecare dintre aceste reguli pentru toate celulele din rețea. Acest lucru poate fi realizat prin plasarea codului următor într-o nouă clasă pe care o voi apela Grilă:

clasa Grid public Point Size get; set privat;  celule private [,] celule; public Grid () Dimensiune = Punct nou (Game1.CellsX, Game1.CellsY); celule = celule noi [Size.X, Size.Y]; pentru (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) rezultat = fals; dacă (! living && count == 3) rezultat = adevărat; celule [i, j] .IsAlive = rezultat;  (...)

Singurul lucru pe care îl lipsește de aici este magia GetLivingNeighbors care numără pur și simplu câți vecini ai celulei curente sunt în prezent în viață. Deci, să adăugăm această metodă la adresa noastră Grilă clasă:

public int GetLivingNeighbors (int x, int) int count = 0; // Verificați celula din dreapta. dacă (x! = Size.X - 1) dacă (celule [x + 1, y] .IsAlive) numără ++; // Verificați celula din dreapta jos. dacă (x! = Size.X - 1 && y! = Size.Y - 1) dacă (celule [x + 1, y + 1] .IsAlive) numără ++; // Verificați celula din partea inferioară. dacă (y! = Size.Y - 1) dacă (celule [x, y + 1] .IsAlive) numără ++; // Verificați celula din stânga jos. dacă (x! = 0 && y! = Size.Y - 1) dacă (celule [x - 1, y + 1] .IsAlive) numără ++; // Verificați celula din stânga. dacă (x! = 0) dacă (celule [x - 1, y] .IsAlive) numără ++; // Verificați celula din partea stângă sus. dacă (x! = 0 && y! = 0) dacă (celule [x - 1, y - 1] .IsAlive) numără ++; // Verificați celula deasupra. dacă (y! = 0) dacă (celule [x, y - 1] .IsAlive) numără ++; // Verificați celula din dreapta sus. dacă (x! = Size.X - 1 && y! = 0) dacă (celule [x + 1, y - 1] .IsAlive) numărătoarea ++; retur; 

Rețineți că în codul de mai sus, primul dacă declarația fiecărei perechi verifică pur și simplu că nu suntem la marginea grila. Dacă nu am avea această verificare, am fi întâlnit în mai multe Excepții de la depășirea limitelor matricei. De asemenea, deoarece acest lucru va duce la numara niciodată nu cresc când verificăm marginile, adică jocul presupune că marginile sunt moarte, deci echivalează cu a avea o margine permanentă de celule albe, moarte în jurul ferestrelor noastre de joc.


Actualizarea rețelei în pași discrete de timp

Până acum, toată logica pe care am implementat-o ​​este solidă, dar nu se va comporta corect dacă nu ne asigurăm că simularea noastră se desfășoară în pași discrete. Acesta este doar un mod fantezist de a spune că toate celulele noastre vor fi actualizate în același timp, de dragul coerenței. Dacă nu am implementa acest lucru, am avea un comportament ciudat deoarece ordinea în care au fost verificate celulele ar conta, astfel că regulile stricte pe care tocmai le-am stabilit s-ar destrăma și se va produce mini-haos.

De exemplu, bucla noastră de mai sus verifică toate celulele de la stânga la dreapta, așa că dacă celula din stânga pe care tocmai am verificat-o a venit viu, acest lucru ar schimba contele pentru celula din mijloc pe care o verificăm acum și o poate face să vină în viață . Dar, dacă verificăm de la dreapta la stânga, celula din dreapta ar putea fi moartă, iar celula din stânga nu a mai trăit încă, deci celula noastră de mijloc ar rămâne mort. Acest lucru este rău pentru că este inconsecvent! Ar trebui să putem verifica celulele în orice ordine aleatoare pe care o dorim (ca o spirală!), Iar următorul pas ar trebui să fie întotdeauna identic.

Din fericire, acest lucru este într-adevăr destul de ușor de implementat în cod. Tot ce ne trebuie este să avem o a doua rețea de celule în memorie pentru următoarea stare a sistemului nostru. De fiecare dată când determinăm următoarea stare a unei celule, o păstrăm în cea de-a doua rețea pentru următoarea stare a întregului sistem. Apoi, când am găsit următoarea stare a fiecărei celule, le aplicăm pe toate în același timp. Deci, putem adăuga o matrice 2D de booleani nextCellStates ca variabilă privată, și apoi adăugați această metodă la Grilă clasă:

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

În cele din urmă, nu uitați să vă reparați Actualizați de mai sus, astfel încât să aloce rezultatul la următoarea stare, mai degrabă decât cea curentă, apoi să apeleze SetNextState la sfârșitul anului Actualizați după ce buclele au fost completate.


Desenarea Grilei

Acum că am terminat părțile mai complicate ale logicii rețelei, trebuie să o putem trage pe ecran. Grila va desena fiecare celula apelandu-si metodele de tragere la un moment dat, astfel incat toate celulele vii vor fi negre, iar cele moarte vor fi albe.

Grila actuală nu are nevoie pentru a atrage orice, dar este mult mai clară din perspectiva utilizatorului dacă adăugăm niște linii de rețea. Acest lucru permite utilizatorului să vadă mai ușor limitele celulelor și comunică, de asemenea, un sentiment de amploare, deci să creăm un A desena după cum urmează:

public void Draw (SpriteBatch spriteBatch) foreach (Celula celulară în celule) cell.Draw (spriteBatch); // Desenează linii verticale. pentru (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); 

Rețineți că în codul de mai sus, luăm un singur pixel și îl întindem pentru a crea o linie foarte lungă și subțire. Motorul dvs. particular de joc ar putea oferi un simplu DrawLine unde puteți să specificați două puncte și să aveți o linie între ele, ceea ce ar face mai ușor decât cele de mai sus.


Adăugarea unei logici de joc la nivel înalt

În acest moment, avem toate piesele de bază de care trebuie să facem jocul, trebuie doar să le aducem laolaltă. Deci, pentru început, în clasa principală a jocului dvs. (cea care începe totul), trebuie să adăugăm câteva constante precum dimensiunile rețelei și framerate (cât de repede se va actualiza) și toate celelalte lucruri de care avem nevoie ca imaginea unui singur pixel, dimensiunea ecranului și așa mai departe.

De asemenea, trebuie să inițializăm multe dintre aceste lucruri, cum ar fi crearea rețelei, stabilirea dimensiunii ferestrei pentru joc și asigurarea că mouse-ul este vizibil pentru a putea face clic pe celule. Dar toate aceste lucruri sunt specifice motorului și nu prea interesante, așa că vom trece peste el și vom ajunge la lucrurile bune. (Desigur, dacă urmăriți în XNA, puteți descărca codul sursă pentru a obține toate detaliile.)

Acum că avem totul înființat și gata să plecăm, ar trebui să fim capabili să conducem jocul! Dar nu atât de repede, pentru că există o problemă: nu putem face nimic pentru că jocul este mereu în desfășurare. Este practic imposibil să desenezi anumite forme pentru că se vor rupe în timp ce le tragi, așa că trebuie să fim capabili să oprim jocul. De asemenea, ar fi frumos să putem șterge grilă dacă devine o mizerie, deoarece creatiile noastre vor crește deseori de sub control și vor lăsa un dezastru.

Deci, să adăugăm un cod pentru a întrerupe jocul ori de câte ori bara de spațiu este apăsată și ștergeți ecranul dacă este apăsat spate:

suprascriere protejată void Actualizare (GameTime gameTime) keyboardState = Keyboard.GetState (); dacă (GamePad.GetState (PlayerIndex.One) .Buttons.Back == ButtonState.Pressed) this.Exit (); // Toggle pauză când bara de spațiu este apăsată. dacă (keyboardState.IsKeyDown (Keys.Space) && lastKeyboardState.IsKeyUp (Keys.Space)) Paused =! Paused; // Ștergeți ecranul dacă este apăsat spate. dacă (keyboardState.IsKeyDown (Keys.Back) && lastKeyboardState.IsKeyUp (Keys.Back)) grid.Clear (); base.Update (GameTime); grid.Update (GameTime); lastKeyboardState = tastaturăState; 

De asemenea, ne-ar ajuta dacă am fi foarte clar că jocul a fost întrerupt, așa că ne scriem A desena , adăugați un cod pentru a face roșu fundalul și scrieți "Paused" în fundal:

protejat suprascrie void Draw (GameTime gameTime) if (Paused) GraphicsDevice.Clear (Color.Red); altceva GraphicsDevice.Clear (Color.White); spriteBatch.Begin (); dacă (Pauză) string paused = "Paused"; spriteBatch.DrawString (Font, întrerupt, ScreenSize / 2, Color.Gray, 0f, Font.MeasureString (întrerupt) / 2, 1f, SpriteEffects.None, 0f);  grid.Draw (spriteBatch); spriteBatch.End (); base.Draw (GameTime); 

Asta e! Totul ar trebui să funcționeze acum, ca să-i dai un vârtej, să tragi niște forme de viață și să vezi ce se întâmplă! Mergeți și explorați modele interesante pe care le puteți face prin trimiterea la pagina Wikipedia din nou. Puteți, de asemenea, să jucați cu dimensiunile framerate, dimensiune celulară și grilă pentru a le modifica în funcție de preferințele dvs..


Adăugarea de îmbunătățiri

În acest moment, jocul este pe deplin funcțional și nu este nici o rușine să-i numim o zi. Dar o anomalie pe care ați fi observat-o este că clicurile de mouse nu se înregistrează întotdeauna atunci când încercați să actualizați o celulă, astfel încât atunci când faceți clic și trageți mouse-ul peste rețea, va lăsa o linie punctată în spatele, mai degrabă decât o solidă unu. Acest lucru se întâmplă deoarece rata la care actualizarea celulelor este și rata la care mouse-ul este verificat și este mult prea lent. Deci, pur și simplu trebuie să decuplăm rata la care jocul se actualizează și rata la care se citește intrarea.

Începeți prin definirea ratei de actualizare și a frameratului separat în clasa principală:

public const int UPS = 20; // actualizări pe secundă public const const FPS = 60;

Acum, când inițializați jocul, folosiți frameratul (FPS) pentru a defini cât de repede va citi intrarea și tragerea mouse-ului, ceea ce ar trebui să fie un nivel foarte bun de 60 FPS cel puțin:

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

Apoi, adăugați un cronometru la dvs. Grilă clasa, astfel încât să se actualizeze numai atunci când este necesar, independent de framerate:

public void Actualizare (GameTime gameTime) (...) updateTimer + = GameTime.ElapsedGameTime; dacă (updateTimer.TotalMilliseconds> 1000f / Game1.UPS) updateTimer = TimeSpan.Zero; (...) // Actualizați celulele și aplicați regulile. 

Acum, ar trebui să puteți rula jocul cu orice viteză dorită, chiar și cu o actualizare foarte lentă de 5 secunde, astfel încât să puteți urmări cu atenție simularea pe care o puteți desfășura, în timp ce puteți încă să desenați linii frumoase, netede, la o frameră solidă.


Concluzie

Acum ai un joc de viață fără probleme și funcțional pe mâinile tale, dar dacă vrei să-l explorezi mai departe, există întotdeauna mai multe trucuri pe care le poți adăuga. De exemplu, rețeaua presupune că, dincolo de marginile sale, totul este mort. Ați putea să o modificați astfel încât grila să se înfășoare, astfel încât un planor să zboare pentru totdeauna! Nu există nici o lipsă de variații în acest joc popular, astfel încât imaginația să fie sălbatică.

Vă mulțumim pentru lectură și sper că ați învățat astăzi câteva lucruri utile!