Le premier site francophone dédié au développement Pocket PC

c2iBubbles, un petit jeu en VB.NET
Auteur
Richard Clark
Date 13 janvier 2003
 
   


Petite note de

Ce très bon article est aussi disponible sur l'excellent site c2i.fr de notre ami Richard Clark, l'un des grands spécialistes français de .NET

Introduction

Ceci est mon premier programme pour le Compact .NET Framework, et j'avoue que l'aventure fut passionnante mais surtout, un peu plus ardue que je ne pensais. Enfin, c'est comme tout, c'est la première fois le plus dur, après, c'est de la routine...


Sous Pocket PC
(enfin l'émulateur mais je vous assure que sur mon iPaq c'est pareil)

Sous Windows XP
(ou 2000 ou 98 ou Me ou Tablet PC)

Sous Windows CE 4

Pour développer cette application, j'ai utilisé Visual Studio .NET 2003 en Final Beta ainsi que le Compact .NET Framewor(CF par la suite) qui est maintenant en version Gold (donc redistribuable). Si vous ne désirez que tester l'application sur votre machine, il vous faudra dans un premier temps télécharger et installer ce CF à l'adresse suivante :

http://www.gotdotnet.com/team/netcf/askdotnet/downloadRuntime.aspx

Vous le trouverez en version arm, mips et sh3.

Rappel des règles

Elles sont très simples : quand vous cliquez sur une bulle, un score apparait. Celui-ci est calculé en fonction du nombre de bulles adjacentes de même couleur (verticalement et horizontalement) et est le produit de ce nombre par son nombre moins 1 (n*(n-1)). Ainsi si trois bulles rouges sont adjacentes, vous aurez un score de 3*(3-1) soit 6. Un deuxième clic sur l'une de ces bulles, elles sont détruites et vous êtes crédité alors de ce score et les bulles placées au dessus des bulles détruites descendent sous l'effet de la gravité. Si une colonne entière est vide, les colonnes placées à gauche sont décalées vers la droite (j'ai horreur du vide).

Le jeu se termine s'il n'y a plus de bulle de même couleur adjacente.

NB : mon record actuel réalisé dans le train me ramenant des DevDays de Lyon à 2h du mat est de 1550 points, avis aux amateurs.

Organisons notre travail

Bien entendu, je souhaite développer cette application pour mon iPAQ mais aussi pour mes postes de travail sous Windows XP ainsi que pour Windows CE en version 4 ! Comme je n'ai pas envie de réécrire 15 fois le code, j'ai du organisé quelque peu ma façon de développer.

J'ai donc créé trois projets, un pour le CF (répertoire c2iBubbles), l'autre pour une application Windows traditionnelle (répertoire c2iWinBubbles) et le dernier pour Windows CE 4 (répertoire WPBubbles). Certains fichiers seront communs à ces trois projets, je les ai donc placés dans un autre dossier, Common :

Dans chaque projet, j'ai juste une Form principale, Form1, une Form secondaire pour l'affichage des scores, FormHS, des images gif et les fichiers placés dans le répertoire Common. Vous remarquerez que dans l'explorateur de projet de VS .NET, ils sont représentés avec une petite flèche de raccourci en bas à gauche :

Astuce : pour pouvoir ajouter un fichier dans votre projet sans en faire la copie, il faut faire juste une petite manipulation au moment de l'importation dans votre projet. Je dois avouer que jusqu'il y a 2 jours, j'effectuais cette manoeuvre "à la main", c'est-à-dire en éditant le fichier .vbproj avec le bloc-notes. Heureusement, une personne dont j'ai oublié le nom m'a donné cette astuce sur le newsgroup de Miscrosoft sur .NET : Clic droit sur votre projet et choississez "Ajoutez un Item existant". Sélectionnez votre fichier mais au lieu de cliquer sur "Ajouter", cliquez sur la petite flèche placée à droite du bouton et sélectionnez "Lier le fichier". Et voilà, simple et effeicace pour des fichiers partagés comme c'est le cas ici.

A noter que malheureusement, vous ne pouvez avoir dans VS .NET des projets Pocket PC et WinXP dans une même solution. Dommage cela m'aurait éviter d'avoir trois instances de VS .NET en même temps.

Important : A noter qu'il existe une autre solution plus élégante comme nous le verrons dans un prochain article pour développer sur plusieurs plateformes à la fois un même projet mais je voulais ici pouvoir tester mon application directement sur les trois plateformes.

Les différents fichiers

Form1.vb et FormHS.vb sont les deux forms qui sont spécifiques à chaque plateforme.

Les images 0 à 4.gif sont les différentes bulles de couleur. Ce sont des bitmaps de 16*16 pixels avec une couleur de fond blanche qui nous servira de couleur de transparence. Comme ces fichiers sont livrés en tant que tel avec l'application, l'utilisateur peut très bien créer ses propres bulles du moment qu'elles respectent la taille et la couleur de transparence.

fond.gif est l'image de fond, ici la lune. De même, en la changeant, vous pouvez placer Cindy Crawford en arrière plan.

logo.gif est mon petit logo en haut à droite (on ne le change pas SVP, ca me fait de la pub !).

bubbles.gif et ico.ico ne servent à rien pour l'instant (j'ai oublié de les supprimer de la solution).

score est un petit bitmap de 16*16 qui est affiché quand on souhaite connaitre le score quand on détruit des bulles (dans la capture plus haut de l'émulateur Pocket PC, c'est la bulle marron ou apparait le score 30).

Les autres fichiers, donc ceux avec la flèche de raccourci, contiennent les types de l'application que nous allons décrire de ce pas.

Les différents types utilisés

Cell (dans le fichier Tableau.vb)

Tout d'abord, chaque cellule est de type Cell (normal quoi) avec les membres suivants :

Col et Row indiquant les numéros de colonne et ligne de la cellule,
Color indiquant la couleur de la cellule (-1 si elle est vide),
Enfin Checked de type booléen qui nous servira par la suite pour évaluer les scores.

Tableau (dans le fichier Tableau.vb)

Le Tableau de jeu de type Tableau possédant une propriété principale Cells : c'est un tableau à deux dimensions contenant des Cells :

Nous verrons plus en détail l'ensemble de ses membres plus tard mais regardons tout d'abord son constructeur :

Public Sub New(ByVal inColor As Int32, ByVal iTaille As Int32)
      _inColor = inColor 'nombre de couleurs utilisés
      _iTaille = iTaille     'taille du tableau
      ReInit()
End Sub

_inColor permet de déterminer le nombre de couleur utilisées. Dans tous mes projets je l'initialise à 5 mais cela laisse la porte ouverte pour créer des tableaux avec 245 000 couleurs différentes !!!
_iTaille (accessible via la propriété en lecture seule Taille) est le nombre de colonne et de ligne du tableau. Fainéant comme je suis, j'ai décidé que le tableau était carré mais rien ne vous empêche par la suite d'effectuer quelques modifications pour changer cela. Dans mes projets finaux, la taille sera toujours égale à 15.
J'appele ensuite la méthode ReInit qui me permet de réinitialiser le tableau, c'est-à-dire de remplir le tableau à deux dimensions Cells de Cell de couleurs différentes. Le seul problème de cette méthode est qu'il faut que chaque couleur ai le même nombre de bulle (à un chouia près si le nombre totale de bulle n'est pas divisible par le nombre de couleur). L'astuce que vous verrez dans le code téléchargeable plus bas consiste :

  • Dans un premier temps à créer un ArrayList contenant des références vers les cellules du tableau.
  • Ensuite, on calcule le nombre NColor de cellule d'une couleur (NColor = Taille * Taille / NbrCouleur).
  • Puis pour chaque couleur, on fait un For...Next allant de 0 à NColor - 1. Dans cette boucle, on prend au hasard une cellule de l'ArrayList et on lui affecte la couleur en cours. On retire ensuite la cellule de l'ArrayList.
  • Dernière étape, si le carré de la Taille n'est pas divisible par le nombre de couleur, il reste encore des Cell dans l'ArrayList. On détermine alors au hasard leur couleur.

Bon, le plus simple est de regarder le code en détail ;-)

Voici quelques uns des autres membres de ce type :

  • getScore renvoi le score si l'on souhaite détruire la bulle de coordonnée iRow, iCol. Ce score est calculé grâce à la méthode récursive CheckNext. Vous verrez que c'est dans CheckNext que j'utilise la propriété Checked d'une bulle pour savoir si celle ci a déjà été analysée. De plus, si la bulle analysée est de la bonne couleur, je l'ajoute dans l'ArrayList CellsChecked. Ainsi, si l'utilisateur reclique sur une bulle faisant déjà parti de cette ArrayList, je sais que l'utilisateur souhaite la détruire (et obtenir le score correspondant).
  • DeleteBubble renvoi le score quand on détruit des bulles. Concrètement, il appele dans un premier temps la méthode getScore pour remplir l'ArrayList CellsChecked et obtenir le score. Puis, via une itération, il détruit les bulles de l'ArrayList (c'est à dire qu'il met leur couleur à -1). Enfin, il appele la méthode ReorganizeTableau pour ... réorganiser le tableau, c'est à dire déplacer les bulles du dessus vers le bas. Cette dernière méthode est intéressante car elle fait appel à la collection Queue livrée avec le Framework (FIFO). Je parcours en effet l'ensemble des bulles d'une colonne et si sa couleur est distincte de -1, je l'ajoute dans la Queue. Ensuite, je sors de ma Queue les bulles les unes après les autres et je les place dans la même colonne.
  • Enfin, IsMoreToCome regarde rapidement s'il existe encore des bulles adjacentes dans le tableau. Nous verrons plus loin ce qu'il fait si ce n'est pas le cas (ie, fin de la partie).

GameUIManager (dans le fichier GameUIManager.vb)

Maintenant, concentrons nous sur le type GameUIManager.

C'est lui qui est au coeur du développement inter-plateforme. Le constructeur de ce type attend comme argument une référence vers un System.Windows.Forms.Form. Dans ce dernier, j'abonne un certain nombre de méthodes aux évènements de la Form :

Public Sub New(ByVal frmParent As System.Windows.Forms.Form)
      _frmParent = frmParent 
      'ajout des callbacks
      AddHandler _frmParent.Paint, AddressOf frmPaint AddHandler
      _frmParent.MouseUp, AddressOf frmMouseUp
      AddHandler _frmParent.Closed, AddressOf frmClosed 
     '...
End Sub

Ainsi, quand la form (sur Pocket PC, Win32 ou Win CE) devra être repainte, la méthode frmPaint de mon GameUIManager sera appelée. De même pour le MouseUp et le Closed. Ce qui signifie que dans mes Forms (spécifiques à la plateforme), je n'ai que deux lignes de code et une déclaration :

Public Sub New()
      MyBase.New() 
      InitializeComponent()
      myInit()
End Sub

Private WithEvents oGameUIManager As GameUIManager

Private Sub myInit()
     oGameUIManager = New GameUIManager(Me)
End Sub

Ce sont les méthodes du GameUIManager qui gèreront les évènements de ma form. Pourquoi je fais cela ? Tout simplement parce que je laisse la possibilité au designer des applications d'ajouter des contrôles qui peuvent être spécifiques à la plateforme ciblée. L'exemple le plus visible est le menu. Vous verrez que le constructeur utilisé automatiquement par Visual Studio .NET pour un menu Windows est différent de celui pour un Pocket PC.

Ensuite, car ce n'est pas fini avec le constructeur du GameUIManager, je crée une instance d'un tableau (il était temps) et je charge dans des objets Bitmaps les différentes images qui seront utilisées pour le fond, les bulles, le logo, etc.

Juste une remarque pour le chargement des images : j'utilise pour cela le constructeur du type Bitmap qui attend le nom du fichier. Si vous êtes sous Windows XP & Co, vous obtenez le chemin de l'exécution de l'application simplement avec le code suivant :

System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly.Location)

Malheureusement sous PocketPC, la propriété Location n'existe pas. Damned, comment faire ? Et bien, c'est possible avec la ligne suivante :

System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly.GetName.CodeBase)

Mais attendez, là ou cela se corse comme dirait Tino Rossi, c'est que cette dernière ligne ne fonctionne pas sous Windows XP ! Du coup, on est obligé, pour ce type qui, je le rappelle, sera utilisé dans des projets WinXP, PocketPC et WinCE4, d'utiliser la compilation conditionnelle. Il suffit pour cela dans les propriétés du projet d'ajouter une constante personnalisée que j'ai appelée PPC :

Dans mes projets PocketPC et WinCE4, PPC vaudra True mais dans mon projet WinXP, il sera à False. Enfin, pour déterminer mon chemin de l'application, j'aurais le code suivant :

#If PPC Then
 _sCurrentPath = _ 
 System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly.GetName.CodeBase)
#Else 
 _sCurrentPath = _ 
 System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly.Location)              
#End If

De la bonne compilation conditionnelle comme au bon vieux temps de VB4 avec les Win16 et Win32 !

Affichage des bulles

Une fois que j'ai donc mes bitmaps, il va falloir les afficher ou plus exactement afficher les Cells du Tableau. Le principe est donc de parcourrir l'ensemble des éléments de type Cell de la propriété Cells de mon type Tableau. Pour pouvoir exécuter cela le plus rapidement possible, il n'est pas question de dessiner les bitmaps directement dans la form mais d'utiliser un Bitmap placé en mémoire. C'est le rôle de la fonction PaintOffBitmap. Comme avec DirectX, on fait tout le travail du dessin sur une image en mémoire puis, une fois le travail effectué, on fait une copie de cette image sur notre form. L'évènement Paint de notre form (ou plutôt la procédure frmPaint du GameUIManager appelée par le délégué Paint de la form) ne tient donc qu'en une seule ligne :

Protected Sub frmPaint(ByVal sender As Object, _ 
 ByVal e As System.Windows.Forms.PaintEventArgs)              
 
 e.Graphics.DrawImage(_oBitmapOff, 0, 0) 


End Sub

Ou _oBitmapOff est le bitmap en mémoire.

La procédure PaintOffBitmap, qui dessine sur le _oBitmapOff, est un peu plus compliquée (regardez donc le code à télécharger) mais elle fait juste dans un premier temps une itération sur les Cells du Tableau pour les dessiner puis dessine les cadres des Cells sélectionnées (collection CellsChecked) puis la prévisualisation du score et enfin le score.

Gestion du clic sur l'écran

On gère donc le clic sur l'écran via la procédure frmMouseUp du GameUIManager, procédure s'étant abonnée à l'évènement de la form. Rien de bien compliqué dans cette procédure puisque l'on transforme dans un premier temps les coordonnées du clic en numéro de ligne et de colonne pour savoir sur quelle Cell on a cliqué. Mais il faut savoir si c'est un clic qui affiche un score ou qui confirme la suppression des bulles.

Souvenez-vous, quand j'appele la méthode getScore, je remplie un ArrayList, CellsChecked. Donc, dans le MouseUp, je fais une itération sur l'ensemble des bulles de cette collection :

  • Si elle est présente, c'est que l'utilisateur l'a déjà sélectionné et qu'il souhaite donc la détruire : j'appelle la méthode DeleteBubble (qui réorganise le tableau) puis je fais la mise à jour de l'écran avec PaintBitmapOff,
  • Si ce n'est pas le cas, c'est que l'utilisateur souhaite connaitre le score s'il veut détruire cette bulle. J'appelle donc la méthode getScore pour calculer le score (et qui remplit CellsChecked).

Regardez donc le code en détail !

En revanche, il faut gérer le fait que le tableau est terminé, le fameux GAME OVER. A ce moment, une nouvelle feuille devrait s'afficher avec les scores ou l'enregistrement du HALL OF FAME. Seul problème, cette analyse est effectuée dans notre GameUIManager donc indépendant de l'interface utilisateur, ie la plateforme cible. Moralité, on va délégué le travail à notre form appelante. Ceci explique pourquoi dans la form principale, notre GameUIManager est déclaré avec le mot clé WithEvents :

Private WithEvents oGameUIManager As GameUIManager

Nous allons donc ajouter un évènement au GameUIManager. Pour des raisons qui ne sont pas obscures mais que je ne dévoilerais pas ici (achetez donc mon livre "Au coeur de VB .NET" chez MS Press), j'ajoute le code suivant :

Protected Sub frmMouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
     '...
     If Not (_Tab.IsMoreToCome) Then 'si MoreToCome vaut False, GAME OVER
          OnTableauTermine(EventArgs.Empty)
     End If
End Sub

'déclaration de l'évènement
Public Event TableauTermine(ByVal sender As Object, ByVal e As EventArgs)

Protected Sub OnTableauTermine(ByVal e As EventArgs)
     'déclenchement de l'évènement
     RaiseEvent TableauTermine(Me, e)
End Sub

Du coup, dans la form principale, je peux gérer cet évènement :

Private Sub oGameUIManager_TableauTermine(ByVal sender As Object, _
 ByVal e As System.EventArgs) Handles oGameUIManager.TableauTermine
                  
 Dim bShowSaisie As Boolean = False
 If oGameUIManager.HighScores.IsHighScore(oGameUIManager.Score) Then bShowSaisie = True 
 
 Dim ofrmScore As New frmHS(oGameUIManager.HighScores, oGameUIManager.Score, True) 
 ofrmScore.ShowDialog() 
 ofrmScore.Dispose() 

End Sub

La seule chose qu'il faut retenir, c'est que c'est à la form de créer une nouvelle form. Dans le projet WinCE 4 par exemple, j'ai juste décidé d'afficher le score obtenu. Dans les projets Pocket PC et WinXP en revanche, j'ai décidé d'afficher et de stocker un HALL OF FAME. C'est le sujet de cette dernière partie et le rôle du type HighScore :

HighScore (fichier HighScore.vb)

C'est ce type qui est en charge de gérer le HALL OF FAME. Il possède les propriétés Min et Max qui sont suffisamment explicites pour que je ne vous dise pas ce qu'elles retournent.

IsHighScore indique si l'Int32 passé en argument est un HighScore, c'est-à-dire supérieur à l'un des scores déjà enregistré.

Add enfin ajoute un nouveau HighScore avec le nom de la personne qui l'a réalisé. Il enregistre également la date et l'heure de cette performance historique ;-) . Mais justement, je parle d'enregistrement et c'est là ou intervient la sérialisation sous Pocket PC.

La sérialisation sous Pocket PC

Il va falloir donc stocker les meilleurs scores. Ma première idée a donc été de chercher un sérialisateur de type binaire, XML ou, au pire SOAP (dans le genre verbeux...) comme ceux qui existent pour le .NET Framework. Ne cherchez pas comme je l'ai fais : y'en a pas ! C'est d'autant plus bizarre que l'on peut consommer sans problème des services web avec le Compact Framework ! Tant pis, pas de sérialisateur.

Deuxième idée, utilisez System.IO et enregistrer mes données dans un fichier au format propriétaire (ici, type texte). Sympa mais je n'avais toujours pas touché à l'espace de noms System.Data et c'était l'occasion de m'y mettre. J'ai donc décidé d'utiliser un DataSet et plus particulièrement ses méthodes ReadXML et WriteXML.

Les données sont donc stockées dans un fichier XML de données lu par le DataSet et modifié par ce dernier. C'est pourquoi le constructeur de ce type HighScore attend le nom du fichier.

  • Il regarde dans un premier temps s'il existe (System.IO.File.Exists(FileName)). Si ce n'est pas le cas, il ajoute dans le DataSet une nouvelle DataTable avec les colonnes Name (String), Score (Int32) et DateOccur (DateTime) et la remplie avec 5 lignes (DataRow) avec des valeurs initiales (Anonymous, 501, date de création du fichier).
  • S'il existe, il charge le DataSet avec sa méthode ReadXML.

Du coup, dans la méthode Add, je vérifie dans un premier temps si le score que l'on souhaite ajouté est bel et bien un high score puis et si c'est le cas, je modifie le score le plus bas (c'est-à-dire la DataRow correspondante obtenue avec la méthode privée getMinRow). Je sauvegarde alors le tout avec la méthode WriteXML du DataSet.

Distribution

Maintenant que notre programme est terminé, il va falloir le distribuer. La première solution consiste simplement à copier les fichiers nécessaires (ie les gif et l'exe) dans un répertoire du Pocket PC pour que cela fonctionne, mais il n'apparaitra pas alors dans la liste des programmes installés.

Pour cela, là encore, VS .NET 2003 nous facilité grandement la tâche : un seul clic suffit ! Dans la barre d'outils Device, vous verrez un bouton "Build Cab File". Cliquez dessus et il nous compile alors automatiquement les CAB pour tous les POcket PC possibles (arm, mips et sh3). Simple non ?


Sous Pocket PC
(enfin l'émulateur mais je vous assure que sur mon iPaq c'est pareil)

Sous Windows XP
(ou 2000 ou 98 ou Me ou Tablet PC)

Sous Windows CE 4

Ces codes sont en libre service. Vous pouvez en faire ce que vous voulez, les modifier, les triturer dans tous les sens SAUF : si vous créez une nouvelle version, celle ci doit aussi est libre de droit et devra mentionner le programme d'origine (et un lien vers cet article). Avertissez moi aussi, ça me fera plaisirs ;-)

Téléchargement obligatoire du Compact .NET Framework pour Pocket PC ARM, MIPS, SH3 (environ 2 Mo)

Installation et exécutable pour ARM (104 Ko)

Installation et exécutable pour MIPS (107 Ko)

Installation et exécutable pour SH3 (103 Ko)

Codes sources pour VS .NET 2003 pour Pocket PC, WinCE 4 et Windows (215Ko)

Conclusion

En quelques heures, ce petit programme a donc pu être développé sans problèmes pour les différentes plateformes. Mais ne vous inquiètez pas, il reste pas mal de choses à voir sur les Pocket PC, ce qui sera l'objet, of course, d'autres articles.


Richard Clark

 

 

 
   

Copyright 2001-2004 - Tous droits réservés
Toutes les autres marques et produits présents dans ces pages sont la propriété exclusive de leurs sociétés respectives.