Anexe Windows Phone : Optimiser le chargement des assets

Le chargement des assets en Xna est un veritable casse tête pour les développeurs. Le multithreading n’était pas pas un solution pour l’heure, bon nombre de développeur s’orientent vers un chargement de type bulk (en masse) au lancement du jeu.


Malheureusement le lancement d’un jeu est soumis à plusieurs règles :


  • Elle doit afficher un premier écran en moins de 5 secondes
  • Au bout de 20 secondes l’application doit répondre aux interactions de l’utilisateur

Si ces deux règles ne sont pas respectées, le Watch Dog du système d’exploitation tuera le processus de votre jeu et l’utilisateur retournera à l’écran précédent.


Ajoutez à cela un accès mémoire relativement lent sur le hardware de base du WP et vous voilà confronté à un gros problème, surtout si votre jeu doit charger un grand nombre d’assets.


Voyons les différentes techniques qui s’offrent à nous pour améliorer le loading de vos jeux.


 


Premier moyen : réduire la taille des fichiers


Un seul règle ici : plus c’est gros, plus c’est long.


Gérer la taille d’un fichier pour la réduire, c’est sans doute le moyen le plus efficace d’améliorer les temps de chargement. Par default le content pipeline tente de générer à la compilation de son contenu les assets les plus fidèles en qualité au contenu original. Ce qui produit généralement un fichier aussi gros (sinon plus) que l’original ! Le développeur est heureusement libre de preciser l’encodage du fichier sous la forme d’un algorithme ou d’une qualité de compression. Trois types de fichiers se prêtent bien à cette optimisation : les fichiers sons, les fichiers images (textures), les fichiers Xml.


Son


Lorsque l’on expand les propriétés du processor lié à un asset sonore qu’il s’agisse d’un SoundEffect ou d’un Music il est possible de préciser la qualité de compression sonore. La fidélité de la qualité d’un son à l’original dépend bien entendu du son en lui même, mais il est possible de garder une qualité acceptable avec un taux de compression important. Le loading peut alors être considérablement réduit :



Certains tests peuvent montrer jusqu’à 20% de temps de chargement gagné. En outre il est possible de gagner encore 8% si le développeur utilise des sons en 44.1 kHz au lieu d’un 48kHz qui, il faut bien l’avouer n’apporte rien de plus sur un WP.


Textures


Les fichiers textures peuvent paradoxalement aboutir à un fichier bien plus important que la source, notamment si le choix du format de texture se porte sur “Color” :



L’utilisation du format Dxt (utilisant DXT1 pour des textures opaques et DXT5 pour les texture avec transparence) peut améliorer la taille du fichier et son chargement (les cartes graphiques peuvent décompresser une telle texture sur le hardware). Mais la qualité ici peut être radicalement différente. Il convient de toujours éviter une telle compression si votre texture contient des motifs fins, si elle contient du texte ou si elle est destinée à être visible de manière précise par le joueur. Au programmeur de faire un choix en fonction du contexte d’utilisation de la texture.


Enfin on peut être tenter de ne pas compiler (Build Action à None) une texture et de simplement autoriser sa copie dans le répertoire de destination (“Copy to Output Directory” à “Copy if newer“) afin de la charger avec un Texture2D.FromStream au runtime :



Effectivement, un png peut sembler plus petit que le Xnb généré par le content pipeline. Sachez juste que le DXT est utilisé nativement par le GPU qui les exploitent très facilement et rapidement. Utiliser une texture avec FromStream obligera une “decompression” du fichier au runtime et un chargement ddes données en 32 bits qui coutera du temps et surtout de la mémoire.


Pensez à toujours utiliser des textures ayant une taille puissance de 2. Quitte à les redimensionner. Leur chargement est plus rapide.


Utilisez aussi des SpriteSheet. Technique qui consiste à inclure dans une grosse texture plusieurs petites textures. Optimisant de fait le temps de chargement.


Xml


La sérialisation est d’une simplicité déconcertante aujourd’hui. L’utilisation du namespace System.Xml.Serialisation fait gagner un temps considérable. Malheureusement, comme précédemment, .Net ne fait pas dans la dentelle, il essaye de reproduire aussi bien que l’original. Ce qui dans le cas du Xml est très … verbeux.


Regardez la classe suivante que nous allons serialiser :


    public class DataOfMyGameWithAgreatNameForAClassIsntIt
    {

        public string ButYouKnowMyFriendMyPropertiesHaveGigNamesToo = “yes that’s true”;
        public int IntegerTakesPlaceToo;
       
    }


Elle possède des noms exagerement grand afin de mettre en exergue l’importance de maitriser la sérialisation de tout en bout. Serialisée cette classe donne :


  <?xml version=”1.0″?>
  <DataOfMyGameWithAgreatNameForAClassIsntIt xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
    <ButYouKnowMyFriendMyPropertiesHaveGigNamesToo>yes that’s true</ButYouKnowMyFriendMyPropertiesHaveGigNamesToo>
    <IntegerTakesPlaceToo>0</IntegerTakesPlaceToo>
  </DataOfMyGameWithAgreatNameForAClassIsntIt>


Lisible… mais gros. Donc long à charger. Surtout si la serialisation portable sur un ensemble conséquent de données. La solution est très simple, elle consiste à la fois à demander de sérialiser un champs en tant qu’attribut Xml et non en tant qu’élément et à donner à cet attribut un nom aussi court que possible :


    [XmlRoot(“c”)]
    public class DataOfMyGameWithAgreatNameForAClassIsntIt
    {

        [XmlAttribute(AttributeName = “a”)]
        public string ButYouKnowMyFriendMyPropertiesHaveGigNamesToo = “yes that’s true”;
        [XmlAttribute(AttributeName = “b”)]
        public int IntegerTakesPlaceToo;
       
    }


Le resultat se passe de commentaire :


  <?xml version=”1.0″?>
  <c xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:xsd=”http://www.w3.org/2001/XMLSchema” a=”yes that’s true” b=”0″/>


Plus de 50% de gain…




Second moyen : Adapter au besoin la ressource


Trois étapes ici. D’abord analyser la bonne utilisation de ses assets :


Pourquoi mettre un son wave dans un jeu qui n’est audible que sur 2s et pourtant dure 10 ?


Pourquoi utiliser une texture énorme pour un effet de particule là ou une simple image de 16*16 serait suffisante ?


Est il vraiment nécessaire d’utiliser autant de musiques dans son jeu ?


L’utilisation d’un modèle 3D de grande qualité est-elle nécessaire ? Elle prend du temps à charger… mais aussi à afficher.


En sommes il faut être sûr de ne pas gaspiller du temps de chargement avec des assets qui représentent un “luxe” bien trop important là ou une asset plus … “modeste” aurait été suffisante sans perte de qualité visuelle ou sonore.


Ensuite vérifier les “orphans” :


Un orphan (ou orphelin) est un fichier ajouté un jour au projet d’assets par le développeur et qui n’est plus utilisé.  Certes si ce fichier n’est plus utilisé il ne peut rien couter en temps de chargement. Mais bon nombre de développeurs (dont votre serviteur) chargent les assets au lancement du jeu en s’aidant d’une liste qui les énumère et permet de les charger de manière séquentielle. Le contenu de cette liste n’est pas forcement objectif par rapport a la réelle utilisation d’une asset dans un jeu. On peut se retrouver avec le chargement d’une ressource très importante qui paradoxalement n’est plus utilisée.


Enfin s’adapter au device :


Le besoin de qualité d’une texture (et sa taille) n’est pas forcement le même sur Windows Phone que sur Xbox 360. Les possibilités de cette dernière étant largement supérieure en hardware et en surface d’affichage les textures utilisées doivent forcement être plus importante en taille afin de garder un affichage aussi beau que possible. Le Xna Framework est un framework portable, un simple clique droit sur un fichier permet de passer une application d’un device vers un autre. Seul un projet ne change pas dans cette démarche : le projet de contenu. Il est alors possible de se retrouver avec un projet Windows Phone utilisant des assets destinées à la Xbox 360.La surface d’affichage d’un écran de télé ou d’ordinateur n’est pas la même que sur un WP ! Il est facile d’imaginer que le temps de chargement d’une texture en 2048*2048 n’est pas le même sur ces deux devices. Il existe deux solutions ici. Créer un nouveau projet de contenu reproduisant la même arborescence sur l’original mais avec des assets adaptée au device ciblé. Ou bien utiliser des directives pre processor pour modifier le code à la compilation en fonction de la cible de compilation. Un :


#if Windows_Phone
//code
#endif


ne compilera la partie “//code” que si le projet est compilé avec pour cible le Windows Phone. On peut alors imaginer que “//code” est une suite d’appel à la méthode Load du ContentManager avec des assets adaptées.


 


Troisième moyen : charger de manière intelligente


Charger de manière intelligent des assets c’est d’abord charger au besoin, c’est “se jouer du joueur”, c’est profiter du contexte du jeu, c’est, enfin, ménager le joueur. Voyons en détail ces trois points.


Regardez cette vidéo d’un jeu que j’ai réalisé avec quelques amis :


[View:http://www.youtube.com/watch?v=VgI9SCbURU4:550:0]


On y distingue 3etapes : l’affichage d’un logo animé, l’affichage d’un écran de trademark avec une progressbar, l’affichage du menu.


D’abord le logo animé. Vous avez sans doute remarqué qu’il y avait 3 parties ici :


  1. Le logo apparait de gauche vers la droite
  2. Il stoppe 3secondes
  3. Puis disparait de droite vers la gauche.

On ne le voit pas mais ici, je me joue du joueur en chargeant des assets durant les 3secondes ou le logo ne bouge plus. Je ne risque pas de faire laguer le jeu ou de deconcerter le joueur car cet écran est passif et n’affiche aucune animation. Le joueur n’y voit que du feu ! J’ai tout simplement profité d’un contexte ou le jeu n’avait rien d’autre à faire.


Seconde étape. Je ménage le joueur. Le trademark affiche une progressbar avec quelques messages amusants et du texte lire. L’écran profite là aussi pour charger un grand nombre d’assets mais ne déstabilise pas l’utilisateur puisqu’il lui donne des informations sur son état au travers de la progressbar. Sans cette dernière, un joueur non averti pourrait croire à un plantage ou rapidement se lasser de cet écran. En outre cet écran (tout comme le précédent) ne charge pas les assets demandée par une partie mais uniquement celles demandées par le menu. Lorsque le joueur lancera une partie, un nouvel écran chargera les données demandées par une partie. Le chargement réalisé par étape devient plus rapide et plus acceptable. Généralement le second écran dans un jeu (qui n’est pas visible dans la video ci-dessus) affiche des trucs et astuces ou bien les commandes pour apprendre au joueur à utiliser son jeu. D’autres jeux présentent le contexte dramatique du jeu par l’intermédiaire d’une carte ou le joueur voir sa progression dans le monde. Bref… de quoi passer le temps.


In game il est possible aussi de se jouer du joueur. Quand l’utilisateur clique sur un bouton provoquant une modification importante de la trame dramatique du jeu ou affiche un menu important, pkoi ne pas prendre une seconde pour charger quelques assets ? Le joueur verra un petit lag certes, mais il lui semblera “logique” eu égard à l’importance de l’action qu’il a réalisée.


Ceux qui connaissent Age of Empire ont sans doute remarqué qu’en changeant d’âge, toutes les assets du monde changent. Sur les vielles machines on peut sentir un leger lag là aussi. Mais ce lag semble logique au joueur. Pour le développeur c’est un moyen de ne charger ces nombreuses assets au moment de la partie avec le “consentement” tacite du joueur…


 


Quatrième moyen : Eviter le watchdog


Pour éviter le watch dog et un kill de son application due à un chargement trop long la méthode est simple.


Il faut tout d’abord tenter d’aller sur le premier update/draw le plus rapidement possible.


Ne mettez donc aucun load dans les méthode LoadContent ni aucun traitement lourd dans Initialise. Réalisez une liste des assets à charger pour le premier écran interractif du jeu (généralement l’écran titre). Voici par exemple la liste d’assets de mon jeu:


<Assets>
  <Asset Name=”Fonts\SubTitleSpriteFont” Type=”Microsoft.Xna.Framework.Graphics.SpriteFont, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
  <Asset Name=”Fonts\LegendSpriteFont” Type=”Microsoft.Xna.Framework.Graphics.SpriteFont, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
  <Asset Name=”Maps\homescreen” Type=”Arcane.TowerDefense.Terrain.LayeredTerrain, Arcane.TowerDefense, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null” />


  <!– …–>


  <Asset Name=”LogoATD” Type=”Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
  <Asset Name=”logo_gs” Type=”Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
  <Asset Name=”Textures\magicTrail” Type=”Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
  <Asset Name=”Textures\Summer\ground” Type=”Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
  <Asset Name=”Textures\Summer\badly” Type=”Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
  <Asset Name=”Textures\Summer\darkgrass” Type=”Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
  <Asset Name=”Textures\Summer\grass” Type=”Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842CF8BE1DE50553″ />
</Assets>


J’ai volontairement supprimé une bonne cinquantaine d’assets pour ne pas polluer cet article. En analysant ce fichier je peux récupérer le nombre d’assets à charger. Je peux donc tenir un indexeur à jour qui m’indique ma progression de chargement dans cette liste. Je suis donc capable de réaliser une progress bar.


Enfin je réalise un Load d’asset non pas à la suite afin de ne pas freezer le jeu mais séquentiellement à chaque update. Si j’ai 50 assets à charger, il me faudra 50 updates. Évidemment je ne ferait pas cela dans un écran demandant des interactions utilisateur ou affichant des animations. Le jeu freezera beaucoup et il ne faut pas le montrer. Les deux premiers écrans de la videos sont donc parfait pour ce type de traitement : dans le premier je charge les asset quand l’animaton s’arrête, dans le second j’affiche un écran statique où seule la progresse bar est animée.


En outre je ne charge que les assets nécessaires à l’écran que je veux afficher. Ceci est très important pour les problématiques de recover. Quand notre jeu est soumis à une interruption (quitter temproairement le jeu pour répondre à un sms, aller sur le web…) il libère toutes ses assets. Le joueur attends donc de pouvoir revenir au jeu très rapidement pour continuer sa partie Le développeur doit donc être capable de ne charger que les ressources qui seront utilisées pour le prochain écran interractif. Le meilleur moyen reste donc de grouper les assets part scènes de jeu.


Souvenez vous qu’une asset chargée ne sera jamais rechargée si elle n’est pas disposée par une interruption ou un appel à dispose. Et ce, même si vous appelez une nouvelle voit Load avec son identifiant.


Pour terminer soyez ami avec le Garbe Collector. Il est l’enfant terrible du WP. Le GC se lance à chaque fois qu’un nouveau méga de mémoire est alloué par l’application. Autant dire que pour le chargement d’assets il est souvent appelé. Il peut intervenir moins souvent si vous gerez au mieux la taille de vos assets et leur ordre de chargement en ayant en tête ce méga. Un freeze GC peut faire jusqu’à 200ms ce qui n’est pas rien…


Je mettrais en ligne sous peu mon système de chargement.


 


Etre inventif et astucieux


C’est les deux qualités à avoir pour réaliser un chargement rapide, transparent et pérenne pour ses jeux. N’oubliez pas que le chargement est la première impression qu’un joueur va avoir de votre appli… elle est donc la brique la plus sensible que vous aurez à gérer…


 


[Soon]


Valentin

One thought on “Anexe Windows Phone : Optimiser le chargement des assets”

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>