This article has been localized into Italian by the community.
In this article series, we're building a complete Snake game from scratch. It makes sense to start with the Introduction and then work your way through the articles one by one, to get the full understanding.
If you want to get the complete source code for the game at once, to get started modifying and learning from it right now, consider downloading all our samples!
Rilevare collisioni
Ora che abbiamo implementato l'area di gioco, il cibo e il serpente, così come il movimento continuo del serpente, abbiamo solo bisogno di un'ultima cosa per rendere il gioco reale: Collision detection . Il concetto si basa sul controllo costante se il nostro serpente ha appena colpito qualcosa; ne abbiamo bisogno per due scopi: vedere se il serpente ha appena mangiato del cibo o se ha colpito un ostacolo (il muro o la sua coda).
Il metodo DoCollisionCheck()
Il rilevamento delle collisioni verrà eseguito con un metodo chiamato DoCollisionCheck () , quindi dobbiamo implementarlo. Ecco come dovrebbe apparire attualmente:
private void DoCollisionCheck()
{
SnakePart snakeHead = snakeParts[snakeParts.Count - 1];
if((snakeHead.Position.X == Canvas.GetLeft(snakeFood)) && (snakeHead.Position.Y == Canvas.GetTop(snakeFood)))
{
EatSnakeFood();
return;
}
if((snakeHead.Position.Y < 0) || (snakeHead.Position.Y >= GameArea.ActualHeight) ||
(snakeHead.Position.X < 0) || (snakeHead.Position.X >= GameArea.ActualWidth))
{
EndGame();
}
foreach(SnakePart snakeBodyPart in snakeParts.Take(snakeParts.Count - 1))
{
if((snakeHead.Position.X == snakeBodyPart.Position.X) && (snakeHead.Position.Y == snakeBodyPart.Position.Y))
EndGame();
}
}
Come promesso, eseguiamo due controlli: per prima cosa vediamo se la posizione attuale della testa del serpente corrisponde alla posizione del pezzo di cibo corrente. In tal caso, chiamiamo il metodo EatSnakeFood() (ne parleremo più avanti). Quindi controlliamo se la posizione della testa del serpente supera i confini del GameArea, per vedere se il serpente sta uscendo da uno dei confini. In tal caso, chiamiamo il metodo EndGame () . Infine, controlliamo se la testa del serpente corrisponde a una delle posizioni della parte del corpo - in tal caso, il serpente si è appena scontrato con la sua stessa coda, ciò farà terminare il gioco, con una chiamata a EndGame () .
Il metodo EatSnakeFood()
Il metodo EatSnakeFood() è responsabile di fare un paio di cose: non appena il serpente mangia il pezzo di cibo corrente, dobbiamo aggiungere un nuovo pezzo di cibo in una nuova posizione, nonché regolare il punteggio, la lunghezza del serpente e l'attuale velocità di gioco. Per il punteggio, dobbiamo dichiarare una nuova variabile locale chiamata currentScore :
public partial class SnakeWPFSample : Window
{
....
private int snakeLength;
private int currentScore = 0;
....
Aggiungi il metodo EatSnakeFood ():
private void EatSnakeFood()
{
snakeLength++;
currentScore++;
int timerInterval = Math.Max(SnakeSpeedThreshold, (int)gameTickTimer.Interval.TotalMilliseconds - (currentScore * 2));
gameTickTimer.Interval = TimeSpan.FromMilliseconds(timerInterval);
GameArea.Children.Remove(snakeFood);
DrawSnakeFood();
UpdateGameStatus();
}
Come accennato, qui accadono diverse cose:
- Aumentiamo le variabili snakeLength e currentScore per riflettere il fatto che il serpente ha appena catturato un pezzo di cibo.
- Modifichiamo Interval di gameTickTimer , utilizzando la seguente regola: currentScore viene moltiplicato per 2 e quindi sottratto dall'intervallo corrente (velocità). Questo farà crescere esponenzialmente la velocità insieme alla lunghezza del serpente, rendendo il gioco sempre più difficile. In precedenza abbiamo definito un limite inferiore per la velocità, con la costante SnakeSpeedThreshold, il che significa che la velocità di gioco non scende mai al di sotto di un intervallo di 100 ms.
- Quindi rimuoviamo il pezzo di cibo appena consumato dal serpente e quindi chiamiamo il metodo DrawSnakeFood () che aggiungerà un nuovo pezzo di cibo in una nuova posizione.
- Infine, chiamiamo il metodo UpdateGameStatus (), che assomiglia a questo:
private void UpdateGameStatus()
{
this.Title = "SnakeWPF - Score: " + currentScore + " - Game speed: " + gameTickTimer.Interval.TotalMilliseconds;
}
Questo metodo aggiornerà semplicemente la proprietà Title della Window per riflettere il punteggio attuale e la velocità del gioco. Questo è un modo semplice per mostrare lo stato corrente, che può essere facilmente esteso in seguito, se lo si desidera.
Il metodo EndGame()
Abbiamo anche bisogno di un po 'di codice da eseguire quando il gioco dovrebbe finire. Lo faremo dal metodo EndGame () , che attualmente viene chiamato dal metodo DoCollisionCheck (). Come puoi vedere, al momento è molto semplice:
private void EndGame()
{
gameTickTimer.IsEnabled = false;
MessageBox.Show("Oooops, you died!\n\nTo start a new game, just press the Space bar...", "SnakeWPF");
}
Oltre a mostrare un messaggio all'utente, fermiamo semplicemente il gameTickTimer . Poiché questo timer è ciò che fa accadere tutte le cose nel gioco, non appena viene fermato, anche tutti i movimenti e i disegni si fermano.
Rettifiche finali
Ora siamo quasi pronti con la prima bozza di un gioco Snake completamente funzionale - in effetti, dobbiamo solo fare due piccoli aggiustamenti. Innanzitutto, dobbiamo assicurarci che venga chiamato il DoCollisionCheck () - questo dovrebbe avvenire come l'ultima azione eseguita nel metodo MoveSnake () , che abbiamo implementato in precedenza:
private void MoveSnake()
{
.....
//... and then have it drawn!
DrawSnake();
// Finally: Check if it just hit something!
DoCollisionCheck();
}
Ora il rilevamento delle collisioni viene eseguito non appena il serpente si è mosso! Ora ricordi come ti ho detto che abbiamo implementato una semplice variante del metodo StartNewGame () ? Dobbiamo espanderlo un po ', per assicurarci di reimpostare il punteggio ogni volta che il gioco viene (ri) avviato, come un paio di altre cose. Quindi, sostituisci il metodo StartNewGame () con questa versione leggermente estesa:
private void StartNewGame()
{
// Remove potential dead snake parts and leftover food...
foreach(SnakePart snakeBodyPart in snakeParts)
{
if(snakeBodyPart.UiElement != null)
GameArea.Children.Remove(snakeBodyPart.UiElement);
}
snakeParts.Clear();
if(snakeFood != null)
GameArea.Children.Remove(snakeFood);
// Reset stuff
currentScore = 0;
snakeLength = SnakeStartLength;
snakeDirection = SnakeDirection.Right;
snakeParts.Add(new SnakePart() { Position = new Point(SnakeSquareSize * 5, SnakeSquareSize * 5) });
gameTickTimer.Interval = TimeSpan.FromMilliseconds(SnakeStartSpeed);
// Draw the snake again and some new food...
DrawSnake();
DrawSnakeFood();
// Update status
UpdateGameStatus();
// Go!
gameTickTimer.IsEnabled = true;
}
Quando inizia una nuova partita, ora accadono le seguenti cose:
- Dato che questo potrebbe non essere il primo gioco, dobbiamo assicurarci che vengano rimossi tutti gli eventuali avanzi di un gioco precedente: questo include tutte le parti esistenti del serpente, così come il cibo rimasto.
- Dobbiamo anche ripristinare alcune delle variabili alle loro impostazioni iniziali, come il punteggio , la lunghezza , la direzione e la velocità del timer. Aggiungiamo anche la testa di serpente iniziale (che verrà automaticamente espansa con il metodo MoveSnake ()).
- Chiamiamo quindi i metodi DrawSnake () e DrawSnakeFood () per riflettere visivamente che un nuovo gioco è stato avviato.
- Chiamiamo il metodo UpdateGameStatus ().
- E infine, siamo pronti per iniziare il gameTickTimer - inizierà immediatamente ad andare, fondamentalmente mettendo in moto il gioco.
Sommario
Se hai superato tutta questa serie di articoli: congratulazioni, hai appena costruito il tuo primo gioco WPF! Goditi tutto il tuo duro lavoro eseguendo il tuo progetto, premendo il tasto Spazio e inizia a giocare - anche in questa semplicissima implementazione, Snake è un gioco divertente e avvincente!