Per gestire le collisioni con la MMGameLibrary2D esistono sostanzialmente 2 modi:
- Lo sviluppatore si crea la propria logica per verificare le collisioni nel game loop, utilizzando il metodo CheckSpritesCollision della classe
ScrollableBackground
. Questo metodo ritorna true se i 2 oggetti passati come parametri sono in collisione - Impostare la gestione delle collisioni automatica e mettersi semplicemente in attesa dell'evento CollisionDetect della classe
ScrollableBackground
.
Il primo metodo è più performante e lascia molta libertà di gestione allo sviluppatore, sollevandolo solo dall'onere di implementare l'algoritmo che determina se 2 oggetti si stanno toccando.
Il secondo metodo è un po' meno prestazionale ma facilita enormemente la gestione delle collisioni. In questo tutorial infatti, vedremo come utilizzare proprio quest'ultimo.
Come appena spiegato, per gestire le collisioni non dobbiamo fare altro che aggiungere un delegato per l'evento CollisionDetect dell'istanza del nostro Background, dove rimuoveremo semplicemente dalla lista degli oggetti a video l'oggetto che è in collisione con il nostro personaggio.
Iniziamo dunque con il creare la funzione Background_CollisionDetect
nel nostro GameModel:
void Background_CollisionDetect(Sprite sprite1, Sprite sprite2)
{
// se il personaggio è sprite1 allora rimuovo sprite2
// altrimenti viceversa
if(sprite1.SpriteType == "player")
{
this.GameBackground.RemoveSprite(sprite2);
}
else
{
this.GameBackground.RemoveSprite(sprite1);
}
}
e la registriamo per l'evento CollisionDetect
nel costruttore della classe (appena dopo aver preparato i layer dello sfondo)
public GameModel
{
...
// Crea il layer per le colline e lo aggiunge all'istanza del background
...
// registra il delegato per l'evento delle collisioni
_gameBackground.CollisionDetect += Background_CollisionDetect;
// Aggancia gli eventi della tastiera
...
}
Finalmente (se tutto va bene..) il nostro amichetto è in grado di papparsi le sue amate fragole! Quindi proviamo a compilare ed eseguire, e che la caccia alla fragola abbia inizio!
Adesso che il nostro personaggio può riempinzarsi di fragole, cerchiamo di rendergli la vita più difficile inserendo qua e là qualche temibile bomba.
Per inserire le bombe modifichiamo metodo AddObject
, aggiungendo un parametro che ci consenta di specificare il tipo di oggetto che desideriamo aggiungere (fragola o bomba) in modo da utilizzare lo stesso metodo per entrambe:
public async void AddObject(string objectType)
{
// genera un numero casuale per determinare l'altezza di uscita
// dell'oggetto che va da 0 (bordo superiore dello schermo)
// a 668 (= 768 - l'altezza degli oggetti che è 100)
int top = HeightRandom.Next(0, 668);
// crea un nuovo oggetto Sprite (fragola o bomba) appena fuori dallo schermo a destra
Sprite newSprite;
if(objectType == "strawberry")
{
newSprite = new Sprite(81, 100, new Point(1366, top));
// aggiunge l'immagine
await newSprite.SetSpriteSheet(new Uri("ms-appx:///Assets/Images/strawberry.tif"), 1, 1);
}
else
{
newSprite = new Sprite(79, 100, new Point(1366, top));
// aggiunge l'immagine
await newSprite.SetSpriteSheet(new Uri("ms-appx:///Assets/Images/bomb.tif"), 1, 1);
}
// imposta il tipo
newSprite.SpriteType = objectType;
// abilita le collisioni
newSprite.isCollisionEnabled = true;
// inserisce l'oggetto nella lista degli oggetti a video
this.GameBackground.AddSprite(newSprite);
}
Poi andiamo nella MainPage e ripetiamo la stessa procedura delle fragole anche per le bombe, dichiarando un'altra coppia di variabili per la gestione della casualità
public sealed partial class MainPage : Page
{
...
int bombInterval = 0;
Random bombRandom = new Random();
E gestendole infine nell'evento del timer, dove dobbiamo anche modificare la chiamata al metodo AddObject
creato in precedenza, in quanto è stato aggiunto il parametro del tipo e di conseguenza lo dobbiamo passare alla funzione:
void TimeTick(object timer, object e)
{
...
// verifica se è il momento di far apparire una fragola
if(strawberryInterval == 0)
{
// se è il momento la aggiunge
this.game.AddObject("strawberry");
// e imposta il successivo intervallo casualmente
// tra 0 e 4 secondi
strawberryRandom = strawberryRandom.Next(0, 4);
}
else
{
// se non è il momento decrementa semplicemente l'intervallo
strawberryInterval -= 1;
}
// verifica se è il momento di far apparire una bomba
if(bombInterval == 0)
{
// se è il momento la aggiunge
this.game.AddObject("bomb");
// e imposta il successivo intervallo casualmente
// tra 1 e 5 secondi
bombRandom = bombRandom.Next(1, 5);
}
else
{
// se non è il momento decrementa semplicemente l'intervallo
bombInterval -= 1;
}
}
L'ultima cosa che ci resta da fare è gestire la collisione con le bombe nel delegato dell'evento CollisionDetect
. Infatti se il personaggio entra in collisione con una bomba, dobbiamo fermare il gioco in quanto lo sfortunato mostriciattolo è saltato per aria...
Aggiungiamo per comodità un metodo GameOver al nostro GameModel, dove fermeremo tutto il gioco e visualizzeremo un messaggio per l'utente:
public async void GameOver()
{
// ferma il gioco
this.GameloopStoryboard.Stop();
// ferma lo scroll dello sfondo
this.GameBackgound.StopScroll();
// ferma l'animazione del personaggio
this.PlayerSprite.StopAnimation();
// imposta lo stato
this.GameState = enGameState.STOP;
// visualizza il messaggio
Windows.UI.Popups.MessageDialog message = new Windows.UI.Popups.MessageDialog("Mi dispiace, hai perso", "Game Over");
await message.ShowAsync();
}
Poi modifichiamo la gestione delle collisioni aggiungendo la chiamata al metodo GameOver
in caso di collisione con una bomba:
void Background_CollisionDetect(Sprite sprite1, Sprite sprite2)
{
// se il personaggio è sprite1 allora rimuovo sprite2
// altrimenti viceversa
if(sprite1.SpriteType == "player")
{
if(sprite2.SpriteType == "strawberry")
{
this.GameBackground.RemoveSprite(sprite2);
}
else
{
this.GameOver();
}
}
else if(sprite2.SpriteType == "player")
{
if(sprite1.SpriteType == "strawberry")
{
this.GameBackground.RemoveSprite(sprite1);
}
else
{
this.GameOver();
}
}
}
Infine dobbiamo richiamare il metodo GameOver
anche nel caso in cui una fragola finisca fuori dallo schermo senza che il personaggio sia riuscito a mangiarla. Per fare questo è sufficiente verificare la posizione di ogni singola fragola nel GameLoop
e, se la sua X è minore di 0, vuol dire che sta uscendo dall'area di gioco, pertanto il gioco finisce.
Già che ci siamo facciamo anche un controllo sulle bombe in quanto, una volta uscite dall'area di gioco, vanno rimosse dalla lista degli oggetti a video (altrimenti continuano ad occupare risorse inutilmente, andando a spasso nel nulla...). Non è possibile però rimuovere un oggetto dalla collection che si sta ciclando, pertanto per rimuovere le bombe dobbiamo creare una lista di appoggio dove inserire gli oggetti "eliminabili", che utilizzeremo alla fine del ciclo for per rimuoverli dalla lista principale.
private void GameLoop(Object sender, Object e)
{
...
// lista di appoggio per le bombe da eliminare
List<Sprite> bombsToRemove = new List<Sprite>();
// fa avanzare tutti gli oggetti presenti a video di 5 pixel alla volta
foreach(Sprite sprite in this.GameBackground.Sprites)
{
if(sprite.SpriteType != "player")
{
sprite.Move(new Point(sprite.Position.X - 5, sprite.Position.Y));
// se una fragola finisce fuori dallo schermo il gioco finisce
if(sprite.SpriteType == "strawberry" && sprite.Position.X < -sprite.Size.Width)
{
this.GameOver();
return;
}
// se una bomba finisce fuori dallo schermo va aggiunta alla lista
// degli oggetti da eliminare
if(sprite.SpriteType == "bomb" && sprite.Position.X < -sprite.Size.Width)
{
bombsToRemove.Add(sprite);
}
}
}
// fa ripartire il timer dello StoryBoard
this.GameloopStoryboard.Begin)();
}
Se provate ora a compilare ed eseguire, dovreste essere in grado di giocare effettivamente alla vostra prima creazione videoludica, complimenti!
Prima di passare al capitolo finale del tutorial, ovvero l'aggiunta dei suoni, sarebbe bello fare in modo che quando il gioco finisce, si possa ricominciare da capo senza dover riavviare il progetto ogni volta, non credete? Aggiungiamo allora qualche riga di codice dopo la visualizzazione del messaggio nel metodo GameOver
:
public async void GameOver()
{
...
// elimina tutti gli oggetti a video
List<Sprite> objectsToRemove = new List<Sprite>();
objectsToRemove.AddRange(this.GameBackground.Sprites);
foreach (Sprite obj in objectsToRemove)
{
if(obj.SpriteType == "player")
{
// se è il personaggio lo riposiziona
obj.Position = new Point(100, 200);
}
else
{
// tutti gli altri oggetti li rimuove
this.GameBackground.RemoveSprite(obj);
obj.Dispose();
}
}
// resetta le variabili dei comandi
this.LeftButtonPressed = false;
this.RightButtonPressed = false;
this.UpButtonPressed = false;
this.DownButtonPressed = false;
// fa ripartire il gioco
this.StartGame();
}
Benissimo, in questo modo quando l'utente chiude il messaggio di game over, il gioco riparte automaticamente dall'inizio. Adesso noi siamo davvero pronti a dare l'ultimo tocco al nostro gioco.