Partie 30: Niveau 2, chargement et sauvegarde du jeu

Téléchargements

Code source
Exécutable de l'éditeur de niveaux - exactement le même que la partie 29 (Windows 32bits)
Exécutable du jeu (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.

Niveau 2

J'ai fait un brouillon du niveau 2.


Quand je l'ai testé, j'ai seulement trouvé un petit bug de clipping dans le dessin des portes que j'ai facilement
corrigé.

Dans ce niveau, on va rencontrer beaucoup de nouvelles choses: des monstres, des trous dans le sol, des leviers,
des télé-porteurs, des coffres...
Mais la différence la plus frappante avec le premier niveau est le nombre de portes et les clés pour les ouvrir.
J'ai mis un bouton sur chaque porte pour qu'on puisse visiter tout le niveau même si on n'a pas encore
implémenté toutes les façons de les ouvrir.
Notez que si vous essayez de prendre les escaliers à la fin du niveau, le jeu va crasher.

Mais nous parlerons de toutes ces nouvelles choses dans les prochaines parties.
Pour l'instant on va utiliser la mémorisation des maps de la dernière partie, parce que si vous commencez à
explorer ce gros niveau, ça prendra du temps. Alors dans cette partie on va implémenter le système de sauvegarde
et de chargement.

Que faut-t-il sauver ?

Les fichiers de sauvegardes devront contenir l'état des niveaux qu'on a visités, alors on va simplement sauver
images des maps mémorisées.
On aurait probablement pu sauver uniquement les différences entre l'image et la map d'origine, mais les fichiers
des niveaux ne sont pas très gros.
Le principal inconvénient de sauver toute la map c'est que si le format de la map change, le fichier de sauvegarde
sera invalide.
Mais c'est beaucoup plus facile à programmer.

Voici le code pour sauver l'image d'une map:

		void CSnapshot::save(FILE* handle)
		{
			fwrite(&level, sizeof(level), 1, handle);
			map.save(handle);

			uint8_t state;
			for (int i = 0; i < map.mSize.x * map.mSize.y; ++i)
			{
				state = pressPlateStates[i];
				fwrite(&state, sizeof(uint8_t), 1, handle);
			}
		}
				
Mais ce n'est pas la seule chose qu'on doit sauver.
On a besoin du numéro du niveau, de la position du joueur et de sa direction (c'est facile à sauver...).
Et on a aussi besoin de l'état des personnages et des objets qu'ils transportent.
Alors voila la fonction de sauvegarde pour un personnage:

		void CCharacter::save(FILE* handle)
		{
			// sauve le portrait
			fwrite(&portrait, sizeof(portrait), 1, handle);

			// sauve le nom
			int size = firstName.size();
			fwrite(&size, sizeof(size), 1, handle);
			fwrite(firstName.c_str(), size, 1, handle);

			size = lastName.size();
			fwrite(&size, sizeof(size), 1, handle);
			fwrite(lastName.c_str(), size, 1, handle);

			// sauve les stats
			for (int i = 0; i < eStatCount; ++i)
			{
				fwrite(&stats[i].value, sizeof(stats[i].value), 1, handle);
				fwrite(&stats[i].maxValue, sizeof(stats[i].maxValue), 1, handle);
				fwrite(&stats[i].startValue, sizeof(stats[i].startValue), 1, handle);
			}

			// sauve les objects
			for (int i = 0; i < eBodyPartCount; ++i)
				bodyObjects[i].save(handle);

			// sauve le sort courant
			fwrite(spell, 4, 1, handle);
		}
				
Enfin, la fonction pour sauver tout l'état du jeu ressemble à ça.

		void CGame::saveGame(const char *fileName)
		{
			snapshotLevel();
			FILE*       handle = fopen(fileName, "wb");

			if (handle != NULL)
			{
				// sauve le niveau courant et la position du joueur
				fwrite(¤tLevel, sizeof(currentLevel), 1, handle);
				fwrite(&player.pos.x, sizeof(player.pos.x), 1, handle);
				fwrite(&player.pos.y, sizeof(player.pos.y), 1, handle);
				fwrite(&player.dir, sizeof(player.dir), 1, handle);

				// sauve les personnages
				for (int i = 0; i < 4; ++i)
					game.characters[i].save(handle);

				// sauve les images des maps
				int numSnapshots = mVisitedLevels.size();
				fwrite(&numSnapshots, sizeof(numSnapshots), 1, handle);

				for (int i = 0; i < numSnapshots; ++i)
					mVisitedLevels[i].save(handle);

				fclose(handle);
			}
		}
				
Les fonctions de chargement sont pratiquement les mêmes avec des "fread" au lieu des "fwrite" et quelques lignes en
plus pour allouer la mémoire avant de charger les données.

L'interface disque

Je n'expliquerai pas le code dans cette partie parce qu'il est simple. Je parlerai principalement des graphismes et
de la séquence d'images à afficher.

Dans les graphismes du jeu d'origine, j'ai trouvé seulement une image qui contenait les éléments des interfaces
disque:


Le jeu utilisait probablement beaucoup de bidouilles dans le code pour construire tous les écrans d'interface à
partir de ça. En particulier quand on sait qu'il y a des états "pressés" pour les boutons qui sont assez différents.
Alors j'ai préféré découper cette image en plusieurs parties:


Quand vous cliquez qur la disquette dans la feuille de personnage, l'interface disque principale apparaît:


Celle-ci était un peu différente dans le jeu d'origine:
Le jeu d'origine sauvait seulement sur disquette. Et vous ne pouviez sauvegarder qu'une seule partie par disquette.
Mais on est en 2018 et on n'utilise plus de disquettes. Je programme ce jeu sur un PC avec un disque dur.
Et je pense que ça pourrait être cool d'avoir plusieurs sauvegardes sur ce disque.

Donc quand vous appuierez sur le bouton "CHARGER" ou "SAUVEGARDER" il s'affichera d'abord "enfoncé" pendant un
petit instant...


...puis un sélecteur de fichier apparaitra. Comme le jeu utilise Qt, ça demande juste 1 ligne de code pour
afficher ce sélecteur.


Une fois que vous avez sélectionné le fichier que vous voulez charger ou sauver, une boite "CHARGEMENT..." ou
"SAUVEGARDE..." apparaitra.


En fait, si vous chargez/sauvez depuis un disque dur, cette boite sera probablement trop rapide et vous ne la
verrez pas.
Si vous voulez simuler un chargement plus long pour la voir, vous pouvez mettre la variable dummyDiskTimer dans
CInterface::updateDiskButtons() à disons 60 au lieu de 2.
2 est la valeur minimale pour être sur que cette boite soit affichée au moins 1 frame.

Une fois que cette boite est affichée, un flag est mis pour appeler la fonction de chargement ou de sauvegarde au
début de MainLoop().
Remarquez que j'ai aussi déplacé la fonction pour prendre les escaliers ici.

		QImage& CGame::mainLoop()
		{
			static QImage image(SCREEN_WIDTH,
			                    SCREEN_HEIGHT,
			                    QImage::Format_RGBA8888);

			// évènements spéciaux
			if (player.shouldTakeStairs)
				player.takeStairs();

			if (interface.shouldLoad == true)
			{
				loadGame(interface.saveName.c_str());
				interface.shouldLoad = false;
				interface.setMainState(CInterface::eMainLoadedDialog);
			}

			if (interface.shouldSave == true)
			{
				saveGame(interface.saveName.c_str());
				interface.shouldSave = false;
				interface.setMainState(CInterface::eMainSavedDialog);
			}

			// efface les zones souris
			[...]
		}
				
Puis, si vous étiez en train charger, une boite "JEU CHARGE" apparaitra:


Si vous appuyez sur le bouton "OK", vous reviendrez à la vue 3D.

Le jeu d'origine n'avait pas de boite "JEU SAUVE", alors j'en ai ajouté une:


Si vous appuyez sur le bouton "OK" ici, vous reviendrez à l'interface disque principale.