Partie 7: Se déplacer dans la map

Téléchargements

Code source
Exécutable de l'éditeur de niveaux - exactement le même que la partie 5 (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.

Le timer

Maintenant que allons ajouter du mouvement dans le jeu, on va avoir besoin de mettre à jour l'image à un rythme constant.
A l'époque où Dungeon Master est sorti, les jeux se basaient sur l'interruption de synchronisation verticale des circuits graphiques.
Mais sur les PC modernes ce n'est pas facile d'accéder à ce signal, car ça dépend du pilote graphique et de l'API qu'on utilise (OpenGL ou DirectX).
De plus sur les moniteurs actuels, les taux de rafraichissements sont beaucoup plus variables (par ex. on peut trouver des moniteur à 120Hz).

Donc pour simuler une bonne vieille V-Blank à 60Hz j'ai utilisé un timer dans main.qml:

		Timer  {
				id: elapsedTimer
				interval: 1000/60;
				running: true;
				repeat: true;
				onTriggered: {
					main.image1.source = "image://myimageprovider/" + frame;
					frame++;
				}
			}
				
Il appelle l'ImageProvider approxiamtivement 60 fois par secondes. Mais il n'est pas très constant comme vous pouvez le voir si vous dé-commentez les lignes
suivantes dans bridge.cpp

		/*    static clock_t  lastClock;
			clock_t  curClock = clock();
			qDebug() << (((float)(curClock - lastClock)) * 1000.0f /CLOCKS_PER_SEC);
			lastClock = curClock;
		*/
				
Ce code affiche dans la console de QtCreator's le temps entre 2 frames en milli-secondes.
Mais même s'il n'est pas très précis, ça suffira pour le moment.

Accélération

Dès la première version du jeu on a affiché des image (au moins le sol et le plafond).
C'était fait dans la fonction displayMainView() dans game.cpp.
Mais regardons comment on les chargeait dans la dernière partie:

		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);
				
Naturellement ça vaut aussi pour les graphismes des murs.
Vous pouvez voir qu'à chaque fois qu'on affiche une image, on la charge à partir du disque et on la convertit en QImage.
Si on continue à faire ça, le disque dur tournera en permanence et ça va ralentir notre jeu.

Donc dans "common/sources/system" j'ai ajouté les fichiers "filecache.*". Regardond une parie du ".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;
				
Chaque fois qu'on aura besoin de charger un fichier qu'on utilisera souvent, on va appeler la fonction getFile().
La première fois, elle charge le fichier et le stocke dans "mCache", puis à chaque fois qu'on l'appelle pour le même fichier,
elle n'a pas besoin de re-charger le fichier puisqu'il est déjà en mémoire.
J'ai aussi ajouté un 2ième niveau de cache pour les images. Quand on appelle getImage(), elle appelle getFile() en interne, mais
elle évite aussi de refaire la conversion du fichier PNG en QImage.

Donc mainteanant pour charger les images dans le jeu, on va appeler:

		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);
				

Le clavier

Normalement les touches sont gérées dans la partie QML. Mais dans notre cas, on va seulement les utiliser dans la partie C++.
Donc on va les intercepter. Avec Qt, on réalise ça en installant un "Event Filter". C'est ce qu'on va faire à la fin de "main.cpp":

		// enregistre l'EventFilter pour le clavier
		QApplication::instance()->installEventFilter(&keyboard);

		int retValue = app.exec();

		// désenregistre l'EventFilter
		QApplication::instance()->removeEventFilter(&keyboard);

		return retValue;
				
L'Event Filter lui-même se trouve dans le fichier keyboard.cpp dans les sources:

		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' en QWERTY, 'A' en AZERTY
						player.turnLeft();
						break;

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

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

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

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

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

					default:
						break;
					}
				}
				return true;
			}

			return false;
		}
				
Il n'y a pas grand chose à expliquer ici. Comme vous le voyez, pour chaque touche on appelle une fonction dans "player.cpp" pour
bouger ou tourner.
J'ai utilisé les touches classiques "QWE-ASD" ("AZE-QSD" en AERTY) et j'ai utilisé les "scan codes" natif pour que ça marche avec les différents
types de claviers.
Mais en fait je dois avouer que je n'ai essayé qu'avec un clavier AZERTY...

Bouger

Voila les fonctions de déplacement dans 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;
		}
				
Les fonctions "turn" changent simplement la direction que l'on regarde.
Les fonctions "move" appellent d'abord getWallSide() pour récupérer la direction du déplacement en fonction de la direction dans laquelle
on regarde, et la passe à moveIfPossible().
Et moveIfPossible() calcule les nouvelles coordonnées et teste le mur correspondant de la case courante pour voir si on peut bouger.

Donc on met seulement à jour les variables "dir" et "pos" de la classe CPlayer. On n'a pas besoin de faire autre chose, car displayMainView()
utilise déjà ces variables pour dessiner les murs appropriés.