Skape liv Conways livsspill

Noen ganger kan et enkelt sett med grunnleggende regler gi deg svært interessante resultater. I denne opplæringen bygger vi kjernemotoren til Conways Game of Life fra grunnen opp.

Merk: Selv om denne opplæringen er skrevet med C # og XNA, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst 2D-spillutviklingsmiljø.


Introduksjon

Conway's Game of Life er en cellulær automat som ble utviklet på 1970-tallet av en britisk matematiker som heter John Conway.

Gitt et todimensjonalt rutenett av celler, med noen "på" eller "levende" og andre "av" eller "døde", og et sett av regler som styrer hvordan de kommer til live eller dør, kan vi ha en interessant "livsform "Utvikle rett foran oss. Så, ved å tegne noen få mønstre på nettet vårt, og deretter starte simuleringen, kan vi se grunnleggende livsformer utvikle seg, spre, dø av og til slutt stabilisere seg. Last ned de endelige kildefilene, eller sjekk demoen nedenfor:

Nå er dette "Game of Life" ikke strengt et "spill" - det er mer en maskin, hovedsakelig fordi det ikke er noen spiller og ikke noe mål, det utvikler seg ganske enkelt ut fra sine opprinnelige forhold. Likevel er det mye moro å leke med, og det er mange prinsipper for spilldesign som kan brukes til å skape. Så, uten videre, la oss komme i gang!

For denne opplæringen gikk jeg videre og bygget alt i XNA fordi det er det jeg er mest komfortabelt med. (Det er en veiledning for å komme i gang med XNA her, hvis du er interessert.) Du bør imidlertid kunne følge med med 2D spillutviklingsmiljø som du er kjent med.


Opprette cellene

Det mest grunnleggende elementet i Conway's Game of Life er celler, hvilke er "livsformer" som danner grunnlaget for hele simuleringen. Hver celle kan være i ett av to tilstander: "levende" eller "død". For konsistensens skyld holder vi oss til de to navnene til cellestatusene for resten av denne opplæringen.

Cellene beveger seg ikke, de påvirker bare sine naboer basert på deres nåværende tilstand.

Nå, når det gjelder programmering av funksjonaliteten, er det de tre oppføringene vi trenger for å gi dem:

  1. De trenger å holde oversikt over deres posisjon, grenser og stat, slik at de kan klikkes og tegnes riktig.
  2. De trenger å bytte mellom levende og døde når de klikkes, noe som gjør at brukeren faktisk kan få interessante ting å skje.
  3. De må tegnes som hvite eller svarte hvis de er døde eller levende, henholdsvis.

Alt ovenfor kan oppnås ved å opprette en Celle klassen, som vil inneholde koden nedenfor:

klassecelle offentlig poengposisjon get; privat sett;  offentlige rektangulære bånd get; privat sett;  offentlig bool IsAlive get; sett;  Offentlig Cell (Punktposisjon) Posisjon = Posisjon; Bounds = ny rektangel (Position.X * Game1.CellSize, Position.Y * Game1.CellSize, Game1.CellSize, Game1.CellSize); IsAlive = false;  offentlig ugyldig oppdatering (MouseState mouseState) if (Bounds.Contains (nytt punkt (mouseState.X, mouseState.Y))) // Gjør celler levende med venstre-klikk eller drep dem med høyreklikk. hvis (mouseState.LeftButton == ButtonState.Pressed) IsAlive = true; ellers hvis (mouseState.RightButton == ButtonState.Pressed) IsAlive = false;  offentlig ugyldig tegning (SpriteBatch spriteBatch) hvis (IsAlive) spriteBatch.Draw (Game1.Pixel, Bounds, Color.Black); // Ikke tegne noe hvis det er dødt, siden standard bakgrunnsfargen er hvit. 

The Grid og Reglene

Nå som hver celle skal oppføre seg riktig, må vi opprette et rutenett som vil holde dem alle, og implementere logikken som forteller hverandre om den skal komme til live, bli levende, dø eller bli død (ingen zombier!).

Reglene er ganske enkle:

  1. Enhver levende celle med færre enn to levende naboer dør, som om forårsaket av underbefolkningen.
  2. Enhver levende celle med to eller tre levende naboer lever videre til neste generasjon.
  3. Enhver levende celle med mer enn tre levende naboer dør, som om ved overbefolkning.
  4. Enhver død celle med nøyaktig tre levende naboer blir en levende celle, som ved reproduksjon.

Her er en rask visuell veiledning til disse reglene i bildet nedenfor. Hver celle uthevet av en blå pil vil bli påvirket av den tilsvarende nummererte regelen ovenfor. Med andre ord, celle 1 vil dø, celle 2 vil forbli i live, celle 3 vil dø, og celle 4 vil komme levende.

Så, ettersom spillsimuleringen kjører en oppdatering med konstante tidsintervaller, vil ruten sjekke hvert av disse reglene for alle cellene i rutenettet. Det kan oppnås ved å sette følgende kode i en ny klasse jeg skal ringe Nett:

klasse grid offentlig punktstørrelse get; privat sett;  Private Cell [,] celler; offentlig grid () Størrelse = nytt punkt (Game1.CellsX, Game1.CellsY); celler = ny celle [størrelse.x, størrelse.y]; for (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) resultat = false; hvis (! living && count == 3) result = true; celler [i, j] .IsAlive = result;  (...)

Det eneste vi mangler herfra er den magiske GetLivingNeighbors metode, som bare teller hvor mange av de nåværende cellens naboer som i dag lever. Så la oss legge til denne metoden til vår Nett klasse:

offentlige int GetLivingNeighbors (int x, int y) int count = 0; // Sjekk cellen til høyre. hvis (x! = Størrelse.X - 1) hvis (celler [x + 1, y] .IsAlive) telle ++; // Sjekk celle nederst til høyre. hvis (x! = Størrelse.X - 1 && y! = Størrelse.Y - 1) hvis (celler [x + 1, y + 1] .IsAlive) telle ++; // Sjekk celle på bunnen. hvis (y! = Size.Y - 1) hvis (celler [x, y + 1] .IsAlive) telle ++; // Sjekk celle nederst til venstre. hvis (x! = 0 && y! = Size.Y - 1) hvis (celler [x - 1, y + 1] .IsAlive) telle ++; // Sjekk cellen til venstre. hvis (x! = 0) hvis (celler [x - 1, y] .IsAlive) telle ++; // Sjekk cellen øverst til venstre. hvis (x! = 0 && y! = 0) hvis (celler [x - 1, y - 1] .IsAlive) telle ++; // Sjekk cellen på toppen. hvis (y! = 0) hvis (celler [x, y - 1] .IsAlive) telle ++; // Sjekk cellen øverst til høyre. hvis (x! = Size.X - 1 && y! = 0) hvis (celler [x + 1, y - 1] .IsAlive) teller ++; retur telle; 

Merk at i den ovennevnte koden, den første hvis erklæringen av hvert par kontrollerer bare at vi ikke er på kanten av rutenettet. Hvis vi ikke hadde denne sjekken, ville vi løpe inn i flere unntak fra å overskride grensene for arrayet. Også, siden dette vil føre til telle blir aldri økt når vi sjekker forbi kantene, det betyr at spillet "antar" kanter er døde, så det tilsvarer å ha en permanent kantlinje av hvite, døde celler rundt våre spillvinduer.


Oppdaterer rutenettet i diskrete tidspunkter

Hittil er all logikken vi har implementert lyden, men det vil ikke oppføre seg riktig hvis vi ikke er forsiktige med å sørge for at simuleringen vår går i diskrete tidstrinn. Dette er bare en fancy måte å si at alle våre celler vil bli oppdatert på nøyaktig samme tid, for konsistensens skyld. Hvis vi ikke implementerte dette, ville vi få merkelig oppførsel fordi rekkefølgen som cellene ble sjekket inn, ville bety, så de strenge reglene vi nettopp hadde satt, ville falle fra hverandre og mini kaos ville oppstå.

For eksempel kontrollerer vår sløyfe over alle cellene fra venstre til høyre, så hvis cellen til venstre vi nettopp sjekket kom til live, ville dette endre tellingen for cellen i midten vi nå sjekker og kan få den til å komme i live . Men hvis vi sjekket fra høyre til venstre i stedet, kan cellen til høyre være død, og cellen til venstre har ikke kommet til live ennå, så vår midtcelle ville bli død. Dette er dårlig fordi det er inkonsekvent! Vi bør kunne sjekke cellene i hvilken som helst tilfeldig rekkefølge vi vil ha (som en spiral!), Og neste trinn bør alltid være identisk.

Heldigvis er dette veldig enkelt å implementere i kode. Alt vi trenger er å ha et andre rutenett av celler i minnet for neste tilstand i vårt system. Hver gang vi bestemmer den neste tilstanden til en celle, lagrer vi den i vårt andre rutenett for neste tilstand i hele systemet. Da, når vi har funnet den neste tilstanden i hver celle, søker vi dem alle samtidig. Så vi kan legge til en 2D rekke boolesker nextCellStates som en privat variabel, og legg deretter til denne metoden til Nett klasse:

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

Til slutt, ikke glem å fikse din Oppdater metode ovenfor slik at den tildeler resultatet til neste tilstand i stedet for den nåværende, og deretter ring SetNextState på slutten av Oppdater metode, rett etter at løkkene er ferdige.


Tegner rutenettet

Nå som vi har ferdig med de vanskeligere delene av rutenettets logikk, må vi kunne tegne den på skjermen. Gitteret vil tegne hver celle ved å kalle tegnemetoder en om gangen, slik at alle levende celler blir svarte, og de døde vil bli hvite.

Det faktiske rutenettet gjør det ikke trenge å tegne noe, men det er mye tydeligere fra brukerens perspektiv hvis vi legger til noen rutenettlinjer. Dette gjør at brukeren lettere kan se cellegrenser, og kommuniserer også en skalfølge, så la oss lage en Tegne metode som følger:

Offentlig tomrom Draw (SpriteBatch spriteBatch) foreach (Cell celle i celler) celle. Draw (spriteBatch); // Tegn vertikale gridlinjer. for (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); 

Merk at i koden ovenfor tar vi en enkelt piksel og strekker den for å lage en veldig lang og tynn linje. Din spesielle spillmotor kan gi en enkel Drawline metode der du kan spesifisere to poeng og ha en linje ved å tegne mellom dem, noe som ville gjøre det enda enklere enn det som er nevnt ovenfor.


Legge til høyt nivå spilllogikk

På dette punktet har vi alle de grunnleggende brikkene vi trenger for å få spillet til å løpe, vi trenger bare å bringe alt sammen. Så for startere, i spillets hovedklasse (den som starter alt), må vi legge til noen få konstanter som dimensjonene til rutenettet og frameratet (hvor raskt det vil oppdatere), og alle de andre tingene vi trenger som det enkle pikselbildet, skjermstørrelsen og så videre.

Vi må også initialisere mange av disse tingene, for eksempel å lage rutenett, sette vinduets størrelse for spillet, og sørg for at musen er synlig slik at vi kan klikke på celler. Men alle disse tingene er motorspesifikke og ikke veldig interessante, så vi hopper over det og kommer til de gode greiene. (Selvfølgelig, hvis du følger med i XNA, kan du laste ned kildekoden for å få alle detaljer.)

Nå som vi har alt satt opp og klar til å gå, bør vi bare kunne kjøre spillet! Men ikke så fort, fordi det er et problem: vi kan egentlig ikke gjøre noe fordi spillet alltid kjører. Det er i utgangspunktet umulig å tegne spesifikke figurer fordi de vil knekke fra hverandre når du tegner dem, så vi trenger virkelig å kunne sette pause på spillet. Det ville også være fint hvis vi kunne rydde rutenettet hvis det blir et rot, fordi våre kreasjoner ofte vil bli ute av kontroll og gi et rotete bak.

Så la oss legge til noe kode for å pause spillet når mellomromstasten trykkes, og fjern skjermen hvis du trykker på backspace:

beskyttet overstyring ugyldig oppdatering (GameTime gameTime) keyboardState = Keyboard.GetState (); hvis (GamePad.GetState (PlayerIndex.One) .Buttons.Back == ButtonState.Pressed) this.Exit (); // Bytt pause når mellomromstasten er trykket. hvis (keyboardState.IsKeyDown (Keys.Space) && lastKeyboardState.IsKeyUp (Keys.Space)) Paused =! Paused; // Fjern skjermen hvis du trykker på backspace. hvis (keyboardState.IsKeyDown (Keys.Back) && lastKeyboardState.IsKeyUp (Keys.Back)) grid.Clear (); base.Update (gametime); grid.Update (gametime); lastKeyboardState = keyboardState; 

Det ville også hjelpe hvis vi gjorde det veldig klart at spillet ble stanset, slik som vi skriver Tegne metode, la oss legge til noen kode for å gjøre bakgrunnen rød, og skriv "Paused" i bakgrunnen:

beskyttet overstyring ugyldig tegning (GameTime gameTime) hvis (pauset) GraphicsDevice.Clear (Color.Red); ellers GraphicsDevice.Clear (Color.White); spriteBatch.Begin (); hvis (pauset) string paused = "Paused"; spriteBatch.DrawString (Font, pauset, ScreenSize / 2, Color.Gray, 0f, Font.MeasureString (pauset) / 2, 1f, SpriteEffects.None, 0f);  grid.Draw (spriteBatch); spriteBatch.End (); base.Draw (gametime); 

Det er det! Alt skal nå fungere, slik at du kan gi det en virvel, tegne noen livsformer og se hva som skjer! Gå og utforske interessante mønstre du kan lage ved å referere til Wikipedia-siden igjen. Du kan også spille med framerate, celle størrelse og rutenett dimensjoner for å tilpasse det til din smak.


Legger til forbedringer

På dette tidspunktet er spillet fullt funksjonelt, og det er ingen skam å kalle det en dag. Men en irritasjon du kanskje har lagt merke til er at museklikkene ikke alltid registreres når du prøver å oppdatere en celle, så når du klikker og drar musen over rutenettet, vil den etterlate en stiplede linje i stedet for en solid en. Dette skjer fordi hastigheten som cellene oppdaterer er også den hastigheten som musen blir sjekket på, og det er altfor sakte. Så, vi trenger bare å avkoble frekvensen som spillet oppdaterer, og hastigheten som leser innspillingen.

Begynn med å definere oppdateringshastigheten og frameratet separat i hovedklassen:

offentlig konsept UPS = 20; // Oppdateringer per sekund offentlig const int FPS = 60;

Nå, når du initialiserer spillet, bruker du framerate (FPS) for å definere hvor raskt det vil lese innspill og tegning av musen, som i hvertfall bør være en fin jevn 60 FPS:

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

Deretter legger du til en timer til din Nett klassen, slik at den bare vil oppdatere når den trenger, uavhengig av frameratet:

offentlig ugyldig oppdatering (GameTime gameTime) (...) updateTimer + = gameTime.ElapsedGameTime; hvis (updateTimer.TotalMilliseconds> 1000f / Game1.UPS) updateTimer = TimeSpan.Zero; (...) // Oppdater cellene og bruk reglene. 

Nå bør du være i stand til å kjøre spillet uansett hastighet du vil, til og med en veldig langsom 5 oppdateringer per sekund, slik at du nøye kan se simuleringen din utfolde seg, mens du fremdeles kan tegne fine glatte linjer på en solid framerate.


Konklusjon

Du har nå en jevn og funksjonell Game of Life på hendene, men hvis du vil utforske det videre, er det alltid flere tweaks du kan legge til. For eksempel antar rutenettet at utover kantene er alt dødt. Du kan endre det slik at rutenettet brytes rundt, så en glidebrett ville fly for alltid! Det er ingen mangel på variasjoner på dette populære spillet, så la fantasien din gå tomt.

Takk for at du leser, og jeg håper du har lært noen nyttige ting i dag!