Part 30: Level 2, loading and saving the game

Downloads

Source code
Executable for the map editor - exactly the same as in part 29 (Windows 32bits)
Executable for the game (Windows 32bits)

Before you try to compile the code go to the "Projects" tab on the left menu, select the "Run" settings for your kit,
and set the "Working directory" to the path of "editor\data" for the editor or "game\data" for the game.

Level 2

I created a draft of level 2.


When I tested it, I only found a small clipping bug in the doors' drawing that I fixed easily.

In this level we will encounter a lot of new things: monsters, holes in the ground, switches, teleporters, chests...
But the most noticeable difference with the first level is the number of doors and keys to open them.
I put a button on each door so that you can visit the whole level even if we have not yet implemented all the ways to open
them.
Note that if you try to take the stairs at the end of the level, the game will crash too.

But we will talk about all these new things in the next parts.
For now we will make use of the map snapshots from the last part, because if you start to wander in this big level, it will
take time. So in this part, we will implement the loading and saving system.

What to save ?

The save files should contain the states of the levels that we visited. So we will simply save the map snapshots.
We probably could have only saved the differences between the snapshot and the original state of the map, but the levels
files are not too big.
The main drawback of saving the whole map is that if the map format changes, the save file will be invalid.
But it is far more easier to code.

Here is the code to save a map snapshot:

		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);
			}
		}
				
But it's not the only thing we need to save.
Wa also need the current level number, the position of the player and it's direction - that's easy to save...
And we need the state of the characters and the objects they are carrying too.
So here is the save function for a character:

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

			// save the name
			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);

			// save the 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);
			}

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

			// save the current spell
			fwrite(spell, 4, 1, handle);
		}
				
Finally the function to save the full game state looks like this.

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

			if (handle != NULL)
			{
				// save current level and player pos
				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);

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

				// save maps' snapshots
				int numSnapshots = mVisitedLevels.size();
				fwrite(&numSnapshots, sizeof(numSnapshots), 1, handle);

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

				fclose(handle);
			}
		}
				
The loading functions are nearly the same with "fread" instead of "fwrite" and a few additionnal lines to allocate
memory before loading the datas.

The disk interface

I won't explain the code in this part as it is simple. I will mainly talk about the graphics and the sequence of images
to display.

In the graphics of the original game, I found only one image that contained the elements of the disk interfaces:


The game probably used a lot of code tricks to build up all the interface screens from this. Especially when you know
that there are "pressed" states for the buttons that are quite different.
So I prefered to split this image in several parts:


When you click on the floppy disk image in a character's sheet, the main disk interface appears:


This one was a little bit different in the original game:
The original game only saved on floppy disks. And you could only save one game per floppy.
But we are in 2017 and we don't use floppies anymore. I'm coding this game on a PC with a hard disk.
And I think it could be cool to have several saves on this disk.

So when you press the "LOAD" or the "SAVE" button. It will first be displayed "pressed" for a small time...


...then a file selector will appear. As the game is using Qt it only takes 1 line of code to display this selector.


Once you selected the file you wanted to save or load. A "LOADING..." or "SAVING..." box will appear.


In fact, if you are saving/loading from a hard drive, this box will probably be too fast, and you won't see it.
If you want to simulate a longer loading to see it, you can set the dummyDiskTimer variable in CInterface::updateDiskButtons()
to say 60 instead of 2.
2 is the minimal value to be sure that this box is displayed at least during 1 frame.

Once this box is displayed, a flag is sets to call the loading or saving functions at the beginning of the MainLoop(). Note that I also moved the function to take the stairs here.

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

			// special events
			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);
			}

			// clear the mouse areas
			[...]
		}
				
Then, if you were loading, a "GAME LOADED" box will appear:


If you press the "OK" button, you will get back to the 3D view.

The original game didn't have a "GAME SAVED" box, so I added one:


If you press the "OK" button here, you will get back to the main disk box.