Intégration de MonoGame avec du XAML sous Windows 8 Metro

Nous avons vue dans un précédent tutoriel comment mettre en place un projet MonoGame pour Windows 8 Metro, aujourd’hui je vais vous expliquer comment utiliser du XAML avec MonoGame. Avant de commencer je tiens à vous dire que mélanger MonoGame et XAML n’offre pas de bonnes performances sur les appareils de faible puissance, comme les tablettes ou certains netbook. L’objectif de cet article va être de créer deux pages, un menu qui sera tout en XAML et une page de jeu où on utilisera MonoGame. J’ai pu écrire cet article grâce à un poste sur le forum de MonoGame ainsi qu’à un article du blog Game Development Resources que j’ai adapté et corrigé.

1 – Prérequis

Pour suivre ce tutoriel vous devez avoir une version de Windows 8 RC ou finale ainsi que Visual Studio Express 2012 final (ou une version supérieur). Vous devrez aussi compiler la dernière version de MonoGame pour Windows 8 (consultez ce tutoriel si vous ne savez pas comment faire). Si vous ne pouvez pas compiler MonoGame pas d’inquiétude, le projet complet est téléchargeable à la fin de cet article.

Enfin vous devrez connaitre les bases du XAML et les bases du fonctionnement d’une application Metro (je sais il ne faut plus les appeler comme ça).

2 – La création du projet

La première chose à faire est de créer un projet avec Visual Studio, en choisissant Application vide (xaml), ensuite vous devrez éditer le fichier du projet comme indiqué dans le précédent tutoriel afin de pouvoir charger du contenu à partir du dossier Content. J’attire encore une fois votre attention sur le fait que MonoGame n’a pas de compilateur de contenu (pour le moment) et que vous ne pouvez charger que des fichiers xnb déjà compilés.

Une fois que le projet est créé vous avez 2 fichiers xaml, App.xaml qui représente votre application, c’est cette classe qui est instanciée en premier quand l’utilisateur lance l’application et MainPage.xaml qui représente le contenu de la première page visible par l’utilisateur. Nous allons ajouter une nouvelle page au projet qui sera la page dite de « Jeu » et qui contiendra le rendu et la logique de MonoGame. Faites donc un clique droit sur votre projet (pas la solution, le projet) et choisissez dans le menu contextuel l’option Ajouter / Nouvel élément.. et enfin page vierge. Vous vous retrouvez maintenant avec une nouvelle page XAML que nous allons modifier dans un moment. Enfin nous ajoutons une référence vers MonoGame.Windows8.dll au projet.

3 – La classe App.xaml.cs

La première chose à faire est d’ajouter les directives using necessaires pour travailler avec MonoGame

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

On déclare ensuite quelques références pour naviguer entre les pages, on initialise le gestionnaire de contenu et le gestionnaire de service.

public ContentManager Content { get; set; }
public GameServiceContainer Services { get; set; }
public GamePage GamePage { get; set; }
public Frame RootFrame { get; set; }

Il ne reste plus qu’à initialiser MonoGame avec une méthode InitializeXNA() par exemple, que nous appellerons dans le constructeur de App.

// Constructeur
public App()
{
	this.InitializeComponent();
	this.InitializeXNA();
	this.Suspending += OnSuspending;
}

private void InitializeXNA()
{
	Services = new GameServiceContainer();
	Services.AddService(typeof(IGraphicsDeviceService), new SharedGraphicsDeviceManager());

	Content = new ContentManager(Services);
	Content.RootDirectory = "Content";
}

Enfin nous allons changer le contenu de la méthode OnLaunched afin d’y initialiser la page de jeu « GamePage » et définir comme première vue MainPage.xaml.

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
	if (args.PreviousExecutionState == ApplicationExecutionState.Running)
	{
		Window.Current.Activate();
		return;
	}

	if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
	{
		// On termine l'application
	}

	RootFrame = new Frame();
	if (!RootFrame.Navigate(typeof(MainPage)))
	{
		throw new Exception("Impossible de créer la page initiale");
	}

	// Initialisation de la page de jeu qui contient le code XNA
	GamePage = new GamePage();

	Window.Current.Content = RootFrame;
	Window.Current.Activate();
}

Il faut que je vous parle de la méthode OnSuspending(), elle est appelée lorsque l’utilisateur switch vers une autre application ou ferme votre application. Il faudra donc dans cette méthode mettre le code nécessaire pour sauvegarder les données du jeu (score, avancement, etc…)

4 – MainPage.xaml

Je vous propose de réaliser un petit menu en XAML, rien de bien compliqué, un titre avec un sous titre et des boutons pour choisir l’action désirée. Alors voilà ce que je veux que vous fassiez

Menu XAML
Et voilà le menu !

C’est un chouette non ? Bon c’est très vide d’accord ! Mais ça ira très bien pour notre tutoriel 😉 Vous trouverez le code à la fin de l’article et comme cette partie est très simple je vais juste vous donner le code de l’événement du bouton « New Game »

private void btnLaunchGame_Click_1(object sender, RoutedEventArgs e)
{
	var gamePage = ((App)App.Current).GamePage;

	Window.Current.Content = gamePage;
	Window.Current.Activate();
}

lorsque l’on cliquera sur « New Game » on passera sur le rendu XNA, par contre attention les choses vont se compliquer un peu (mais vraiment un peu) à partir de maintenant.

5 – La vue Game.xaml

Si vous ouvrez cette vue vous pourrez constater qu’elle dérive de la classe Page mais nous, nous voulons un rendu XNA et pas une page. On va donc remplacer tout le code généré par celui ci

<SwapChainBackgroundPanel 
    x:Class="MonoGameXAML.GamePage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:MonoGameXAML"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Button x:Name="btnBack" Content="Back" HorizontalAlignment="Center" Width="120" Height="35" Margin="10, 20, 10, 0" VerticalAlignment="Top" Click="btnBack_Click" />
</SwapChainBackgroundPanel>

Alors attention au copier/coller s’il vous plait ! La deuxième ligne x:Class= »MonoGameXAML.GamePage » doit être adaptée à votre projet et votre espace de nom. J’utilise comme espace de nom pour mon application MonoGameXAML (c’est le nom de mon application aussi du coup), donc là il faut bien mettre le nom de votre espace de nom sinon ça ne marchera pas hein 😉

La première chose marquante sur ce code est le remplacement de la classe Page par SwapChainBackgroundPanel ! Cette classe permet d’utiliser un rendu DirectX dans du Xaml or MonoGame utilise justement DirectX pour le rendu graphique (mais comme ça nous arrange tout ça !). Bon sinon j’ai ajouté un petit bouton pour revenir en arrière (donc au menu dans notre cas mais ça peut être n’importe quelle page xaml).

6 – GamePage.xaml.cs

Bon les amis là on ne rigole vraiment plus ! C’est ici qu’on va retrouver toutes les méthodes bien connues, à savoir Initialize(), LoadContent(), UnloadContent(), Update() et enfin.. Draw(). Pour commencer en douceur on va ajouter les directives using qui vont bien, comme on l’a déjà fait dans App.xaml.cs. Ensuite il faut déclarer les objets nécessaires au fonctionnement de MonoGame

private GameTimer gameTimer;
private SharedGraphicsDeviceManager manager;
private ContentManager content;
private bool pageLoaded;
private SpriteBatch spriteBatch;

L’objet GameTimer va permettre de créer la boucle de jeu infinie, le SharedGraphicsDeviceManager va nous permettre d’accéder aux différents objets de GraphicsDeviceManager et les 3 autres et bien vous les connaissez bien je pense 😉 Dans le constructeur nous allons nous abonner à 2 évènements de la classe SwapChainBackgroundPanel et indiquer que la page n’est pas chargée.

public GamePage()
{
	this.pageLoaded = false;
	this.Loaded += GamePage_Loaded;
	this.Unloaded += GamePage_Unloaded;
	this.InitializeComponent();
}

Nous nous occupons tout de suite des évènements de chargement et déchargement de la page comme suis

#region Evénements de la page

private void GamePage_Unloaded(object sender, RoutedEventArgs e)
{
	UnloadContent();
}

private void GamePage_Loaded(object sender, RoutedEventArgs e)
{
	if (!pageLoaded)
	{
		manager = SharedGraphicsDeviceManager.Current;
		manager.PreferredBackBufferWidth = (int)this.ActualWidth;
		manager.PreferredBackBufferHeight = (int)this.ActualHeight;
		manager.SwapChainPanel = this;
		manager.ApplyChanges();

		gameTimer = new GameTimer();
		gameTimer.UpdateInterval = TimeSpan.FromTicks(166666);
		gameTimer.Update += Update;
		gameTimer.Draw += Draw;

		this.SizeChanged += GamePage_SizeChanged;

		// Le contenu est chargé une fois
		LoadContent();

		pageLoaded = true;
	}

	// L'initialisation doit se faire à chaque rechargement
	Initialize();
}

private void GamePage_SizeChanged(object sender, SizeChangedEventArgs e)
{
	ResizePage((int)e.NewSize.Width, (int)e.NewSize.Height);
}

private void ResizePage(int width, int height)
{
	manager.PreferredBackBufferWidth = width;
	manager.PreferredBackBufferHeight = height;
	manager.ApplyChanges();

        // A la suite vous pouvez modifier d'autres tailles et positions
        // de vos objets graphiques
}

#endregion

On a 2 méthodes issus des évènements déclarés dans le constructeur, GamePage_Unload() sera appelée au déchargement de la page et videra la mémoire des ressources chargées et GamePage_Loaded() sera appelée quand la page sera chargé et devra initialiser le gestionnaire graphique, le timer et s’abonner à l’évènement SizeChanged (je vais y revenir). Dans tous les cas on lance la méthode Initialize().

Cycle de vie d’une application

Je vais vous parler rapidement du cycle de vie de l’application. Quand vous lancer pour la première fois GamePage.xaml le constructeur fait son travail et l’initialisation est faite. Si vous revenez au menu et que vous relancez GamePage.xaml, la page ne sera pas recréée car elle a déjà été instanciée une fois,  par contre la méthode GamePad_Loaded() sera rappelée une seconde fois, c’est pour cette raison que l’on a une variable qui va nous permettre de savoir si la page a déjà été chargée, auquel cas on ne réinitialise ni le gestionnaire graphique, ni les ressources, par contre on réinitialise le reste avec Initialize() afin que les différentes positions et autres réglages d’initialisation soit remis à zéro.

Un autre aspect important à prendre en compte lors du cycle de vie de l’application est le redimensionnement de celle ci. Les applications Windows 8 Metro peuvent en effet être redimensionnées, pour par exemple s’accrocher sur le bord de l’écran (voir le screen plus haut) et il faut donc à chaque changement de taille, faire les changements qui vont bien (changer la taille de la scène ou la mettre en pause si la surface est trop petite par exemple).

Il y a deux autres évènements issus de la classe SwapChainBackgroundPanel assez pratiques à exploiter que je vous encourage à implémenter. LostFocus et GotFocus sont très importants lorsque l’utilisateur accroche l’application au bureau par exemple, car il peut arriver que votre application perde le focus et dans ce cas il faut agir en conséquence (la mettre en pause, sauvegarder les données, afficher un truc rigolo ou qui fait peur ?). Lorsqu’elle retrouve le focus (l’utilisateur agit dessus) vous pourrez alors tout remettre en place (virer la pause ou le trucs rigolo ou qui fait peur!). Je vous invite à lire l’article du blog Dot.Blog de qui explique très bien le cycle de vis d’une application.

Fin d’implémentation

Enfin nous allons écrire les méthodes Update() et Draw() qui permettent respectivement de mettre à jour la logique du jeu et dessiner le tout à l’écran

#region XNA GameState pattern

private void LoadContent()
{
	spriteBatch = new SpriteBatch(manager.GraphicsDevice);
	// On récupère l'instance du gestionnaire de contenu
	// créé dans App.xaml.cs
	content = ((App)App.Current).Content;
}

private void Initialize()
{
	// 1 - Il faut démarrer le timer
	gameTimer.Start();

	// Vos initialisations
}

private void UnloadContent()
{
	// On stop la boucle de jeu
	gameTimer.Stop();
}

private void Update(object sender, GameTimerEventArgs e)
{
	// Vos mises à jour de logique de jeu
	// on accéde au temps écoulé avec 
	// e.ElaspsedGameTime
}

private void Draw(object sender, GameTimerEventArgs e)
{
	manager.GraphicsDevice.Clear(Color.DarkCyan);
	spriteBatch.Begin();
	spriteBatch.Draw(...);
	spriteBatch.End();
}

#endregion

On termine avec le bouton « Back » qui permet de revenir au menu. On récupère l’instance de la classe App et on redirige vers MainPage.xaml.

private void btnBack_Click(object sender, RoutedEventArgs e)
{
	var mainPage = ((App)App.Current).RootFrame;

	Window.Current.Content = mainPage;
	Window.Current.Activate();
}

Voilà c’est terminé ! Vous avez une application Metro en XAML et MonoGame (bon sang j’ai faillis écrire XNA un paquet de fois !)

 

Vous retrouverez le projet entier sur mon espace Github dédié à ce blog avec un exemple d’utilisation basique de MonoGame. Vous pouvez aussi le télécharger depuis ce lien. Le projet est complet avec les références de MonoGame et la modification du projet pour profiter du gestionnaire de contenu grâce au dossier Content

J’espère que cet article vous aura permis d’y voir un peu plus clair sur l’utilisation de MonoGame et de XAML, n’oubliez pas comme je l’ai dis en introduction que les performances XAML + MonoGame ne sont pas exceptionnelles sur des machines de faible puissance. Sur un PC classique ça fonctionne en tout cas très bien. Si vous rencontrez des problèmes de compilation, de code ou autre, n’hésitez pas à laisser un commentaire ou à me contacter sur ma page Google+. Codez bien ! On se revoit dans un prochain article.