Il n’est jamais trop tard pour préparer Noël et la période estivale est le meilleur moment pour parler de neige, vision rafraîchissante s’il en est. Mais tel n’est pas réellement mon propos. Sous ce prétexte glacé (mais pas glaçant) se cache des notions techniques très utiles sous Silverlight (ou WPF).
Les boucles d’animation, le contrôle des fps (frame per second, images par seconde) et les sprites (petits objets autonomes animés) sont certes autant d’incursions dans le monde du jeu mais la créativité permet d’envisager mille utilisations de ces techniques dans d’autres contextes.
Je me suis librement inspiré d’un billet s’intitulant « Making it snow in Silverlight » écrit par Mike … Snow (facile à se souvenir !). J’utilise un code différent et (à mon sens) amélioré, mais il était important de rendre à Snow la neige qui lui appartient (et de faire un peu d’humour bas de gamme au passage).
Avant d’expliquer certains aspects, voici une démonstration live que vous pouvez passer en mode plein écran :
[silverlight:source=/SLSamples/IlNeige/IlNeige.xap;width=500;height=350]
Le but
Le but du jeu est donc de faire tomber de la neige sur un fond, c'est-à-dire de faire se mouvoir des centaines d’objets autonomes ayant chacun un aspect légèrement différent et une trajectoire un peu aléatoire. Le tout assez vite pour que cela paraisse naturel.
Le principe
Chaque flocon de neige est en réalité une instance d’un UserControl, il y a ainsi des centaines d’instances au même instant. Chaque flocon est responsable de son affichage et de la façon dont il avance à chaque « pas ». La coordination générale étant réalisée par une boucle d’animation principale, c’est elle qui scande le rythme et demande à chaque flocon d’avancer d’un « pas ».
Tout se trouve donc dans deux endroits stratégiques : la boucle principale et le UserControl « Flocon ».
Suivre l'article : le code n'étant pas très lisible dans le blog, sauf extraits très courts, je n'ai volontairement pas placé ce dernier dans le corps du billet. Je vous conseille ainsi de télécharger le projet (en fin de billet) et de suivre les explications en ayant le code chargé sous Blend 3 ou Visual Studio (2008 + SL3 minimum).
Le sprite Flocon
Il s’agit d’un UserControl possédant une interface très simple puisqu’il s’agit d’un bitmap en forme d’étoile (ou pourrait utiliser n’importe quelle forme vectorielle créée sous Design ou Illustrator). Côté XAML on retrouve ainsi une grille avec un PNG à l’intérieur, rien de palpitant. A noter qu’au niveau du UserControl lui-même on définit deux transformations : une d’échelle (FlakeScale) et une de rotation (FlakeRotation). Dans l’exemple de la neige cette dernière n’a que peu d’effet visuel sauf lorsqu’on choisit une taille assez grosse pour les flocons. L’œil ne discerne pas forcément la rotation de façon consciente mais comme elle est choisit aléatoirement pour chaque flocon cela renforce l’aspect global plus « organique ». De plus il s’agit d’illustrer des techniques utilisables dans d’autres contextes ou avec d’autres sprites pour lesquels l’effet serait plus évident. La technique est extrapolable à toutes les transformations supportées, même les nouvelles transformations 2,5D de Silverlight 3.
Le code de la classe Flocon se divise en deux points : le constructeur et la méthode « Tombe ».
Le constructeur
Il est responsable de la création de chaque instance. C’est à ce niveau qu’on introduit un peu d’aléatoire dans la rotation du flocon. On choisit aussi à ce moment la position de départ en X (la position en Y étant toujours à zéro au départ).
La gestion des flocons est séquentielle (par la boucle d’animation que nous verrons plus loin). De fait il n’y a pas de multithreading et on peut se permettre un développement très simple de la classe. Par exemple les limites du tableau de jeu sont stockées dans des propriétés statiques de la classe. Le programme initialise ces valeurs au lancement et les modifie à chaque fois que la taille du tableau est changée (redimensionnement du browser).
Le constructeur fixe aussi de façon aléatoire l’échelle de l’instance afin que chaque flocon soit de taille légèrement différente par rapport à une échelle de base. Cela permet d’obtenir un effet de profondeur de champ.
La méthode « Tombe »
Il s’agit bien de tomber et non d’outre-tombe et de zombies façon Thriller du très médiatisé Jackson. Pour chaque flocon il s’agit donc ici de savoir choir, avec si possible le sens du rythme et du mouvement du sus-cité Michael.
Pour ce faire la méthode « Tombe » doit calculer la nouvelle position X,Y du flocon. Les flocons évoluent dans un espace de type Canvas particulièrement indiqué pour tout ce qui doit être positionné de cette façon à l’écran.
Sur l’axe des Y le déplacement s’effectue d’un pixel à la fois. Une incrémentation simple est utilisée. Sur l’axe horizontale le flocon change régulièrement de direction pour rendre l’ensemble visuel plus réaliste. On utilise ici un tirage aléatoire pour savoir s’il faut ou non changer de direction. Dans l’affirmative on modifie la valeur du pas de déplacement (hzDeplacement). La position sur X est alors calculée en ajoutant cette valeur à la position courante.
Reste à savoir quand un flocon n’a plus d’intérêt, c'est-à-dire lorsqu’il échappe au champ de vision et qu’il peut être détruit (n’oublions pas que des centaines d’instances sont gérées à la fois et que tout flocon inutile doit être détruit le plus vite possible pour d’évidentes raison d’efficacité). C’est le rôle du test en fin de méthode qui initialise la valeur « HorsZone ». Lorsqu’elle passe à true, le flocon peut être détruit. Cette information est utilisée par la boucle principale.
La boucle d’animation
Maintenant que nous disposons d’un modèle de flocon (la classe Flocon) reste à animer de nombreuses instances pour donner l’illusion de la neige qui tombe.
La technique pourrait utiliser un Timer ou bien une StoryBoard vide dont le déclenchement est réalisé à chaque pas (et comme elle s’arrête immédiatement on obtient une sorte de Timer rythmant la boucle principale). Cette dernière façon de faire était très utilisée sous Silverlight 1.x. Selon la vitesse de la machine cliente la boucle est ainsi déclenchée plus ou moins vite ce qui évite le problème de fixer un pas déterminé comme avec un Timer. Toutefois il est en réalité inutile de calculer la position des flocons plus rapidement que les FPS, pure perte d’énergie, ni de calculer moins souvent (autant abaisser les FPS).
Sous Silverlight 2 un événement a été ajouté donnant accès au rythme du moteur de rendu : CompositionTarget.Rendering. En gérant la boucle principale dans ce dernier on s’assure d’être parfaitement dans le tempo du moteur de rendu, ni plus rapide, ni plus lent.
Cette technique a toutefois dans le principe un défaut connu dans tous les jeux programmés de la sorte : la célérité de l’ensemble dépend de la vitesse de la machine hôte. Un PC très lent fera donc tomber la neige trop lentement, alors qu’un PC fulgurant des années 2020 ne permettra même plus de distinguer le déplacement des flocons tellement cela sera rapide, rendant le jeu « injouable ». Pour le premier problème il n’y a pas vraiment de solution, on ne peut donner plus puissance à la machine hôte (sauf à permettre à l’utilisateur de diminuer la charge du jeu lui-même en limitant par exemple le nombre maximum de flocons, le paramètre de densité peut jouer ce rôle dans notre exemple). Quant au second problème il trouve sa réponde dans la limite à 60 FPS par défaut de Silverlight, limite modifiable via les paramètres du plugin (voir ce billet).
Débarrassés des écueils propres à cette technique, l’événement de rendering devient l’endroit privilégié pour placer le code d’une boucle principale d’animation.
Le contrôle des FPS
Lorsque vous exécutez une application de ce type il est important de contrôler les FPS pour voir comment certaines améliorations du code peuvent accélérer ou non le rendu. Pour ce faire Silverlight permet d’activer l’affichage de deux informations essentielles dans la barre d’état du browser : les FPS actuels et la limite fixée dans le plugin. L’activation se fait via Application.Current.Host.Settings en plaçant EnableFrameRateCounter à true. Il est bien entendu vivement conseillé de déconnecter la fonction en mode Release. Si votre navigateur et ses réglages le supporte vous devez en ce moment pouvoir lire ces informations en bas à gauche de la barre d'état (et si l'exemple live de l'application en début de billet s'est chargé correctement).
Attention : Selon les réglages de sécurité Internet Explorer autant que FirexFox peuvent ne pas autoriser la modification de la barre d’état. Sous IE (sous FF cela est quasi identique) il faut vérifier le paramètre Outils / Options Internet / Sécurité / Zone Internet / Personnaliser le niveau / Autoriser les mises à jour à la barre d’état via le script. Ce paramètre doit être activé pour voir les FPS.
Le Rendering
A chaque pas du rendering nous allons réaliser deux tâches essentielles : déplacer les flocons déjà présents et supprimer ceux devenus inutiles et créer de nouveaux flocons.
Pour déplacer les flocons nous ne faisons que balayer la liste de tous les flocons et appeler la méthode « Tombe() » de chacun. Au passage nous archivons dans une liste temporaire tous les flocons dont l’état HorsZone est passé à true. En fin d’animation les flocons inutiles sont supprimés de la liste principale les laissant ainsi disponible au Garbage Collector.
Pour créer de nouveaux flocons nous déterminons si la densité désirée est atteinte ou non. Le processus est randomisé pour plus de réalisme bien que le seuil de densité soit lui fixé par le programme (et modifiable dans notre exemple). Comme le placement de chaque flocon est réalisé dans le constructeur de la classe Flocon il n’y a rien d’autre à faire que de créer les flocons et de les ajouter à la collection Children du Canvas parent.
Le plein écran
Silverlight permet de passer une application en mode plein écran. Sachez toutefois que ce mode pose de sérieuses conditions à son utilisation. Notamment il ne peut être activé qu’en réponse directe à un clic de l’utilisateur. Cela permet d’éviter (en partie) qu’une application Silverlight soit utilisée de façon à tromper l’utilisateur final en prenant le contrôle de tout l’écran sans son autorisation (on pourrait penser à un fake du bureau Windows permettant par exemple d’extorquer un login). De même, lorsque l’application bascule en mode plein écran un message impossible à supprimer est affiché pour avertir l’utilisateur et lui indiquer que l’appui sur Escape permet de sortir de ce mode spécial.
Si cela n’était pas suffisant, les saisies clavier sont impossibles en plein écran. Seules certaines touches ont un effet (voir la documentation sur MSDN). Ne pensez pas utiliser le plein écran pour une application de saisie par exemple. En revanche pour visionner des vidéos ou pour créer un jeu les touches disponibles et la gestion de la souris sont suffisants. C'est le cas de notre démo.
Une fois connues ces limites raisonnables (Microsoft ne veut pas, à juste titre, que Silverlight soit utilisé par des fraudeurs et autres adaptes du phishing) le passage en plein écran s’effectue par Application.Current.Host.Content.IsFullScreen en mettant cette propriété à true, et ce en réponse au clic sur un bouton en général.
Le code
Télécharger le projet complet de l'exemple "IlNeige" : IlNeige.zip (229,54 kb)