Partie 5: Charger et sauvegarder les maps

Téléchargements

Code source
Exécutable de l'éditeur de niveaux (Windows 32bits)
Exécutable du jeu - exactement le même que la partie 4 (Windows 32bits)

Avant d'essayer de compiler le code, allez dans l'onglet "Projets" dans le menu de gauche, séléctionnez l'onglet "Run" pour votre kit,
et mettez dans "Working directory" le chemin de "editor\data" pour l'éditeur ou "game\data" pour le jeu.

Qu'est-ce qu'il nous manque ?

Maintenant que nous avons un éditeur de niveaux où on peut tracer des murs, que nous avons un projet pour le jeu, et que nous avons des graphismes
pour les murs, est-ce qu'on peut commencer à afficher une simple map en 3D ?
Il nous manque juste une étape avant ça: un moyen de sauvegarder la map dans l'éditeur et de la charger dans le jeu.
De plus, ça sera aussi utile que l'éditeur puisse charger des maps qu'il a sauvgardées.

Nous allons mettre ces 2 fonctions dans "Map.cpp" qui est dans le répertoire "common", pour que les 2 projets puissent y accéder.
Même si le jeu lui-même n'utilisera pas la fonction de sauvegarde, c'est une bonne idée de les garder ensemble parce qu'elles sont symétriques.

Quand on écrit un nouveau format de fichier, c'est habituellement mieux de commencer avec une version ASCII qui peut être facile à lire dans un éditeur de texte.
Mais le format des maps est assez simple pour le moment pour qu'on puisse l'écrire en binaire directement.

Dans cette partie, on va se concentrer sur le chargement et la sauvegarde dans l'éditeur. Ensuite, le chargement de la map dans le jeu se réduira à un simple appel
à la fonction de chargement...

Dans le QML

Dans "main.qml", j'ai ajouté deux objets FileDialog (un pour le chargement et un pour la sauvegarde) qui ressemblent à ça:

		FileDialog {
			id: saveDialog
			visible: false
			title: "Choisissez un fichier"
			folder: "file:///maps"
			nameFilters: [ "Fichiers map (*.map)", "Tous les fichiers (*)" ]
			selectExisting: false;
			onAccepted: {
				bridge.save(fileUrl);
				visible=false;
			}

			onRejected: visible=false;
		}
				
La seule différence entre les 2 ( à part la fonction appelée dans onAccepted) est la propriété "selectExisting" qui es t mise à "true"
pour le dialog de chargement.

Quand "visible" est mis à true, ces dialogs affichent un sélecteur de fichier standard:


Un peu plus bas dans main.qml, J'ai modifié le menu "Fichier":

        Menu {
            title: qsTr("Fichier")
            MenuItem {
                text: qsTr("Charger")
                onTriggered: loadDialog.visible = true;
            }
            MenuItem {
                text: qsTr("Sauver")
                onTriggered: saveDialog.visible = true;
            }
            MenuItem {
                text: qsTr("Quitter")
                onTriggered: Qt.quit();
            }
        }
				
Donc maintenant, il ressemble à ça:


Dans bridge.cpp

Dans "bridge.cpp" il y a 2 nouvelles fonctions:

		void    CBridge::save(QString fileName)
		{
			// [8] pour retirer "file:///"
			map.save(&fileName.toLocal8Bit().data()[8]);
		}

		void    CBridge::load(QString fileName)
		{
			// [8] pour retirer "file:///"
			map.load(&fileName.toLocal8Bit().data()[8]);
			updateQMLImage();
		}
				
Comme le commentaire le dit, il y a une petite astuce ici, parce que les dialogs qml ne nous renvoient pas un chemin, mais une URL qui commence par "file:///".
Par exemple, on recevra: "file:///E:/prg/DungeonMaster/editor/data/maps/test.map"
C'est pour ça que vous pouvez voir un "&" devant "fileName" et "[8]" à la fin, parce que l'on saute 8 caractères.

Dans Map.cpp

Dans "Map" j'ai ajouté 2 fonctions "load()" et "save()".
Voici celle qui sauve la map:

		char    gFileHeader[] = "DMMP"; // Dungeon Master MaP
		#define CURRENT_VERSION     1
		#define CURRENT_REVISION    0

		void CMap::save(char* fileName)
		{
			FILE*       handle = fopen(fileName, "wb");
			uint16_t    ver = CURRENT_VERSION;
			uint16_t    rev = CURRENT_REVISION;

			if (handle != NULL)
			{
				// entête, version, révision
				fwrite(gFileHeader, 1, 4, handle);
				fwrite(&ver, 2, 1, handle);
				fwrite(&rev, 2, 1, handle);

				// taille de la map
				fwrite(&mSize.x, 4, 1, handle);
				fwrite(&mSize.y, 4, 1, handle);

				// données de la map
				for (int y = 0; y < mSize.y; ++y)
					for (int x = 0; x < mSize.x; ++x)
					{
						CTile*  t = getTile(CVec2(x, y));
						fwrite(&t->mType, 1, 1, handle);

						for (int side = 0; side < eWallSideMax; ++side)
							fwrite(&t->mWalls[side].mType, 1, 1, handle);
					}

				fclose(handle);
			}
		}
				
Donc le format ressemble à ça:

D'abord on écrit une chaine de 4 caractères "DMMP" pour que ça soit facile de voir si ce fichier contient des données d'une map.

Puis deux valeurs 16 bits qui contiennent les numéros de version et de révision.
Comme on va probablement faire beaucoup de modifications à ce format dans le futur, c'est important de connaitre quelle version du format
ce fichier utilise, pour éviter de lire des données qui n'existent pas ou les interpréter d'une mauvaise façon.

Puis deux valeurs 32 bits qui donnent la taille de la map en nombre de cases.

Et finalement, pour chaque case, on sauve son type et le type des ses 4 murs, chacun sur 1 octet.

La fonction load() est très similaire, à part qu'on verifie les valeurs de l'entête et que l'on allloue de la mémoire pour lire les données de la map.

Dans "editor/data/maps" j'ai mis une map de test très simple qui a été sauvée par cette fonction.