Un PacMan avec VB.NET. Tutoriel POO
Un PacMan avec VB.NET. Tutoriel POO
Richard Clark
(Vu 16992)
Français Visual Basic .NET ASP .NET Expert Article

Ce jeu de PacMan n'est absolument pas optimisé. Mais son but est de vous montrer les nouveautés de la programmation orientée objet de VB.net

PacMan is back

Bon, autant vous prévenir tout de suite, le code qui vous est présenté ce mois-ci n’est absolument pas optimisé et défit même par moment les règles élémentaires de programmation (et en plus, le jeu n’est pas terrible alors…). Mais il a l’avantage de vous présenter plusieurs Namespaces et certaines nouvelles fonctionnalités de VB.NET, bref il n’est présent que dans un but pédagogique.

Vous allez donc créer, grâce à VB.NET, une énième version du PacMan
(comme dirait Droopy, « I’m happy »).

Toutes les classes que vous pouvez manipuler avec VB.NET sont sagement rangées dans des Namespaces ou espaces de nom (dans un même Namespace, deux classes du même nom ne peuvent cohabiter).

La plate-forme Microsoft.NET nous livre nombre de Namespaces qui contiennent eux-même, nombre de classes. Une bonne partie de la maîtrise de VB.NET va donc consister à bien connaître ces Namespaces et savoir utiliser les bonnes classes au bon endroit.

La structure de l’application

Regardons maintenant comment vous allez organiser votre application. Vous avez à dessiner la carte, le PacMan, les fantômes et à animer le tout. Vous allez créer les classes suivantes :

  • cSprite, une classe qui dessine un élément mouvant,
  • cPacMan pour le PacMan, qui hérite de cSprite,
  • cFantome pour un fantôme, qui hérite de cSprite,
  • cFantomes, une classe collection qui contient l’ensemble des fantômes,
  • cSquare pour un élément du décor,
  • cSquares, une classe collection qui contient l’ensemble des éléments de décors,
  • cAppli, la classe à la tête de votre hiérarchie.

Au début de votre application, vous allez créer une instance de votre classe cAppli. Dans son constructeur, vous passez en paramètres la Form ou vous affichez votre jeu ainsi que le nom du fichier où se trouve la carte :

Public Sub New(ByVal FormA As Form1, ByVal sMapFile As String)

Dans le constructeur, vous allez lire le fichier contenant la carte et créer les instances de tous les objets dont vous avez besoin

Commençons tout de suite l’exploration  très fort avec le NameSpace System.IO pour la lecture de la carte.

Lecture de la carte

Chaque niveau de votre jeu va être stocké dans un fichier texte. Pour concevoir un niveau, utilisez simplement Excel en créant un tableau de 19 lignes par 19 colonnes. Dans chaque cellule, placez des 1 pour les murs, 0 pour du vide, 5 pour l’emplacement de départ du PacMan, 4 pour les fantômes (les méchants), etc. Puis, sauvegardez le tout dans un fichier au format cvs (on dirait une recette de cuisine ! ). Ce qui vous donne un fichier du genre :

1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1
1;5;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;4;1
1;0;1;1;1;0;1;0;1;0;1;0;1;0;1;1;1;0;1


1;6;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;4;1
1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1

La lecture de ce fichier va vous permettre de remplir votre collection cSquares d’objets cSquare.

Pour pouvoir lire un fichier au format texte, vous utilisez un objet nommé StreamReader qui fait partie de System.IO.

Dans System.IO, vous avez des classes qui vous permettent de lire et écrire le contenu de fichiers soit au format binaire (BinaryReader, BinaryWriter), soit au format texte (StreamReader, StreamWriter). C’est ce qui nous intéresse ici mais il y a également des classes qui vous permettent d’obtenir des informations sur les fichiers (File) et les dossiers (Directory) et de les renommer, supprimer, déplacer, etc. Ces derniers remplacent les classes de la référence FileSystemObject.

Pour ouvrir votre fichier, il vous faut créer une instance de la classe StreamReader. Celle-ci possède 9 constructeurs différents mais vous allez utiliser le plus simple qui demande juste le nom du fichier :

Dim oStreamR As New System.IO.StreamReader(sMapFile)

Puis pour lire une ligne :

sRead = oStreamR.ReadLine

A la fin de votre fichier, ReadLine retourne un String vide.

La classe cSquares est une simple classe collection (cf article du mois dernier). Chaque élément du décors (de type cSquare) peut êre un mur, un bonus, un superBonus, etc. Pour une meilleure lisibilité de votre code, vous allez définir une nouvelle énumération définie dans cSquares :

Public Enum eSquareTypes
        Vide = 0
        Mur = 1
        Point = 5
        Bonus = 2
        SuperBonus = 3
End Enum

Rien de bien différent par rapport à VB6 mais attention, en utilisant cette énumération, vous définissez un nouveau type. Cela signifie que si vous tapez le code suivant :

Dim i as Integer = 1
objSquare.SquareType = i      ‘un mur

Le compilateur en mode Strict On va générer une erreur. Pour éviter cela, il faut convertir le type Integer en type eSquareTypes :

Dim i as Integer = 1
objSquare.SquareType = cType(i,eSquareTypes)         ‘un mur

Pour ajouter un élément au décor, vous allez définir la méthode Add suivante :

Public Function Add(ByVal X As Integer, ByVal Y As Integer, ByVal SquareType As eSquareTypes, ByVal PoidsA As Integer) As cSquare

X et Y sont la position de l’élément, SquareType le type et Poids son poids (il nous servira par la suite pour le calcul de la trajectoire des fantômes).

Je vous laisse le soin de regarder le code qui permet d’extraire les informations d’une ligne qui est somme toute classique. N’oubliez pas tout de même de fermer votre fichier :

oStreamR.Close()

Vous aurez remarqué que pour créer mon objet, vous avez été obligé de taper System.IO.StreamReader. Vous êtes de grand fainéant comme moi (si, si) ? Vous pouvez dire au compilateur VB que dans le fichier où est écrit ce code, vous allez utiliser le Namespace System.IO. De la même façon, vous allez le prévenir que vous souhaitez utiliser les déclarations qui sont dans cSquares (l’énumération entre autres). Pour cela, ajoutez les lignes suivantes en tête de votre fichier :

Imports System.IO
Imports c2iPacMan.cSquares

Attention, il ne s’agit pas d’ajouter une nouvelle référence à votre projet, mais bel et bien de dire au compilateur que le code qui suit (dans ce fichier .vb) utilise ce Namespace.

Pour les fantômes et le PacMan, lors de leur création, vous indiquez juste leurs positions et couleurs.

Une fois tous les objets instanciés, il reste à les dessiner le tout sur votre Form. Pour cela, vous allez avoir besoin d’un autre  NameSpace : System.Drawing.

Un peu de dessin

Le dessin n’a jamais été le fort de Visual Basic. J’en veux comme exemple l’aberration pour dessiner un rectangle plein :

Form1.Line (5,10) – Step(10,10),,BF

Gasp ! Comme programmation orientée objets, on faisait mieux et encore, je passe sur les Twips, AutoRedraw & Co. Pour dessiner dans une Form, il vous faut dans un premier temps obtenir l’objet où vous allez dessiner. Sous Win32, il était appelé Devicecontext. Sous MS.NET, c’est un objet Graphics qui fait partie de System.Drawing. C’est lui qui possède toutes les méthodes dont vous avez rêvé pour dessiner toutes les formes que vous voulez. Vous pourrez ainsi dessiner des lignes, des ellipses, des rectangles (classique, quoi) mais plus étonnant des courbes de Beziers, des camemberts, des images, des icônes, etc.

Comment l’obtenir ? La meilleure façon (et la plus sûre) pour récupérer le graphics d’une Form est d’utiliser l’événement Paint. Ce dernier vous renvoie l’objet e de type PaintEventArgs qui possède comme propriété l’objet Graphics ou vous souhaitez dessiner :

Public Sub Form1_Paint(ByVal sender As Object, ByVal e As System.WinForms.PaintEventArgs) Handles Form1.Paint
Dim gForm as Graphics
gForm = e.graphics

End Sub

Il existe une autre méthode que vous utiliserez dans ce projet qui obtient l’objet Graphics d’un objet à partir de son Handle :

Dim gForm as Graphics
gForm = Graphics.FromHWND(mForm.Handle)

Il est très important d’obtenir cet objet juste avant le moment où l’on va s’en servir car il change en permanence notamment quand vous redimensionnez la form. Vous comprenez maintenant pourquoi l’objet Form est une propriété de votre classe cAppli.

Maintenant, vous allez devoir dessiner des rectangles pleins en noir  (pour les zones vides) et des cercles pleins (pour les bonus). Pour cela, vous devrez utilisez des Pens (stylos) et des Brushes (pinceaux).

Toutes les méthodes de Graphics qui dessinent des lignes commencent par Draw et utilisent un Pen.

Toutes les méthodes qui dessinent des formes pleines commencent par Fill et utilisent un Brush. Un Pen de couleur noire se défini ainsi :

Dim oPen as New Pen(Color.Black)

Puis pour dessiner une ligne noire du point (10,10) au point (50,50) :

gForm.DrawLine(oPen,10,10,50,50)

Dans votre cas, ce sont des zones remplies donc vous allez travailler avec des Brushes. Par exemple, pour un cercle de 10 de rayon au point (5,5), vous devez écrire :

gForm.DrawEllipse(New SolidBrush(color.Black), 5, 5, 10, 10)

Le pinceau utilisé est solide, cela signifie que le remplissage est plein, mais vous pouvez aussi utiliser des textures grâce à la classe TextureBrush. C’est ce que vous ferez pour dessiner les murs. Pour cela, le constructeur d’un TextureBrush attend un objet Image qui peut être créé à partir d’un fichier bitmap :

Dim mBitmapMur as New Bitmap(“mur.bmp”)
e.FillRectangle(New TextureBrush(mBitmapMur), 0, 0, 50, 50)

Dans ce cas, on texture un rectangle de 50 pixels de large et de hauteur à partir du fichier mur.bmp.

Pour dessiner une image, c’est aussi simple. Sous VB6, il fallait charger une image dans un PictureBox invisible et de faire des BitBlt ou des PaintPicture sur la Form de destination. On pouvait également utiliser des images en mémoire uniquement, mais cela nécessitait une bonne maîtrise des API. Avec VB.NET, no problemo (citation de Homer. S.) :

e.DrawImage( mBitmapPacMan, 10,10)

Ou, vous l’aurez deviné, mBitmapPacMan est un objet Image.

Bien, vous savez maintenant charger le fichier grâce à System.IO, dessiner votre décor, votre PacMan et vos fantômes (grâce à System.Drawing). Pour les détails, je vous laisse le soin de regarder dans le code sur le CDROM)

En ce qui concerne votre Pacman et vos fantômes, pour que ce soit plus sympathique, il faut les animer. La solution retenue à été d’alterner l’objet Image utilisé toutes les x millisecondes (ces dernières sont en réalité stockées dans une collection ArrayList). Rien de bien compliqué sauf qu’il faut générer cet événement toutes les x millisecondes. Vous pourriez utiliser un contrôle Timer dans votre Form comme sous VB6, mais cela lierait votre objet cPacMan (et cFantome) à votre Form et nuierait à la suite de cet article. Heureusement, vous pouvez maintenant instancier un Timer dans une classe et surtout, s’abonner à son événement : plus besoin de contrôle.

Les évènements

Il existe deux sortes de Timers sous VB.NET. Un dans le NameSpace System.Winforms (le contrôle Timer traditionnel ) et un dans System.Threading. C’est ce dernier que vous allez utiliser. Pour s’abonner à son événement, vous allez utiliser la délégation (nous reviendrons plus en détail dans un autre numéro sur la délégation et les évènements avec VB.NET) grâce à un objet TimeCallBack.

Dim oTCallBack as New TimerCallback(AddressOf Me.Change)
Dim objTimerChange As New Timer(oTCallBack, Me, 50, 0)

Grâce au constructeur de l’objet TimerCallback, vous indiquez que la procédure Change de l’objet où il est instancié (Me) doit être appelée. L’événement généré par le Timer exécutera la procédure Change. Puis vous abonnez votre objet au callback du nouveau Timer. Contrairement au Timer traditionnel qui génère un événement qui se répète régulièrement, le Timer de Threading génère l’événement (au bout de 50ms dans notre cas) puis se détruit. Si l’on veut de nouveau rappeler la procédure Change 50 ms après, il faut recréer un nouveau Timer :

Private Sub Change(ByVal sender As Object)

Dim objTimerChange As New Timer(New TimerCallback(AddressOf Me.Change), Me, 50, 0)
End Sub

Il est important dans la déclaration de Change d’ajouter le paramètre sender as Object pour que la délégation fonctionne.

Petite remarque concernant la beta 1 de MS.NET Framework : au début du développement de cette application, j’avais envisagé de créer un Threading.Timer pour chaque Sprite animé. Malheureusement, au bout de quelques secondes, il perd un peu les pinceaux et les évènements ne sont plus générés. On se limitera donc à un seul Timer pour l’instant.

Deuxième remarque, l’implémentation du Threading.Timer ne fonctionne que sous Windows 2000 pour l’instant.

Tant qu’on y est avec les évènements, vous allez ajouter un événement à votre classe PacMan : Score_Changed. Dès que notre cher PacMan mange des bonus, son score est augmenté. Pour en avertir l’application, vous décidez de créer un événement dans sa classe. Pour la création et la génération de l’événement, rien ne change en apparence :

Public Event Score_Changed(ByVal iScore As Integer)

Public Sub Change()

RaiseEvent Score_Changed(iScore)

End Sub

Pour pouvoir s’abonner à cet événement, la classe (ici cAppli) doit d’abord en avertir le compilateur avec la ligne classique :

Private WithEvents mHero As cPacMan

En revanche, la déclaration de l’évènement change quelque peu :

Public Sub mHero_Score_Changed(ByVal iScore As System.Integer) Handles mHero.Score_Changed

Vous reprenez la définition de l’évènement et pour assurer la délégation, vous utilisez le mot clé Handles en précisant quelle fonction est gérée. Cela se passe exactement de la même façon pour capturer les évènements des contrôles dans une form :

Dim WithEvents Form1 As System.WinForms.Form

Public Sub Form1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Form1.Click

End Sub

Hiérarchisons un peu le tout

Je vous ai dit au début de cet article que les classes cPacMan et cFantome héritaient d’une classe cSprite. Il est temps maintenant de s’expliquer plus avant. Si l’on considère ces deux objets graphiques, tous deux possèdent des propriétés et des méthodes qui se ressemblent étrangement. On doit dessiner un bitmap, ils ont une position X, Y tous les deux, ils se déplacent de la même façon, c’est-à-dire qu’ils évitent les murs, etc. Donc plutôt que de dupliquer le code dans ces deux classes, vous allez créer la classe cSprite qui contiendra les membres communs. Le code dans les classes qui en hériteront sera donc bien spécifique à leur nature. Pour qu’une classe hérite d’une autre (attention, contrairement au C++, les classes MS.NET ne peuvent hériter que d’une seule classe), juste après la définition de la classe, ajoutez la ligne suivante :

Public Class cPacMan
    Inherits cSprite

Il y a beaucoup de membres communs si l’on se penche en détail sur le programme mais nous ne regarderons ici que quelques uns d’entres eux.

Les membres Public

Les propriétés X et Y qui représentent la case où se trouve le sprite doivent être accessibles à l’extérieur de la classe. Définissez les ainsi dans la classe cSprite :

Private mX as Integer
… 
Public Property X() As Integer
        Get
            Return mX
        End Get
        Set
            mX = Value
        End Set
End Property

Comme cPacMan hérite de cSprite, vous pourrez écrire :

ObjPacMan.X = 5

Et c’est le code dans la classe parente qui sera exécutée.

C’est le cas le plus simple.

Les membres protégés

Maintenant, comment créer des membres qui sont privées à la classe mais qui peuvent être utilisées dans les classes filles. Si vous définissez une variable comme Private dans la classe cSprite, celle-ci ne sera visible que dans les objets cSprite. Pour remédier à ce problème, vous allez les déclarer comme Protected. Par exemple, pour la taille du sprite qui n’est pas visible à l’extérieur de la classe :

Protected mWidth as Integer
Protected mHeight as Integer

C’est le cas également pour la méthodes CalculPosition :

Protected Function CalculPosition() As Boolean

Ces membres, définis dans la classe cSprite sont visibles dans la classe cPacMan mais invisibles à l’extérieur de la classe.

Redéfinition d’un membre

Dans certains cas, vous allez devoir redéfinir des membres. Pour vos deux classes, vous devez à chaque appel du Timer afficher le sprite, le code est donc commun. Mais pour le PacMan, vous en profitez pour réactualiser son score et pour les fantômes, calculer leurs trajets. Il faut donc avertir le compilateur que la méthode commune (Change) doit pouvoir être redéfinissable grâce au mot clé Overridable :

Public Overridable Sub Change()

Vous redéfinnissez la procédure Change de la classe cPacMan avec le mot clé Overrides. Comme vous devez faire appel à la méthode Change de la classe parente, vous utilisez MyBase, qui fait référence à la classe parente puis vous ajoutez son code spécifique :

Public Overrides Sub Change()
        MyBase.Change()
        … ‘code spécifique
End Sub

Classes internes

Si vous avez lu l’article du mois dernier sur les classes internes du JDK 1.1 de Java par Philippe Prados, sachez que maintenant, avec VB.NET, vous pouvez définir une classe au sein d’une autre classe. Si vous la définissez comme Private, elle ne sera visible que par les membres de la classe. C’est ce que vous allez faire avec la classe Delta qui représente la position du sprite dans une case :

Public Class cSprite

     Private Delta
     …
     End Class
End Class

C’est une solution avantageuse par rapport à la définition de structure privée puisqu’elle apporte toute la puissance de la programmation orientée objet.

Rendons nos fantômes intelligents

Comme je vous l’ai dis plus haut, au moment où l’on dessine un fantôme (Change), vous calculez sa trajectoire. Ce calcul peut-être très long et il peut être intéressant de l’effectuer dans un autre thread. Bon, je l’avoue, l’implémentation du multithread dans ce programme n’est là juste qu’à titre pédagogique et pourrait être évitée. Faire cela avec VB6 était un véritable casse-tête qui, il faut l’avouer, ne marchait pas (si un lecteur à un exemple qui fonctionne, je suis preneur mais très sceptique). Sous VB.NET, deux lignes suffisent :

Dim t As New Thread(AddressOf Me.CalculTrajectoire)
t.Start()

Plus simple, je ne pense pas que ce soit possible. Dans le NameSpace System.Threading, vous créez un objet Thread en indiquant l’adresse de la procédure à exécuter (ici, CalculTrajectoire). Puis, pour démarrer le nouveau thread, vous utilisez sa méthode Start. Il existe d’autres méthodes qui vous permettent par exemple d’interrompre ou de mettre en veille l’exécution d’un thread de différentes façons(Abort, Interrupt, Stop, Sleep).

Une fois votre Thread créé, vous allez devoir calculer la trajectoire de vos fantômes. La solution retenue est l’implémentation de l’algorithme A* que nous vous avons présentée dans un précédent article. D’accord, c’est un peu comme utiliser un Boeing 747 pour transporter une lettre mais ce qui est intéressant dans ce cas précis, c’est que le code existait déjà sous VB6.

On en arrive au cas délicat de la migration de projets.

VB.NET est livré (pour la Beta 1) avec un wizard qui effectue la migration de vos projets VB6. Quand on pose la question à Microsoft sur l’efficacité de ce Wizard, la réponse est étonnante :

« si ca marche, tant mieux »

Je pense que la question n’est pas la bonne, je dirais plutôt, pourquoi migrer ? Votre projet fonctionne parfaitement sous VB6, pourquoi allez-vous passer 6 mois de déboggage pour simplement utiliser VB.NET ? De plus, vous n’allez certainement pas profiter des avantages de MS.NET. Je reprends l’exemple du mois dernier sur les collections. Votre projet utilise un objet collection de VB6. Si vous migrez celui-ci sous VB.NET, il utilisera un objet collection de compatibilité avec VB6 qui est largement moins performant que les nouveaux objets collections de MS.NET.

Dans le cas de l’implémentation de l’algorithme A*, la migration du code s’est effectuée sans aucun problèmes mais en analysant le code, il a fallu réécrire une bonne partie de celui-ci pour pouvoir profiter de tous les avantages de VB.NET. Par exemple, l’algorithme A* utilise une collection type Fist In, First Out. Celle-ci n’existait pas sous VB6, il fallait donc créer « à la main » une classe qui implémentait cette fonctionnalité. Sous VB.NET, elle existe en natif et est beaucoup plus rapide. De même, au niveau des tests conditionnels du type :

If a=True And b=True Then
          …
End If

ceux-ci étaient décomposés pour des raisons d’optimisation comme suit puisque VB6 évaluait les deux conditions :

If a=True Then
          If b=True Then
                  
          End If
End If

alors que ce n’est pas nécessaire avec VB.NET. En effet il n’évalue la seconde condition que si la première est vérifiée. Cela permet une meilleure lisibilité du code. Ce ne sont que deux exemples, mais ils sont symptomatiques des problèmes que vous risquez de rencontrer dans le cas d’une migration.

Pour conclure

MS.NET et Visual Basic.NET vous permettent de créer de véritables applications orientées objets. C’est le grand saut pour les développeurs Visual Basic, cela nécessitera certainement un temps d’apprentissage important, mais, franchement, cela en vaut la chandelle. Juste un conseil : n’essayez pas de porter vos développements VB6 sous VB.Net, vous seriez déçus. Il y a une telle différence que c’est comme si vous attendiez une mise en page sophistiquée de vos textes écrits sous NotePad dans Word 2000. VB.NET est un nouveau produit, avec une nouvelle approche mais qui vous permettra de développer très rapidement des applications sophistiquées, robustes et évolutives.

Richard Clark
Richard Clark

Vos commentaires

Vous devez être identifié pour pouvoir commenter l'article.

Identification - S'inscrire