• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 29/04/2014

TP : Jeux de hasard (Grattage et secouage)

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Ahhh, ça commence à devenir sympa ce qu’on peut faire ! L’écran tactile et la gestuelle associée, ainsi que l’accéléromètre sont des outils vraiment intéressant à utiliser. Et puis cela nous oblige à sortir de la classique souris et à imaginer des nouveaux types d’interactions avec l’utilisateur.
Nous allons donc mettre en pratique ces derniers éléments dans ce nouveau TP où nous allons créer une petite application de jeu de hasard, découpée en deux petits jeux qui vont utiliser la gestuelle et l’accéléromètre.

Instructions pour réaliser le TP

Créez dans un premier temps une page de menu qui renverra vers une page où nous aurons un jeu de grattage et une autre page où nous aurons un jeu de secouage…
Le jeu en lui-même ne sera pas super évolué et peu esthétique car je souhaite que vous vous concentriez sur les techniques étudiées précédemment, mais rien ne vous empêche de laisser courir votre imagination et de réaliser la prochaine killer-app. ;)

Donc, le grattage, je propose qu’il s’agisse d’afficher 3 rectangles que l’on peut gratter. Une fois ces rectangles grattés, ils découvrent si nous avons gagné ou pas. Un tirage aléatoire est fait pour déterminer le rectangle gagnant et une fois que nous avons commencé à gratter un rectangle, il n’est plus possible d'en gratter un autre. Pas besoin de gratter tout le rectangle, vous pourrez afficher la victoire ou la défaite de l’utilisateur une fois un certain pourcentage du rectangle gratté. N’oubliez pas d’offrir à l’utilisateur la possibilité de rejouer et de voir le nombre de parties gagnées.
Voici à la figure suivante le résultat que je vous propose d’atteindre.

Le grattage
Le grattage

Passons maintenant au secouage. Le principe est de détecter via l’accéléromètre lorsque l’utilisateur secoue son téléphone. À ce moment-là, nous pourrons générer un nombre aléatoire avec une chance sur 3 de gagner. Pourquoi ne pas faire mariner un peu l’utilisateur en lui affichant une barre de progression indéterminée et en attendant deux secondes pour afficher le résultat. :)
Voici à la figure suivante le résultat que je vous propose d’atteindre.

Le secouage
Le secouage

Alors, si vous vous le sentez, n’hésitez pas à vous lancer directement. Sinon, je vais vous proposer quelques pistes de réflexion pour démarrer sereinement le TP.

Tout d’abord, au niveau du grattage. Il y a plusieurs solutions envisageables. Celle que je vous propose est de ne pas avoir réellement un unique rectangle à gratter, mais plutôt plein de petits rectangles qui recouvrent un TextBlock contenant un texte affichant si c’est gagné ou perdu. Chaque TextBlock sera à l’écoute d’un événement de manipulation, j’ai choisi pour ma part la gestuelle du drap & drop du toolkit. Il faut ensuite arriver à déterminer quel élément est concerné lorsque nous touchons l’écran. Pour cela, j’utilise une méthode du framework .NET : FindElementsInHostCoordinates. Par exemple, pour récupérer le TextBlock choisi lors du premier contact, je pourrais faire :

private void GestureListener_DragStarted(object sender, DragStartedGestureEventArgs e)
{
    Point position = e.GetPosition(Application.Current.RootVisual);
    IEnumerable<UIElement> elements = VisualTreeHelper.FindElementsInHostCoordinates(new Point(position.X, position.Y), Application.Current.RootVisual);
    TextBlock textBlockChoisi = elements.OfType<TextBlock>().FirstOrDefault();
}

La méthode GetPosition nous fournit la position du doigt par rapport à la page courante, que nous pouvons obtenir grâce à la propriété RootVisual de l’application. Ainsi, il sera possible de déterminer les éléments qui sont sélectionnés et les supprimer de devant le TextBlock.

Passons maintenant au secouage. Comment détecter que l’utilisateur secoue son téléphone ? Il y a plusieurs solutions. Celle que j’ai choisi consister à détecter un écart significatif entre les deux dernières accélérations du téléphone. Si cet écart se reproduit plusieurs fois, alors je peux considérer qu’il s’agit d’un secouage. Par contre, si l’écart passe sous un certain seuil, alors je dois arrêter d’imaginer un potentiel secouage.

Allez, c’est à vous de jouer.

Correction

Alors, vous avez trouvé comment ? Facile ? Difficile ?
Ce n’est pas toujours facile de se confronter directement à ce genre de situations, surtout lorsqu’on a l’habitude de réaliser des applications clientes lourdes ou web, ou même lorsqu’on a pas du tout l’habitude de réaliser des applications. :)

Voici la correction que je propose. Tout d’abord le menu, vous savez faire, il s’agit de ma page MainPage.xaml qui renvoie vers deux autres pages :

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
        <TextBlock x:Name="ApplicationTitle" Text="TP Jeux de hasard" Style="{StaticResource PhoneTextNormalStyle}"/>
        <TextBlock x:Name="PageTitle" Text="Menu" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
        <StackPanel>
            <Button Content="Grattage ..." Tap="Button_Tap" />
            <Button Content="Secouage ..." Tap="Button_Tap_1" />
        </StackPanel>
    </Grid>
</Grid>

C’est très épuré, le code-behind sera :

public partial class MainPage : PhoneApplicationPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    private void Button_Tap(object sender, System.Windows.Input.GestureEventArgs e)
    {
        NavigationService.Navigate(new Uri("/Grattage.xaml", UriKind.Relative));
    }

    private void Button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
    {
        NavigationService.Navigate(new Uri("/Secouage.xaml", UriKind.Relative));
    }
}

Une utilisation très classique du service de navigation. Passons maintenant à la page Grattage.xaml :

<phone:PhoneApplicationPage 
    …
    xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit">

    <Grid x:Name="LayoutRoot" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
            <TextBlock x:Name="ApplicationTitle" Text="Grattage" Style="{StaticResource PhoneTextNormalStyle}"/>
        </StackPanel>
        <Canvas x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <TextBlock x:Name="TextBlock1" Width="100" Height="100" Canvas.Top="10" Canvas.Left="40" Margin="20 30 0 0">
                <toolkit:GestureService.GestureListener>
                    <toolkit:GestureListener DragStarted="GestureListener_DragStarted" DragDelta="GestureListener_DragDelta" DragCompleted="GestureListener_DragCompleted" />
                </toolkit:GestureService.GestureListener>
            </TextBlock>
            <TextBlock x:Name="TextBlock2" Width="100" Height="100" Canvas.Top="10" Canvas.Left="170" Margin="20 30 0 0">
                <toolkit:GestureService.GestureListener>
                    <toolkit:GestureListener DragStarted="GestureListener_DragStarted" DragDelta="GestureListener_DragDelta" DragCompleted="GestureListener_DragCompleted" />
                </toolkit:GestureService.GestureListener>
            </TextBlock>
            <TextBlock x:Name="TextBlock3" Width="100" Height="100" Canvas.Top="10" Canvas.Left="300" Margin="20 30 0 0">
                <toolkit:GestureService.GestureListener>
                    <toolkit:GestureListener DragStarted="GestureListener_DragStarted" DragDelta="GestureListener_DragDelta" DragCompleted="GestureListener_DragCompleted" />
                </toolkit:GestureService.GestureListener>
            </TextBlock>
            <StackPanel Canvas.Top="250" Width="480">
                <Button Content="Rejouer" HorizontalAlignment="Center" Tap="Button_Tap" />
                <TextBlock x:Name="Resultat" />
            </StackPanel>
        </Canvas>
    </Grid>
</phone:PhoneApplicationPage>

Comme je l’ai proposé, j’ai simplement ajouté trois TextBlock dans un Canvas. Remarquez que j’ai spécifié exhaustivement les largeurs et les hauteurs de chacun. Sur chaque TextBlock, je suis également à l’écoute des trois événements de drag & drop. Rien de bien compliqué.
C’est coté code-behind que cela se complique.

public partial class Grattage : PhoneApplicationPage
{
    private const int largeurRectangle = 15;
    private int nbRects;
    private TextBlock textBlockChoisi;
    private Random random;
    private bool aGagne;
    private int nbParties;
    private int nbPartiesGagnees;

    public Grattage()
    {
        InitializeComponent();
        random = new Random();
    }

    protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
    {
        Init();
           
        base.OnNavigatedTo(e);
    }
}

Nous déclarons dans un premier temps plusieurs variables qui vont nous servir dans la classe. Il y a notamment un générateur de nombre aléatoires, initialisé dans le constructeur. Ensuite, cela se passe dans la méthode Init(). Il faut dans un premier temps créer plein de petits rectangles à afficher par-dessus chaque TextBlock :

private void Init()
{
    textBlockChoisi = null;
    TextBlock[] textBlocks = new[] { TextBlock1, TextBlock2, TextBlock3 };

    int gagnant = random.Next(0, 3);
    for (int cpt = 0; cpt < textBlocks.Length; cpt++)
    {
        TextBlock tb = textBlocks[cpt];
        if (cpt == gagnant)
            tb.Text = "Gagné !";
        else
            tb.Text = "Perdu !";
    }

    foreach (TextBlock textBlock in textBlocks)
    {
        double x = (double)textBlock.GetValue(Canvas.LeftProperty);
        double y = (double)textBlock.GetValue(Canvas.TopProperty);

        nbRects = 0;
        for (double j = 0; j < textBlock.Height; j += largeurRectangle)
        {
            for (double i = 0; i < textBlock.Width; i += largeurRectangle)
            {
                double width = largeurRectangle;
                double height = largeurRectangle;
                if (i + width > textBlock.Width)
                    width = textBlock.Width - i;
                if (j + height > textBlock.Height)
                    height = textBlock.Height - j;
                Rectangle r = new Rectangle { Fill = new SolidColorBrush(Colors.Gray), Width = width, Height = height, Tag = textBlock };
                r.SetValue(Canvas.LeftProperty, i + x);
                r.SetValue(Canvas.TopProperty, j + y);
                ContentPanel.Children.Add(r);
                nbRects++;
            }
        }
    }
}

Le principe est de récupérer la position de chaque TextBlock dans le Canvas puis de générer plein de petits rectangles qui couvrent la surface du TextBlock. Chaque rectangle est positionné dans le Canvas et ajouté à celui-ci. Notez que j’en profite pour rattacher chaque rectangle à son TextBlock grâce à la propriété Tag, cela sera plus simple par la suite pour déterminer quel rectangle on peut supprimer. Remarquons au passage que nous utilisons le générateur de nombre aléatoire pour déterminer quel TextBlock est le gagnant.
Viens ensuite le grattage en lui-même. C’est lors du démarrage de la gestuelle drag & drop que nous allons pouvoir déterminer le TextBlock choisi. Comme je l’avais indiqué, j’utilise la méthode FindElementsInHostCoordinates à partir des coordonnées du doigt. Puis, je peux utiliser le même principe lors de l’exécution de la gestuelle, récupérer le Rectangle à cette position et l’enlever du Canvas.

public partial class Grattage : PhoneApplicationPage
{
    […]
    private void GestureListener_DragStarted(object sender, DragStartedGestureEventArgs e)
    {
        if (textBlockChoisi == null)
        {
            aGagne = false;
            Point position = e.GetPosition(Application.Current.RootVisual);
            IEnumerable<UIElement> elements = VisualTreeHelper.FindElementsInHostCoordinates(new Point(position.X, position.Y), Application.Current.RootVisual);
            textBlockChoisi = elements.OfType<TextBlock>().FirstOrDefault();
        }
    }

    private void GestureListener_DragDelta(object sender, DragDeltaGestureEventArgs e)
    {
        if (textBlockChoisi != null)
        {
            Point position = e.GetPosition(Application.Current.RootVisual);

            IEnumerable<UIElement> elements = VisualTreeHelper.FindElementsInHostCoordinates(new Point(position.X, position.Y), Application.Current.RootVisual);
            foreach (Rectangle element in elements.OfType<Rectangle>().Where(r => r.Tag == textBlockChoisi))
            {
                ContentPanel.Children.Remove(element);
            }
        }
    }

    private void GestureListener_DragCompleted(object sender, DragCompletedGestureEventArgs e)
    {
        if (textBlockChoisi != null)
        {
            double taux = nbRects / 3.0;
            double nbRectangles = ContentPanel.Children.OfType<Rectangle>().Count(r => r.Tag == textBlockChoisi);
            if (nbRectangles <= taux)
            {
                if (textBlockChoisi.Text == "Perdu !")
                    MessageBox.Show("Vous avez perdu");
                else
                {
                    aGagne = true;
                    MessageBox.Show("Félicitations");
                }
            }
        }
    }
}

Enfin, quand la gestuelle est terminée, je peux déterminer combien il reste de rectangles qui recouvrent le TextBlock choisi, et au-dessous d’un certain taux, je peux afficher si c’est gagné ou pas. Reste plus qu’à réinitialiser tout pour offrir la possibilité de rejouer :

public partial class Grattage : PhoneApplicationPage
{
    […]
    private void Button_Tap(object sender, System.Windows.Input.GestureEventArgs e)
    {
        if (aGagne)
            nbPartiesGagnees++;
        nbParties++;
        Resultat.Text = nbPartiesGagnees + " parties gagnées sur " + nbParties;
        Init();
    }
}

Et voilà, le grattage est terminé. Passons maintenant à la partie secouage. Coté XAML c’est plutôt simple :

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
        <TextBlock x:Name="ApplicationTitle" Text="Secouage" Style="{StaticResource PhoneTextNormalStyle}"/>
    </StackPanel>

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
        <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition Height="100" />
            <RowDefinition Height="100" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBlock Text="Secouez le téléphone ..." />
        <ProgressBar x:Name="Barre" Grid.Row="1" Visibility="Collapsed" />
        <TextBlock x:Name="Resultat" Grid.Row="2" HorizontalAlignment="Center" />
        <TextBlock x:Name="Total" Grid.Row="3" />
    </Grid>
</Grid>

Tout se passe dans le code-behind, la première chose est d’initialiser l’accéléromètre et le générateur de nombres aléatoires. J’utilise également un DispatcherTimer pour faire mariner un peu l’utilisateur avant de lui fournir le résultat :

public partial class Secouage : PhoneApplicationPage
{
    private Accelerometer accelerometre;
    private const double ShakeThreshold = 0.7;
    private DispatcherTimer timer;
    private Random random;
    private int nbParties;
    private int nbPartiesGagnees;
    private Vector3 derniereAcceleration;
    private int cpt;

    public Secouage()
    {
        InitializeComponent();
        accelerometre = new Accelerometer();
        accelerometre.CurrentValueChanged += accelerometre_CurrentValueChanged;
        timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
        random = new Random();
    }

    protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
    {
        accelerometre.Start();
        timer.Tick += timer_Tick;
        base.OnNavigatedTo(e);
    }

    protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
    {
        accelerometre.Stop();
        timer.Tick -= timer_Tick;
        base.OnNavigatedFrom(e);
    }
}

Il ne reste plus qu’à gérer la détection du secouage. Comme je vous l’ai indiqué, il suffit de comparer la différence d’accélération suivant un axe entre deux mesures. Si celle-ci dépasse un certain seuil, alors il y a un premier secouage, si par contre elle repasse sous un autre seuil, la détection est interrompue. J’ai fixé arbitrairement ces valeurs à 0.8 et 0.1 car je trouve qu’elles simulent des valeurs acceptables. Pour mesurer la différence d’accélération, je prends la valeur absolue de la différence entre la dernière valeur d’accélération sur un axe et la précédente. Si un changement brutal est détecté, alors on incrémente le compteur. S’il dépasse 4 alors nous considérons qu’il s’agit d’un vrai secouage de téléphone.

public partial class Secouage : PhoneApplicationPage
{
    […]
    private void accelerometre_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e)
    {
        Dispatcher.BeginInvoke(() =>
        {
            Vector3 accelerationEnCours = e.SensorReading.Acceleration;

            if (derniereAcceleration == null)
            {
                derniereAcceleration = accelerationEnCours;
                return;
            }
            double seuilMax = 0.8;
            double seuilMin = 0.1;
            int maxSecouage = 3;
            if (cpt <= maxSecouage && (
                Math.Abs(accelerationEnCours.X - derniereAcceleration.X) >= seuilMax ||
                Math.Abs(accelerationEnCours.Y - derniereAcceleration.Y) >= seuilMax ||
                Math.Abs(accelerationEnCours.Z - derniereAcceleration.Z) >= seuilMax))
            {
                Resultat.Text = string.Empty;
                cpt++;
                if (cpt > maxSecouage)
                {
                    cpt = 0;
                    Barre.Visibility = Visibility.Visible;
                    Barre.IsIndeterminate = true;
                    timer.Start();
                }
            }
            else
            {
                if (Math.Abs(accelerationEnCours.X - derniereAcceleration.X) >= seuilMin ||
                    Math.Abs(accelerationEnCours.Y - derniereAcceleration.Y) >= seuilMin ||
                    Math.Abs(accelerationEnCours.Z - derniereAcceleration.Z) >= seuilMin)
                {
                    cpt = 0;
                }
            }
            derniereAcceleration = accelerationEnCours;
        });
    }
}

À ce moment-là, je démarre le timer ainsi que l’animation de la barre de progression. Il ne reste plus qu’à gérer la suite du jeu :

public partial class Secouage : PhoneApplicationPage
{
    […]
    private void timer_Tick(object sender, EventArgs e)
    {
        timer.Stop();
        Barre.Visibility = Visibility.Collapsed;
        Barre.IsIndeterminate = false;
        int nombre = random.Next(0, 3);
        if (nombre == 1)
        {
            // nombre gagnant
            Resultat.Text = "Gagné !";
            nbPartiesGagnees++;
        }
        else
        {
            Resultat.Text = "Perdu !";
        }
        nbParties++;
        Total.Text = nbPartiesGagnees + " parties gagnées sur " + nbParties;
    }
}

Et voilà. Un petit jeu qui nous a permis de plonger doucement dans la gestuelle et dans l’utilisation de l’accéléromètre !

Aller plus loin

Ici, je suis resté très sobre sur l’interface (qui a dit très moche ?). Mais il serait tout à fait pertinent d’améliorer cet aspect-là de l’application. Pourquoi ne pas utiliser des images de dés et des transformations pour réaliser un lancer de dés majestueux ?

De même, plutôt que d’utiliser des rectangles lors du grattage, on pourrait utiliser des images ainsi que la classe WriteableBitmap pour effacer des éléments de l’image…
Je n’en parle pas ici car cela sort de la portée de ce cours, mais c’est une idée à creuser.
En ce qui concerne le secouage, j’ai proposé un algorithme ultra simpliste permettant de détecter ce genre de mouvement. Il existe une bibliothèque développée par des personnes à Microsoft qui permet de détecter les secouages. Il s’agit de la Shake Gesture Library, que vous pouvez télécharger ici. Nous aurons tout à fait intérêt à l’utiliser ici. L’algorithme de détection est plus pertinent et pourquoi réinventer la roue alors que d’autres personnes l’ont déjà fait pour nous.

Exemple de certificat de réussite
Exemple de certificat de réussite