Partie 8: Les outils de l'éditeur

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 7 (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.

Les outils

Je vais mettre de coté le jeu pendant un moment et me concentrer sur l'éditeur. Mon but est de l'améliorer et de le rendre plus
facile à utiliser avant d'essayer de reproduire un niveau du jeu d'origine. Puis nous ajouterons les différents éléments du jeu au
fur et à mesure.

J'avais ajouté quelques boutons pour les outils dans la partie 3, maintenant c'est le moment de les faire marcher.
Alors dans cette partie, on va voir l'outil de sélection, le couple défaire/refaire et le trio copier/couper/coller.

D'abord, si vous regardez "main.qml" vous pouvez voir que j'ai ajouté un menu "Edition" qui contient copier, couper, coller, défaire et refaire comme
dans la plupart des logiciels PC.
En QML c'est un moyen facile d'intercepter les raccourcis clavier standard associés à ces fonctions.
Vous remarquerez aussi que j'ai ajouté 3 lignes pour les fonctions copier/couper/coller.
Comme auparavant, ces lignes se contentent d'appeler des fonctions de bridge.cpp qui ne font que stocker des valeurs pour la partie C++ et appeler quelques
fonctions dans editor.cpp.

Défaire et refaire

Pour les fonctions défaire/refaire j'ai utilisé un mécanisme de Qt appelé QUndoStack. En fait c'est simplement une pile qui stocke les commandes
qu'on lui ajoute. Vous pouvez facilement faire la même chose avec une liste chaînée.
Pour l'utiliser, on doit créer une classe pour chaque commande qui dérive de la classe QUndoCommand.
L'idée c'est que chacune de ces commandes sait comment défaire et refaire ses actions, et stocke les données nécessaires pour le faire.
J'ai mis toutes ces classes dans un nouveau fichier "tools.cpp" (et son fichier ".h" correspondant).

Jusqu'à maintenant, on n'avait qu'un outil de dessin. Pour que ce soit plus clair, je l'ai séparé en deux outil drawTile et drawWall.
Alors regardons à quoi ressemble drawTile.
Dans tools.h:

		class drawTileTool : public QUndoCommand
		{
		public:
			explicit drawTileTool(QUndoCommand *parent = 0);

			bool init(CVec2 pos, int type);
			void undo() override;
			void redo() override;

		private:
			CVec2   mPos;
			int     mType;
			int     mOldType;
		};
				
Dans tools.cpp:

		drawTileTool::drawTileTool(QUndoCommand *parent)
			: QUndoCommand(parent)
		{
		}

		bool drawTileTool::init(CVec2 pos, int type)
		{
			mPos = pos / TILE_SIZE;
			CTile*   tile = map.getTile(mPos);

			if (tile == NULL)
				return false;

			if (tile->mType == type)
				return false;

			mOldType = tile->mType;
			mType = type;
			return true;
		}

		void drawTileTool::undo()
		{
			map.setTile(mPos, mOldType);
			bridge.updateQMLImage();
		}

		void drawTileTool::redo()
		{
			map.setTile(mPos, mType);
			bridge.updateQMLImage();
		}
				
Dans le constructeur on ne fait rien à part passer le paramètre "parent" au QUndoCommand. Ce paramètre est utilisé pour lier plusieurs commandes
ensemble, mais nous n'utiliserons pas cette fonctionnalité.

J'ai choisi d'ajouter une fonction init() à chaque commande. Elle initialise les données dont on aura besoin plus tard et elle retourne un booléen qui
dit si on a vraiment besoin d'exécuter cette commande et de la stocker dans la pile.
Donc dans notre cas, on stocke la position de la case qu'on veut modifier dans mPos, le type que l'on veut mettre dans mType, et l'ancien type que la case
contenait avant qu'on exécute la commande dans mOldType.
Comme quand on presse le bouton de la souris, l'outil de dessin est appelé à chaque fois qu'on la déplace, on ne veut pas stocker des commandes inutiles
tant qu'on reste sur la même case, donc si la case a déjà le type qu'on veut mettre, on renvoie "false".

Les fonctions undo et redo son faciles à comprendre: on met le type voulu dans la case et on redessine la map.

Maintenant voyons comment la commande est appelée et ajoutée à la pile d'undo. Vers la fin de "editor.cpp" vous trouverez ces lignes:

		// cases
		drawTileTool*   tool = new drawTileTool();
		int type = 0;

		if (bridge.rightPressed == true)
			type = 0;
		else if (bridge.leftPressed == true)
			type = 1;

		if (tool->init(bridge.mMousePos, type) == true)
		{
			unselect();
			mUndoStack->push(tool);
		}
		else
			delete tool;
				
On crée une nouvelle instance de drawTileTool.
On décide du type que l'on veut mettre en fonction du bouton de la souris qu'on a pressé.
On appelle la fonction init() popur initialiser les variables.
Si la fonction init() renvoie "true", on pousse la commande sur la pile. Notez que quand on la pousse, sa fonction redo() est automatiquement appelée. Donc
la commande est vraiment appliquée à la map.
Si la fonction init() renvoie "false", alors on détruit simplement l'instance de drawTileTool.
La fonction unselect() est liée à l'outil de sélection que nous allons voir tout de suite.

Comme vous pouvez le voir dans les fichiers "tools.*", ce mécanisme d'undo est utilisé pour toutes les fonctions qui modifient la map.

L'outil de sélection

J'avais déjà parlé de l'outil de sélection dans la partie 3. Il vous permet de sélectionner une case ou un mur (bientôt nous ajouterons une boîte avec des
informations à propos de l'élément sélectionné).
Et quand vous glissez la souris, vous pouvez aussi sélectionner un rectangle de cases.
Ces sélections vont être utilisées avec les outils copier/couper/coller.

Mais pour dessiner les sélections j'ai du écrire des fonctions qui réutilisent les routines du curseur.
Maintenant "editor.cpp" contient principalement des fonctions graphiques pour dessiner des rectangles. Au milieu de ces fonctions vous trouverez drawCursor():

		void CEditor::drawCursor(QImage* image, CVec2 mousePos, ETabIndex tabIndex)
		{
			if (mousePos.x != -1)
			{
				if (tabIndex == eTabTiles)
					drawTileRect(image, mousePos, CURSOR_COLOR);
				else if (tabIndex == eTabWalls)
					drawWallRect(image, mousePos, CURSOR_COLOR);
				else if (tabIndex == eTabObjects)
					drawObjectRect(image, mousePos, CURSOR_COLOR);
			}
		}
				
Comme vous pouvez le voir elle appelle drawTileRect(), drawWallRect() ou drawObjectRect() en fonction de l'onglet sélectionné dans la tabView.

Et vous pouvez voir une fonction très similaire pour dessiner la sélection:

		void CEditor::drawSelection(QImage* image, ETabIndex tabIndex)
		{
			if (mSelectStart.x != -1)
			{
				if (tabIndex == eTabTiles)
					drawTileSelection(image);
				else if (tabIndex == eTabWalls)
					drawWallSelection(image);
				else if (tabIndex == eTabObjects)
					drawObectSelection(image);
			}
		}
				
Ces 3 fonctions drawTileSelection(), drawWallSelection() et drawObectSelection() appellent les même fonctions que pour le curseur:
drawTileRect(), drawWallRect() et drawObjectRect() si on a sélectionné seulement un élément, ou elles appellent drawRectangleSelection() si on a
sélectionné un rectangle de plusieurs cases.

Copier, couper et coller

La base de ces fonctions est la classe CClipboard qui se trouve dans les fichiers "clipboard.*".
Cette classe stocke seulement un tableau rectangulaire de cases et contient des fonctions pour le copier de/vers la map, ou d'un autre objet clipboard.

L'outil copier est simple. On copie seulement les données de la map dans la structure clipboard.

L'outil couper est un peu différent car il modifie la map (il efface la zone sélectionnée). Donc, il a une classe dans tools.cpp car on doit être capable de
"défaire" cette modification.

L'outil coller fonctionne comme l'outil de dessin ou de sélection. On sélectionne d'abord l'outil, puis on doit cliquer sur la map pour coller la sélection.
Pour l'instant, seul un rectangle apparait pour montrer l'endroit où la sélection va être collée. A l'avenir j'ajouterai peut-être un "aperçu" des cases
ou des murs qui vont être modifiés.

Et pour l'instant il y a encore un petit bug: vous ne pouvez pas copier un mur seul. Les opérations de copier/couper/coller s'appliquent à la case entière où
est le mur.