Walking in the map

Downloads

Source code
Executable for the map editor - exactly the same as in part 5 (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.

The timer

Now that we will add movement in the game we need to update the display at a constant rate.
At the time when Dungeon Master was first released, the games relied on the vertical blank interrupt of the graphics circuits.
On modern PCs it is not so easy to access this signal, as it depends on the graphics driver and on the underlying API used - OpenGL or DirectX.
Moreover today's monitors refresh rates are more varied - i.e. we can find screens at 120Hz.

So to simulate an old V-Blank at 60Hz I used a timer in main.qml:

		Timer  {
				id: elapsedTimer
				interval: 1000/60;
				running: true;
				repeat: true;
				onTriggered: {
					main.image1.source = "image://myimageprovider/" + frame;
					frame++;
				}
			}
				
It calls the image provider roughly 60 times per seconds. It's not very constant, as you can see if you uncomment the following lines in bridge.cpp

		/*    static clock_t  lastClock;
			clock_t  curClock = clock();
			qDebug() << (((float)(curClock - lastClock)) * 1000.0f /CLOCKS_PER_SEC);
			lastClock = curClock;
		*/
				
This code displays in QtCreator's console the time between two frames in milliseconds.
But even though it is not very precise, it will be sufficient for the moment.

Speeding up

Since the first version of the game project we have drawn images - at least the floor and the ceiling.
It was done in the function displayMainView() in game.cpp.
But take a look at how we loaded them in the last part:

		QImage  floorGfx("gfx/3DView/Floor.png");
		QImage  ceilingGfx("gfx/3DView/Ceiling.png");

		graph2D.drawImage(&image, CVec2(0, 33), ceilingGfx);
		graph2D.drawImage(&image, CVec2(0, 72), floorGfx);
				
Obviously the same goes for the wall graphics too.
You can see that each time we draw an image, we load it from the disk and we convert it to a QImage.
If we keep on doing this, the hard disk will constantly turn and it will slow down our game.

So in "common/sources/system" I added the files "filecache.*". Let's have a look at a part of the ".h":

		class CFileCache
		{
		public:
			CFileCache();
			virtual ~CFileCache();

			CFileData   getFile(std::string fileName);
			QImage      getImage(std::string fileName);

		private:
			std::map<std::string, CFileData>    mCache;
			std::map<std::string, QImage>       mImageCache;
		};

		extern CFileCache   fileCache;
				
Every time we will need to load a file that we will use frequently, we will call the function getFile().
The first time, it loads the file and store it in "mCache", then every time we call it for the same file, it does not have to
re-load the file again, as it is already stored in memory.
Additionnally, I added a second level of cache for the images. When we call getImage(), internally it calls getFile(), but
it also avoids the conversion from the PNG file to a QImage.

So now, to load our images in the game, we will call:

		QImage  floorGfx = fileCache.getImage("gfx/3DView/Floor.png");
		QImage  ceilingGfx = fileCache.getImage("gfx/3DView/Ceiling.png");

		graph2D.drawImage(&image, CVec2(0, 33), ceilingGfx);
		graph2D.drawImage(&image, CVec2(0, 72), floorGfx);
				

The keyboard

Normally, the keys are handled int the QML part. But in our case we will only use them in the C++ part.
So we will intercept them. With Qt, we achieve that by installling an Event Filter. So, at the end of "main.cpp", you will find:

		// register the event filter for the keyboard
		QApplication::instance()->installEventFilter(&keyboard);

		int retValue = app.exec();

		// unregister the event filter before exiting
		QApplication::instance()->removeEventFilter(&keyboard);

		return retValue;
				
The Event Filter itself is in the file keyboard.cpp in the sources folder:

		bool CKeyboard::eventFilter(QObject */*object*/, QEvent *event){

			if (event->type() == QEvent::KeyPress)
			{
				QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);

				if (keyEvent->isAutoRepeat() == false)
				{
					quint32 nativeKey = keyEvent->nativeScanCode();

					switch (nativeKey)
					{
					case 16:    // 'Q' on QWERTY, 'A' on AZERTY
						player.turnLeft();
						break;

					case 17:    // 'W' on QWERTY, 'Z' on AZERTY
						player.moveForward();
						break;

					case 18:    // 'E'
						player.turnRight();
						break;

					case 30:    // 'A' on QWERTY, 'Q' on AZERTY
						player.moveLeft();
						break;

					case 31:    // 'S'
						player.moveBackward();
						break;

					case 32:    // 'D'
						player.moveRight();
						break;

					default:
						break;
					}
				}
				return true;
			}

			return false;
		}
				
There is not so much to say about it. As you can see, for every key we call a function in "player.cpp" to move or turn.
I used the well known "QWE-ASD" combination and used the native "scan code" so that it works with different keyboard layouts.
Though I need to confess that I only tried with an AZERTY keyboard...

Moving

Now, for the movement functions in player.cpp:

		//-----------------------------------------------------------------------------------------
		void        CPlayer::turnLeft()
		{
			dir = (dir + 1) % 4;
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::turnRight()
		{
			dir = (dir + 4 - 1) % 4;
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveLeft()
		{
			EWallSide   side = getWallSide(eWallSideLeft);
			moveIfPossible(side);
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveRight()
		{
			EWallSide   side = getWallSide(eWallSideRight);
			moveIfPossible(side);
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveForward()
		{
			EWallSide   side = getWallSide(eWallSideUp);
			moveIfPossible(side);
		}

		//-----------------------------------------------------------------------------------------
		void        CPlayer::moveBackward()
		{
			EWallSide   side = getWallSide(eWallSideDown);
			moveIfPossible(side);
		}

		//-----------------------------------------------------------------------------------------
		void CPlayer::moveIfPossible(EWallSide side)
		{
			CVec2   newPos = pos;

			switch (side)
			{
			case eWallSideUp:
				newPos.y--;
				break;

			case eWallSideDown:
				newPos.y++;
				break;

			case eWallSideLeft:
				newPos.x--;
				break;

			case eWallSideRight:
				newPos.x++;
				break;

			default:
				break;
			}

			CTile*  tile = map.getTile(pos);

			if (tile->mWalls[side].mType == 0)
				pos = newPos;
		}
				
The "turn" functions only change the direction we are looking at.
The "move" functions first call getWallSide() to get the direction of the movement according to the direction we are looking at,
and pass it to moveIfPossible().
And moveIfPossible() computes new coordinates and tests the corresponding inner wall of the current tile to see if we can move.

So, we only update the variables "dir" and pos of the player's class. We don't need to do anything else, as displayMainView()
already uses these variables to draw the walls accordingly.