Un piccolo serpente ci conduce attraverso le principali caratteristiche del Windows Phone 7
Introduzione Core UI & Design Conclusione Premi Discussioni
Marcosroom.it Didatticando

Livelli

Ora, siamo arrivati qui, alla parte più complessa dell'intera applicazione...  Pronto? Iniziamo!

Level class

Iniziamo dall'obiettivo di questa classe: Level deve gestire l'intero livello di gioco e generare degli eventi quando accade qualcosa di interessante. Questi eventi sono Win e Loose. Come dice il nome, il primo viene generato quando l'utente vince, completando tutti gli obiettivi del livello;
il secondo, invece, quando il serpente viene ucciso da qualche nemico, oppure quando urta un muro. il terzo evento (FoodAdded) è contrassegnato come internal poichè deve essere gestito solo da alcune classi interne, e viene utilizzato per notificare ai serpenti nemici quando compare del nuovo cibo. La necessità di questo evento verrà spiegata successivamente, quando parleremo del FoodCatcherSnake.

Ora, invece, è giunto il momento di parlare di come l'oggetto Level funziona.

Iniziamo dal costruttore. Necessita di molti parametri, e ognuno di loro viene memorizzato in un campo privato e riutilizzato in seguito.

  • IMovementController movementController: controller di movimento da utilizzare per muovere il serpente principale
  • int speed: velocità di gioco. In verità, è solo l'intervallo dei timer
  • IEnumerable<IGoal> goal: tutti gli obiettivi che l'utente deve completare per vincere il livello
  • Cell[,] cells: array bidimensionale che contiene tutte le celle del gioco, gia riempite con il giusto contenuto (proprietà CellType già impostata)
  • IEnumerable<Point> snakeCells: lista delle coordinate delle celle del serpente
  • IEnumerable<IEnemySnake> enemySnakes: lista dei serpenti nemici del livello (ne parleremo dopo)

Ecco il codice del costruttore:

//Component initialization
InitializeComponent();

//Imposta le risorse
switch (System.Threading.Thread.CurrentThread.CurrentUICulture.Name.ToLower())
{
    case "it-it":
        this.Resources.MergedDictionaries.Add(new ResourceDictionary() { 
            Source = new Uri("/SnakeMobile.Core;component/Resources/it-IT.xaml", UriKind.Relative) 
        });
        break;
    default:
        this.Resources.MergedDictionaries.Add(new ResourceDictionary() {
            Source = new Uri("/SnakeMobile.Core;component/Resources/en-US.xaml", UriKind.Relative)
        });
        break;
}


//Inizializza qualche campo
_speed = speed;
_gridSize = new Size(cells.GetLength(0), cells.GetLength(1));
_cells = cells;
_goals = goal;
_snakeQueue = new Queue<Cell>();
foreach (Point p in snakeCells)
    _snakeQueue.Enqueue(cells[(int)p.X, (int)p.Y]);
_originalSnakeCells = snakeCells;
_enemySnakes = enemySnakes;


//Backup delle celle (per il replay)
_originalCells = new Cell[(int)_gridSize.Width, (int)_gridSize.Height];
CloneAndAssign(cells, _originalCells);
_originalSnakeQueue = new Queue<Cell>();
foreach (Point p in snakeCells)
    _originalSnakeQueue.Enqueue(_originalCells[(int)p.X, (int)p.Y]);


//Controller di movimento
movementController.Up += MovementController_Up;
movementController.Down += MovementController_Down;
movementController.Left += MovementController_Left;
movementController.Right += MovementController_Right;
if (movementController.IsVisual)
    ContentPresenterMovementController.Content = movementController.GetVisual();

//Snake timer
_snakeTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(speed) };
_snakeTimer.Tick += SnakeTimer_Tick;

//Enemy snake timer
_enemySnakesTimer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(speed * 2) };
_enemySnakesTimer.Tick += EnemySnakesTimer_Tick;


//Gestori di eventi privati
this.Loaded += UserControl_Loaded;
this.SizeChanged += UserControl_SizeChanged;

Come puoi vedere, nel costruttore memorizziamo solo i dati nei campi privati, inizializziamo i timer, aggiungiamo handler, e così via...
Una parte che potrebbe aver bisogno di una spiegazione è l'istruzione switch: l'intera applicazione è stata sviluppata utilizzando la lingua inglese, ma, poichè sono italiano, ho deciso di rendere l'intero gioco localizzabile. Ciò significa che l'applicazione è disponibile sia in inglese che in italiano, e cambia automaticamente lingua in base al linguaggio globale del device. Ovviamente, per un utente francese il gioco sarà ugualmente in inglese, poichè è la lingua di default.

Rendere un'applicazione localizzabile potrebbe sembrare noiso, ma ti posso assicurare che la parte più noiosa è.. è... nulla!

Questa è l'idea: devi avere una sorgente per tutte le stringhe localizzate (o qualunque altra cosa che ha bisogno di essere localizzata), e quando hai bisogno di una stringa, devi solo prenderla da quella sorgente. In questo caso, la nostra sorgente è un ResourceDictionary contenente alcune stringhe. Dovrebbe essere qualcosa come questa:

en-US.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">

<!-- General strings -->
<sys:String x:Key="OK">OK</sys:String>
<sys:String x:Key="Retry">Retry</sys:String>

<!-- Goal names -->
<sys:String x:Key="Goal_FoodEaten">Food eaten</sys:String>
<sys:String x:Key="Goal_GrowUp">Length</sys:String>
<sys:String x:Key="Goal_Time">Time</sys:String>

<!-- Overlay text -->
<sys:String x:Key="Overlay_Text_ReportGoal">Goals report</sys:String>
<sys:String x:Key="Overlay_Text_Death">You are dead!</sys:String>
<sys:String x:Key="Overlay_Text_Win">Level completed</sys:String>
<sys:String x:Key="Overlay_ButtonText_Return">Return to menu</sys:String>
    
</ResourceDictionary>
it-IT.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">

<!-- Stringhe generali -->
<sys:String x:Key="OK">OK</sys:String>
<sys:String x:Key="Retry">Riprova</sys:String>

<!-- Nomi degli obiettivi -->
<sys:String x:Key="Goal_FoodEaten">Cibo mangiato</sys:String>
<sys:String x:Key="Goal_GrowUp">Lunghezza</sys:String>
<sys:String x:Key="Goal_Time">Tempo</sys:String>
    
<!-- Testi per gli overlay -->
<sys:String x:Key="Overlay_Text_ReportGoal">Riepilogo obiettivi</sys:String>
<sys:String x:Key="Overlay_Text_Death">Sei morto!</sys:String>
<sys:String x:Key="Overlay_Text_Win">Livello completato</sys:String>
<sys:String x:Key="Overlay_ButtonText_Return">Ritorna al menu</sys:String>
    
</ResourceDictionary>

Come puoi vedere, in questi piccoli dizionari ci sono delle coppie di stringhe, una in inglese e l'altra in italiano. Importante: stesse stringhe in lingue diverse devono avere la stessa chiave. Perchè, però? Perchè quando hai bisogno di una stringa, per esempio "Retry", chiami semplicemente this.Resources["Retry"] e hai la stringa localizzata. Tornando al costruttore dell'oggetto Level, l'istruzione switch è utilizzata per riempire le risorse del controllo con le stringhe localizzate, in modo tale che una chiamata a this.Resources["Retry"] ritorni "Riprova", se siamo in Italia, altrimenti, "Retry", in tutti gli altri paesi.

Probabilmente ora ti starai chiedendo dove il gioco prende vita. Ecco la risposta: nel metodo UserControl_Loaded() (handler dell'evento Loaded della classe base UserControl). Qui... uhm... le parole sono inutili ora, lasciamo che sia il codice a parlare!

private void UserControl_Loaded(object sender, RoutedEventArgs e) {
            
    //Messaggio degli obiettivi
    MainOverlay.Text = Resources["Overlay_Text_ReportGoal"] as string;
    MainOverlay.AdditionalContent = _goals.BuildReport(32, 22);
    MainOverlay.BackgroundColor = Colors.Orange;
    MainOverlay.Show(new KeyValuePair<string,>(Resources["OK"] as string, () => {

        //Avvia il timer
        _beginTime = DateTime.Now;
        _snakeTimer.Start();
        _enemySnakesTimer.Start();

        //Aggiorna righe e colonne della griglia
        for (int i = 0; i < _gridSize.Width; i++)
            MainGrid.ColumnDefinitions.Add(new ColumnDefinition());
        for (int i = 0; i < _gridSize.Height; i++)
            MainGrid.RowDefinitions.Add(new RowDefinition());

        //Aggiunge celle alla griglia
        for (int x = 0; x < _gridSize.Width; x++)
            for (int y = 0; y < _gridSize.Height; y++) {
                Cell cell = _cells[x, y];
                MainGrid.Children.Add(cell);
                Grid.SetColumn(cell, x);
                Grid.SetRow(cell, y);
            }

    }));

}

Ma... ma... cos'è MainOverlay? è un OverlayMessage! SnakeMobile.Core.OverlayMessage è un controllo che ti permette di creare una sorta di "message layer" contenente un po' di testo e dei bottoni. Il metodo Show() necessita di un KeyValuePair<string, Action>[]; la chiave di ogni KeyValuePair è l'etichetta del bottone, e il suo valore è il delegato da invocare quando l'utente clicca quel bottone stesso.
Ho scritto questo controllo perchè a volte ho bisogno di chiedere qualcosa all'utente, e l'azione da fare quando l'utente clicca uno dei bottoni è diversa caso per caso, quindi, utilizzando un OverlayMessage, posso evitare di scrivere molto codice inutile per la gestione degli handler.

Ora, è giunto il momento della parte più interessante del gioco: il controllo del serpente.

Snake movement

Nella prima parte della figura precedente, ho messo un serpente in una griglia (la testa è a (2, 1)), e prendiamo il caso in cui voglia mangiare il cibo a sinistra.

La direzione del serpente è rappresentata da un oggetto di tipo Point (memorizzato nel campo privato _direction). Le sue coordinate sono solo -1, 0, 1, poichè rappresentano la variazione di coordinate fra la testa e la cella successiva. Un esempio potrebbe chiarire le idee. La testa è a (2, 1) è l'utente vuole muovere il serpente a sinistra. La direzione "sinistra" è il punto (-1, 0), perchè se lo sommiamo alle coordinate della testa, otteniamo le coordinate della cella che conterrà la testa stessa (cerchio blu nella seconda parte della figura).

(2, 1) + (-1, 0) = (1, 1)

Le celle sulle quali è presente il serpente sono gestite attraverso una Queue<Cell>: nel costruttore, aggiungiamo queste celle, in modo tale che in cima alla queue ci sia la coda del serpente e alla fine la testa.

Explanation of the snake queue

Quindi, per far muovere il serpente, dobbiamo fare un po' di azioni:

  1. Innanzitutto, cambiamo la proprietà CellType dell'ultima cella, di modo tale che diventi un normalissimo frammento di coda
  2. Poi, chiamiamo Queue<T>.Dequeue(), rimuovendo l'elemento in cima
  3. Aggiorniamo la prima cella, per farla diventare l'ultimo segmento della coda del serpente
  4. Infine, possiamo aggiungere la nuova testa. Ovviamente, la nuova cella è stata già calcolata utilizzando il concetto della direzione spiegato poco fa
  5. A questo punto, le azioni da fare sono finite, e ricomincia tutto dal primo punto

Non scriverò altre informazioni riguardo al codice per implementare questo meccanismo, poichè è semplice, ma molto molto lungo.

Comunque, il metodo SnakeTimer_Tick (handler dell'evento Tick del timer che fa muovere il serpente) fa altre 2 cose delle quali non abbiamo ancora parlato.

Cosa succede quando il serpente colpisce un muro? O come fa allungarsi? Quando finisce il livello? Questo e molto altro nella prossima puntata! No, no, stavo scherzando, e la risposta è qui, ed è anche semplice!

Fra il primo e il secondo passo della precedente procedura, c'è un piccolo dettaglio che devo mostrarti: dopo il calcolo della successiva cella per la testa del serpente, dobbiamo vedere ciò che accade quando la testa urta quest'ultima.

//Hit test
bool grown = false;
switch (nextCell.HitTest(head))
{
    //Cibo mangiato
    case HitTestResult.Eat:
        _foodEaten++;
        TxtFoodEaten.Text = _foodEaten.ToString();
        grown = true;
        AddFood();
        break;

    //Morte
    case HitTestResult.Death:
        KillSnake();
        return;

    //Taglio
    case HitTestResult.Cut:
        if (_snakeQueue.Contains(nextCell))
            foreach (Cell cutted in _snakeQueue.Cut(_snakeQueue.IndexOf(nextCell)))
                cutted.CellType = CellType.Free;
        else
            _enemySnakes.ForEach(x => x.Cut(nextCell));
        break;

    //Nessuna azione
    case HitTestResult.None:
        break;
}

//Controlla se il serpente è cresciuto
if (!grown) _snakeQueue.Dequeue().CellType = CellType.Free;

Come puoi vedere, le azioni da fare vengono decise in base al risultato dell'hit test fra la testa del serpente e la cella successiva. Se il risultato è Eat (la cella è una cibaria), incrementiamo il campo _foodEaten e impostiamo la variabile grown su falso, poichè questo ci permette di saltare il punto 2 (rimuovere l'ultimo frammento di coda). Se il risultato è Death (una delle celle è Wall o DeathFood), il serpente muore e il gioco finisce.
Se il risultato è Cut (un serpente taglia un altro serpente), controlliamo se il serpente tagliato è il serpente dell'utente, altrimenti, tagliamo uno dei serpenti nemici (parleremo del campo _enemySnakes successivamente).

L'ultima domanda ancora senza risposta è "Quando finisce il gioco?": dipende da quando tutti gli obiettivi del livello vengono completati.
Ogni livello, per essere completato, ha il proprio elenco di obiettivi da completare. Se ti ricordi, il costruttore dell'oggetto Level ha bisogno di un parametro che indica gli obiettivi; questi vengono memorizzati nel campo privato _goals. Alla fine del metodo SnakeTimer_Tick, controlliamo se sono stati tutti completati. Se è così, blocchiamo tutti i timer e visualizziamo un messaggio all'utente per informarlo che il livello è stato completato e che ha vinto.

//Checks the goals
GoalEventArgs gea = new GoalEventArgs(_snakeQueue.Count, _foodEaten, DateTime.Now.Subtract(_beginTime));
if (_goals.All(x => x.IsAccomplished(gea))) {

    //Ferma i timer
    _snakeTimer.Stop();
    _enemySnakesTimer.Stop();

    //Messaggo
    MainOverlay.AdditionalContent = null;
    MainOverlay.Text = Resources["Overlay_Text_Win"] as string;
    MainOverlay.BackgroundColor = Colors.Green;
    MainOverlay.Show(new KeyValuePair<string,>(
        Resources["Overlay_ButtonText_Return"] as string, 
        () => RaiseEvent(this.Win, this, gea))
    );

}

 

Condividi
Indietro Tutti i webmaster che volessero segnalare, non copiare,
il contenuto di questa pagina sul proprio sito, possono farlo liberamente.
E' gradito un preavviso tramite mail all'autore e l'iserimento,
nella pagina di citazione, di un link verso la pagina corrente.
© Copyright    Marco's Room
Avanti
Download SnakeMobile

Scaricato 115 volte

Celle IoC (Inversione di controllo) Un contenitore per Windows Phone 7 Controller di movimento Obiettivi Livelli Serpenti nemici Salvataggio e caricamento dei livelli