A volte anche un semplice insieme di regole base può darti risultati molto interessanti. In questo tutorial costruiremo il motore principale del gioco della vita di Conway da zero.
Nota: Sebbene questo tutorial sia scritto usando C # e XNA, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi 2D.
Conway's Game of Life è un automa cellulare che è stato ideato negli anni '70 da un matematico britannico di nome, beh, John Conway.
Con una griglia bidimensionale di celle, con alcuni "on" o "vivi" e altri "off" o "morti" e un insieme di regole che regolano il modo in cui prendono vita o muoiono, possiamo avere un'interessante "forma di vita" "svolgersi proprio di fronte a noi. Quindi, semplicemente disegnando alcuni modelli sulla nostra griglia, e quindi avviando la simulazione, possiamo osservare le forme di vita di base evolversi, diffondersi, estinguersi e alla fine stabilizzarsi. Scarica i file sorgente finali, o controlla la demo di seguito:
Ora, questo "Game of Life" non è strettamente un "gioco" - è più una macchina, principalmente perché non c'è nessun giocatore e nessun obiettivo, semplicemente si evolve in base alle sue condizioni iniziali. Tuttavia, è molto divertente giocare, e ci sono molti principi di design del gioco che possono essere applicati alla sua creazione. Quindi, senza ulteriori indugi, iniziamo!
Per questo tutorial, sono andato avanti e ho costruito tutto in XNA perché è quello con cui mi sento più a mio agio. (C'è una guida per iniziare con XNA qui, se sei interessato.) Tuttavia, dovresti essere in grado di seguire insieme qualsiasi ambiente di sviluppo di giochi 2D con cui hai familiarità con.
L'elemento più basilare di Conway's Game of Life è il cellule, quali sono le "forme di vita" che formano la base dell'intera simulazione. Ogni cella può essere in uno dei due stati: "vivo" o "morto". Per motivi di coerenza, resteremo fedeli a questi due nomi per gli stati della cella per il resto di questo tutorial.
Le cellule non si muovono, semplicemente influenzano i loro vicini in base al loro stato attuale.
Ora, in termini di programmazione delle loro funzionalità, ci sono i tre comportamenti che dobbiamo dargli:
Tutto quanto sopra può essere realizzato creando un Cellula
classe, che conterrà il codice qui sotto:
classe Cell public Point Position get; set privato; Bozze rettangolari pubbliche get; set privato; public bool IsAlive get; impostato; Cella pubblica (Posizione punto) Posizione = posizione; 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 (new Point (mouseState.X, mouseState.Y))) // Rendi vive le celle con il tasto sinistro del mouse o uccidili con il tasto destro del mouse. 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); // Non disegnare nulla se è morto, poiché il colore di sfondo predefinito è bianco.
Ora che ogni cellula si comporterà correttamente, dobbiamo creare una griglia che li tenga tutti, e implementare la logica che dice a ciascuno se deve venire in vita, rimanere in vita, morire o rimanere morto (senza zombi!).
Le regole sono abbastanza semplici:
Ecco una breve guida visiva a queste regole nell'immagine qui sotto. Ogni cella evidenziata da una freccia blu sarà influenzata dalla corrispondente regola numerata sopra. In altre parole, la cella 1 morirà, la cellula 2 rimarrà in vita, la cellula 3 morirà e la cellula 4 si animerà.
Quindi, poiché la simulazione del gioco esegue un aggiornamento a intervalli di tempo costanti, la griglia controllerà ognuna di queste regole per tutte le celle della griglia. Ciò può essere ottenuto inserendo il seguente codice in una nuova classe che chiamerò Griglia
:
classe Grid dimensione punto pubblico get; set privato; celle [,] celle private; public Grid () Size = new Point (Game1.CellsX, Game1.CellsY); cells = new Cell [Size.X, Size.Y]; per (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) risultato = falso; if (! living && count == 3) result = true; cells [i, j] .Alive = result; (...)
L'unica cosa che ci manca è la magia GetLivingNeighbors
metodo, che conta semplicemente quanti dei vicini della cella corrente sono attualmente vivi. Quindi, aggiungiamo questo metodo al nostro Griglia
classe:
public int GetLivingNeighbors (int x, int y) int count = 0; // Controlla cella a destra. if (x! = Size.X - 1) if (cells [x + 1, y] .IsAlive) count ++; // Controlla la cella in basso a destra. if (x! = Size.X - 1 && y! = Size.Y - 1) if (cells [x + 1, y + 1] .IsAlive) count ++; // Controlla la cella in basso. if (y! = Size.Y - 1) if (cells [x, y + 1] .IsAlive) count ++; // Controlla la cella in basso a sinistra. if (x! = 0 && y! = Size.Y - 1) if (cells [x - 1, y + 1] .IsAlive) count ++; // Controlla la cella a sinistra. if (x! = 0) if (cells [x - 1, y] .IsAlive) count ++; // Controlla la cella in alto a sinistra. if (x! = 0 && y! = 0) if (cells [x - 1, y - 1] .IsAlive) count ++; // Controlla la cella in alto. if (y! = 0) if (cells [x, y - 1] .IsAlive) count ++; // Controlla la cella in alto a destra. if (x! = Size.X - 1 && y! = 0) if (cells [x + 1, y - 1] .IsAlive) count ++; conteggio di ritorno;
Si noti che nel codice precedente, il primo Se
la dichiarazione di ogni coppia sta semplicemente controllando che non siamo sul bordo della griglia. Se non avessimo avuto questo controllo, avremmo incontrato diverse eccezioni per superare i limiti dell'array. Inoltre, dal momento che questo porterà a contare
non viene mai incrementato quando controlliamo oltre i bordi, il che significa che i bordi "presunti" del gioco sono morti, quindi è equivalente avere un bordo permanente di celle bianche e morte attorno alle finestre di gioco.
Finora, tutta la logica che abbiamo implementato è valida, ma non si comporterà correttamente se non facciamo attenzione a garantire che la nostra simulazione funzioni in tempi discreti. Questo è solo un modo elegante per dire che tutte le nostre celle saranno aggiornate allo stesso tempo, per motivi di coerenza. Se non avessimo implementato ciò, avremmo ottenuto un comportamento strano perché l'ordine in cui le celle erano state controllate avrebbe avuto importanza, quindi le rigide regole appena impostate si sarebbero disintegrate e ne sarebbe derivato il mini-caos..
Ad esempio, il nostro ciclo sopra controlla tutte le celle da sinistra a destra, quindi se la cella a sinistra appena controllata è diventata attiva, questo cambierebbe il conteggio della cella nel mezzo che stiamo controllando e potrebbe renderla viva . Ma, se dovessimo controllare da destra a sinistra, invece, la cella a destra potrebbe essere morta, e la cella a sinistra non è ancora viva, quindi la nostra cella centrale rimarrà morta. Questo è male perché è incoerente! Dovremmo essere in grado di controllare le celle in qualsiasi ordine casuale che vogliamo (come una spirale!) E il prossimo passo dovrebbe essere sempre identico.
Fortunatamente, questo è davvero facile da implementare nel codice. Tutto ciò di cui abbiamo bisogno è avere una seconda griglia di celle in memoria per il prossimo stato del nostro sistema. Ogni volta che determiniamo il prossimo stato di una cella, lo memorizziamo nella nostra seconda griglia per il successivo stato dell'intero sistema. Quindi, quando abbiamo trovato lo stato successivo di ogni cella, li applichiamo tutti allo stesso tempo. Quindi possiamo aggiungere una matrice 2D di booleani nextCellStates
come variabile privata, quindi aggiungere questo metodo a Griglia
classe:
public void SetNextState () for (int i = 0; i < Size.X; i++) for (int j = 0; j < Size.Y; j++) cells[i, j].IsAlive = nextCellStates[i, j];
Infine, non dimenticare di aggiustare il tuo Aggiornare
metodo sopra in modo che assegna il risultato allo stato successivo anziché a quello corrente, quindi chiama SetNextState
alla fine del Aggiornare
metodo, subito dopo il ciclo completo.
Ora che abbiamo completato le parti più complicate della logica della griglia, dobbiamo essere in grado di disegnarlo sullo schermo. La griglia disegnerà ogni cella chiamando i loro metodi di disegno uno alla volta, in modo che tutte le celle viventi saranno nere, e quelle morte saranno bianche.
La griglia attuale no bisogno disegnare qualsiasi cosa, ma è molto più chiaro dal punto di vista dell'utente se aggiungiamo alcune linee della griglia. Ciò consente all'utente di vedere più facilmente i contorni delle celle e anche di comunicare un senso di scala, quindi creiamo a Disegnare
metodo come segue:
public public Draw (SpriteBatch spriteBatch) foreach (cella cellulare nelle celle) cell.Draw (spriteBatch); // Disegna linee griglia verticali. per (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);
Nota che nel codice sopra, prendiamo un singolo pixel e lo allunghiamo per creare una linea molto lunga e sottile. Il tuo particolare motore di gioco potrebbe fornire un semplice Disegnare la linea
metodo in cui è possibile specificare due punti e avere una linea disegnata tra loro, il che renderebbe ancora più semplice di quanto sopra.
A questo punto, abbiamo tutti i pezzi di base di cui abbiamo bisogno per far funzionare il gioco, abbiamo solo bisogno di portare tutto insieme. Quindi, per i principianti, nella classe principale del tuo gioco (quella che inizia tutto), dobbiamo aggiungere alcune costanti come le dimensioni della griglia e il framerate (quanto velocemente verrà aggiornato) e tutte le altre cose di cui abbiamo bisogno l'immagine a pixel singolo, le dimensioni dello schermo e così via.
Abbiamo anche bisogno di inizializzare molte di queste cose, come la creazione della griglia, l'impostazione delle dimensioni della finestra per il gioco e l'accertamento del fatto che il mouse sia visibile in modo che possiamo cliccare sulle celle. Ma tutte queste cose sono specifiche del motore e non molto interessanti, quindi salteremo su di esso e passeremo alle cose buone. (Naturalmente, se segui XNA, puoi scaricare il codice sorgente per ottenere tutti i dettagli.)
Ora che abbiamo tutto pronto e pronto, dovremmo essere in grado di eseguire il gioco! Ma non così veloce, perché c'è un problema: non possiamo davvero fare nulla perché il gioco è sempre in esecuzione. È praticamente impossibile disegnare forme specifiche perché si romperanno man mano che le disegni, quindi abbiamo davvero bisogno di essere in grado di mettere in pausa il gioco. Sarebbe anche bello se potessimo cancellare la griglia se diventasse un disastro, perché le nostre creazioni diventeranno spesso incontrollabili e lasceranno dietro di sé un disastro.
Quindi, aggiungiamo del codice per mettere in pausa il gioco ogni volta che viene premuta la barra spaziatrice, e svuota lo schermo se viene premuto backspace:
protected override void Update (GameTime gameTime) keyboardState = Keyboard.GetState (); if (GamePad.GetState (PlayerIndex.One) .Buttons.Back == ButtonState.Pressed) this.Exit (); // Attiva / disattiva la pausa quando viene premuta la barra spaziatrice. if (keyboardState.IsKeyDown (Keys.Space) && lastKeyboardState.IsKeyUp (Keys.Space)) In pausa =! Paused; // Cancella lo schermo se si preme backspace. if (keyboardState.IsKeyDown (Keys.Back) && lastKeyboardState.IsKeyUp (Keys.Back)) grid.Clear (); base.Update (GameTime); grid.Update (GameTime); lastKeyboardState = keyboardState;
Sarebbe anche d'aiuto se chiarissimo che il gioco è stato messo in pausa, così come scriviamo il nostro Disegnare
metodo, aggiungiamo un po 'di codice per rendere lo sfondo rosso, e scrivi "Pausa" in background:
protected override void Draw (GameTime gameTime) if (Paused) GraphicsDevice.Clear (Color.Red); else GraphicsDevice.Clear (Color.White); spriteBatch.Begin (); if (Paused) string paused = "Paused"; spriteBatch.DrawString (Font, in pausa, ScreenSize / 2, Color.Gray, 0f, Font.MeasureString (in pausa) / 2, 1f, SpriteEffects.None, 0f); grid.Draw (spriteBatch); spriteBatch.End (); base.Draw (GameTime);
Questo è tutto! Ora tutto dovrebbe funzionare, così puoi dargli un vortice, disegnare alcune forme di vita e vedere cosa succede! Vai ed esplora modelli interessanti che puoi fare facendo nuovamente riferimento alla pagina di Wikipedia. Puoi anche giocare con il framerate, la dimensione della cella e le dimensioni della griglia per adattarla a tuo piacimento.
A questo punto, il gioco è perfettamente funzionante e non c'è da vergognarsi nel chiamarlo un giorno. Ma un fastidio che potresti aver notato è che i clic del tuo mouse non si registrano sempre quando stai cercando di aggiornare una cella, quindi quando fai clic e trascini il mouse sulla griglia lascerà una linea tratteggiata dietro piuttosto che un solido uno. Ciò accade perché la velocità con cui le celle si aggiornano è anche la velocità con cui viene controllato il mouse, ed è troppo lento. Quindi, abbiamo semplicemente bisogno di disaccoppiare la velocità con cui il gioco si aggiorna e la velocità con cui viene letto l'input.
Inizia definendo separatamente la frequenza di aggiornamento e il framerate nella classe principale:
pubblico const int UPS = 20; // Aggiornamenti al secondo public const int FPS = 60;
Ora, durante l'inizializzazione del gioco, usa il framerate (FPS) per definire la velocità con cui leggerà l'input e il disegno del mouse, il che dovrebbe essere almeno un buon 60 FPS liscio:
IsFixedTimeStep = true; TargetElapsedTime = TimeSpan.FromSeconds (1.0 / FPS);
Quindi, aggiungi un timer al tuo Griglia
classe, in modo che si aggiorni solo quando è necessario, indipendentemente dal framerate:
public void Update (GameTime gameTime) (...) updateTimer + = gameTime.ElapsedGameTime; if (updateTimer.TotalMilliseconds> 1000f / Game1.UPS) updateTimer = TimeSpan.Zero; (...) // Aggiorna le celle e applica le regole.
Ora, dovresti essere in grado di eseguire il gioco alla velocità desiderata, anche con 5 aggiornamenti al minuto molto lenti, in modo da poter osservare con attenzione la tua simulazione, mentre sei ancora in grado di tracciare delle linee morbide con un framerate solido.
Ora hai un gioco di vita fluido e funzionale tra le mani, ma nel caso in cui desideri esplorarlo ulteriormente, ci sono sempre più modifiche che puoi aggiungere ad esso. Ad esempio, la griglia attualmente presuppone che oltre i suoi bordi, tutto sia morto. Potresti modificarlo in modo tale che la griglia si avvolga, quindi un aliante volerebbe per sempre! Non c'è carenza di variazioni su questo gioco popolare, quindi lascia correre la tua immaginazione.
Grazie per la lettura, e spero che tu abbia imparato alcune cose utili oggi!