Partie 12: Les décors des murs - Partie 1

Téléchargements

Code source
Exécutable de l'éditeur de niveaux (Windows 32bits)

Le jeu ne devrait pas fonctionner dans cette partie. Je corrigerai ça dans la prochaine.

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.

Les décors

Les "ornates" (décors) sont tous les éléments graphiques qui apparaissent sur le sol ou sur les murs.
Ils peuvent être purement décoratifs - mousse, crochets, fers... - mais ils peuvent aussi être des éléments interactifs comme des boutons, des leviers,
les tableaux des champions dans le premier niveau, les serrures, les autels ou les textes écrits sur les murs.

Voici un aperçu rapide des décors dans les graphismes d'origine.


Maintenant essayons d'afficher un simple décor (disons les "fers" parce qu'ils sont gros).
Quand on regarde les graphismes pour les fers, on voit qu'il y a seulement 2 sprites, un de face et un pour le coté gauche.


Mais dans le jeu on peut les voir sur différents murs, loins ou proches:


Alors comment ça marche ?
D'abord il est évident que si le décor doit apparaitre sur un mur de droite au lieu d'un mur de gauche, l'image pour la gauche a simplement été
retournée horizontalement.

Ensuite, comme vous pouvez le voir, plus le mur est loin, plus le décor apparait petit.
A l'époque, il y avait peu d'ordinateurs avec des sprites zoomés. Donc les versions réduites des décors étaient précalculés au début du jeu.
Les routine graphiques qu'on utilise peuvent étirer les images, donc on n'aura pas besoin de les précalculer. En fait, si vous avez regardé les
sources quand on retournait les murs dans la partie précédente, on faisait une mise à l'échelle de -1 sur l'axe des X.

Les images apparaissent aussi plus sombres quand elles sont plus loin de nous, pour s'accorder à la couleur des murs. C'est clairement visible sur
la pomme, et un peu moins perceptible sur les fers.
C'est parce qu'il y a une autre limitation à prendre en compte. Les graphismes du jeu étaient pallétisés, donc quand ils étaient précalculés,
les pixels pouvaient seulement prendre un petit nombre de couleurs.
On n'aura pas cette limitation non plus, donc nos décors devraient être plus jolis.

Mais nous parlerons des détails de l'affichage plus tard. Maintenant créons une base de données qui contienne les informations dont on aura besoin.
On l'appellera "ornates.xml":

		<?xml version="1.0" encoding="ISO-8859-1"?>
		<ornates>
			<ornate name="Aucun">
			</ornate>

			<ornate name="Fers">
				<image_front>Manacles_front.png</image_front>
				<image_side>Manacles_side.png</image_side>
				<pos_front x="66" y="71"/>
				<pos_side x="30" y="73"/>
			</ornate>
		</ornates>
				
Nous avons les fichiers graphiques pour nos 2 sprites qui correspondent aux murs les plus proches, et leurs coordonnées à l'écran.
Le jeu lira toutes ces données et calculera les positions pour les images réduites en fonction des positions des murs.

Cette base de données sera aussi copiée dans le répertoire "data" de l'éditeur. L'éditeur aura seulement besoin des noms des décors.
Note pour plus tard: Il faudra que je réorganise les répertoires "data" pour éviter ces copies...
Mais ragardons comment nous allons ajouter les décors dans l'éditeur.

Paramètres additionnels dans "walls.xml"

Quand on sélectionne un mur dans l'éditeur, on veut voir quelque chose comme ça:


Mais la 2ième ComboBox ne sera pas créée comme la première. En fait il va nous falloir des paramètres différents pour chaque type de mur.
Disons par exemple que ce premier type sera un mur simple avec seulement un "ornate" décoratif.
Le type 2 pourrait être un mur avec un texte, donc il nous faudra un un paramètre de type "texte".
Le 3ième type serait pour les boutons et les leviers, donc on aurait 2 ComboBoxes de décor (une pour l'état off, et une pour l'état on)
et peut-être un paramètre booléen pour stocker l'état du bouton.
Et ainsi de suite.

Pour que ça soit flexible, on ne va pas hard-coder ces types dans l'éditeur.
Donc il faudra qu'on crée dynamiquement les ComboBoxes, les champs de texte, et autres boutons pour ces paramètres.
D'abord on va modifier "walls.xml" pour définir notre paramètre additionnel:

		<?xml version="1.0" encoding="ISO-8859-1"?>
		<walls>
			<wall name="Rien">
			</wall>

			<wall name="Simple">
				<image>Wall.png</image>
				<param type="ornate">Decor</param>
			</wall>
		</walls>
				
Pour le moment on n'a qu'un type de paramètre. On le définit dans un enum pour que le code soit plus lisible quand on aura plus de types:

		enum EParamType
		{
			eParamOrnate = 0,
		};
				
Pour stocker les noms de paramètres dans le code, on définit un vecteur de vecteurs de structures CParamType dans la map:

		struct CParamType
		{
			EParamType  mType;
			QString     mName;
		};

		class CMap
		{
			[...]
			std::vector<std::vector<CParamType>>  mWallsParams; // liste des paramètres pour chaque type de mur
			[...]
		};
				
Il y a 2 vecteurs parce que:
  • On a plusieurs types de murs
  • Pour chaque type de mur il peut y avoir plusieurs paramètres

  • Il faut qu'on modifie readWallsDB() pour lire ces types de paramètres et ces noms:
    
    		void    CEditor::readWallsDB(QString fileName)
    		{
    			[...]
    
    			while(!wall.isNull())
    			{
    				std::vector paramsTypesList;
    
    				if (wall.tagName() == "wall")
    				{
    					CWallData   newWall;
    
    					[...]
    
    					while(!wallInfo.isNull())
    					{
    						if (wallInfo.tagName() == "image")
    						{
    							[...]
    						}
    						else if (wallInfo.tagName() == "param")
    						{
    							QString paramType = wallInfo.attribute("type");
    
    							if (paramType == "ornate")
    							{
    								CParamType  newParamType;
    								newParamType.mType = eParamOrnate;
    								newParamType.mName = wallInfo.text();
    								paramsTypesList.push_back(newParamType);
    							}
    						}
    
    						wallInfo = wallInfo.nextSibling().toElement();
    					}
    					wallsDatas.push_back(newWall);
    					map.mWallsParams.push_back(paramsTypesList);
    				}
    				wall = wall.nextSibling().toElement();
    			}
    		}
    				

    Les valeurs des paramètres

    Maintenant que l'on a les types de paramètres et les noms, voyons comment on stocke leurs valeurs dans chaque mur.
    On définit une classe de base CParam, et chaque type de paramètre va hériter de cette classe:
    
    		class CParam
    		{
    		public:
    			EParamType  mType;
    		};
    
    		class CParamOrnate : CParam
    		{
    		public:
    			CParamOrnate();
    			uint8_t mValue;
    		};
    				
    La classe CWall va contenir une liste de ces CParams:
    
    		std::vector<CParam*>    mParams;
    				
    Mais en fait la classe CWall a complètememnt changé...
    Avant, elle contenait seulement une variable mType. Mais maintenant, chaque fois qu'on va changer ce type, on devra réallouer la liste de
    tous les paramètres qui sont associés à ce type.
    Donc voilà à quoi ressemble la déclaration de CWall maintenant:
    
    		class CWall
    		{
    		public:
    			CWall();
    			virtual ~CWall();
    
    			void    setType(uint8_t type);
    			uint8_t getType();
    			CWall&  operator=(CWall& rhs);
    			void    load(FILE* handle);
    			void    save(FILE* handle);
    
    			std::vector<CParam*>    mParams;
    
    		private:
    			void    deleteParams();
    
    			uint8_t mType;
    		};
    				
    Laissez-moi vous expliquer ces fonctions.

    getType() renvoie simplement la valeur de mType, il n'y a rien de spécial là dedans.
    
    		uint8_t CWall::getType()
    		{
    			return  mType;
    		}
    				
    deleteParams() efface la liste des paramètres:
    
    		void CWall::deleteParams()
    		{
    			for (size_t i = 0; i < mParams.size(); ++i)
    				delete mParams[i];
    			mParams.clear();
    		}
    				
    La fonction setType() change la valeur de mType. Et comme je l'ai dit, elle réalloue les paramètres pour ce type:
    
    		void CWall::setType(uint8_t type)
    		{
    			if (mType == type)
    				return;
    
    			mType = type;
    
    			// alloue les paramètres
    			deleteParams();
    			std::vector<CParamType>& paramTypes = map.mWallsParams[type];
    
    			for (size_t i = 0; i < paramTypes.size(); ++i)
    			{
    				CParam* newParam = NULL;
    
    				switch (paramTypes[i].mType)
    				{
    					case eParamOrnate:
    						newParam = (CParam*)(new CParamOrnate);
    						break;
    
    					default:
    						break;
    				}
    				mParams.push_back(newParam);
    			}
    		}
    				
    Donc j'ai du changer tous les endroits dans le code où le mType du mur était utilisé ou modifié pour les remplacer avec ces fonction "get" et "set".

    load() et save() sont assez explicites, elles sont utilisées pour sauvegarder le type de mur avec tous ses paramètres.

    Et finalement l'opérateur "=" est utilisé pour les fonctions copier/couper/coller que j'ai du modifier aussi.

    Créer les widgets

    Maintenant qu'on a les paramètres et leurs valeurs, c'est le moment de voir comment on les affiche dans l'éditeur.
    Donc pour notre simple paramètre, il faut qu'on affiche un label pour son nom et une ComboBox.


    On va utiliser 2 fichiers qml comme modèles que l'on chargera pendant l'éxécution:
    "ParamLabel.qml"...
    
    		import QtQuick 2.5
    		import QtQuick.Controls 1.4
    
    		Label {
    			height: 20
    			text: qsTr("Test")
    			verticalAlignment: Text.AlignVCenter
    			horizontalAlignment: Text.AlignLeft
    			renderType: Text.QtRendering
    		}
    				
    ... et "ParamComboBox.qml".
    
    		import QtQuick 2.5
    		import QtQuick.Controls 1.4
    
    		ComboBox {
    			property int identifier: -1
    			currentIndex: -1
    			onCurrentIndexChanged: bridge.setSelParamCombo(identifier, currentIndex);
    		}
    				
    On va ajouter ces widgets aux grilles dans les différentes "feuilles" de sélection, alors il faut qu'on récupère des pointeurs sur ces
    grilles dans "main.cpp":
    
    		QQuickItem*     tileSelGrid;
    		QQuickItem*     wallSelGrid;
    		QQuickItem*     objSelGrid;
    		
    		[...]
    
    		QObject *rootObject = engine.rootObjects().first();
    		tileSelGrid = qobject_cast(rootObject->findChild("tSelGrid"));
    		wallSelGrid = qobject_cast(rootObject->findChild("wSelGrid"));
    		objSelGrid = qobject_cast(rootObject->findChild("oSelGrid"));
    				
    Nos widgets seront créés dans la fonction toolSelect(), chaque fois qu'on sélectionne un seul mur:
    
    		void    CEditor::toolSelect()
    		{
    			if (bridge.leftPressed == true)
    			{
    				[...]
    				
    				switch (bridge.mTabIndex)
    				{
    					[...]
    
    				case eTabWalls:
    					if (isSelectSingleWall() == true)
    					{
    						EWallSide   side = getWallSideAbs(mSelectStart);
    						uint8_t     type = map.getTile(rect.tl)->mWalls[side].getType();
    						bridge.set_selWallType(type);
    
    						deleteParamItems(&mWallParamItems);
    						addWallParamsItems(type);
    					}
    					break;
    
    				default:
    					break;
    				}
    
    				bridge.updateQMLImage();
    			}
    		}
    		
    		[...]
    		
    		void    CEditor::addWallParamsItems(uint8_t type)
    		{
    			CVec2       pos = mSelectStart / TILE_SIZE;
    			EWallSide   side = getWallSideAbs(mSelectStart);
    			CWall*      wall = &map.getTile(pos)->mWalls[side];
    
    			std::vector<CParamType>& sourceList = map.mWallsParams[type];
    
    			for (size_t i = 0; i < sourceList.size(); ++i)
    			{
    				CParam* param = wall->mParams[i];
    
    				if (sourceList[i].mType == eParamOrnate)
    				{
    					CParamOrnate*   par = (CParamOrnate*)param;
    					addLabel(wallSelGrid, sourceList[i].mName + ":", &mWallParamItems);
    					addComboBox(wallSelGrid, bridge.ornatesList, &mWallParamItems, i, par->mValue);
    				}
    			}
    		}
    				
    Et voici les fonction qui chargent vraiment les qml et qui créent des widgets à partir d'eux. C'est un peu de la "magie Qt".
    
    		//---------------------------------------------------------------------------------------------
    		QQuickItem* CEditor::loadQml(QString file, QQuickItem* parent)
    		{
    			QQmlComponent   component(myQmlEngine, QUrl(file));
    			QQuickItem*     object = qobject_cast<QQuickItem*>(component.create());
    			QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
    			object->setParent(parent);
    			object->setParentItem(parent);
    			return object;
    		}
    
    		//---------------------------------------------------------------------------------------------
    		void CEditor::addLabel(QQuickItem* parent, QString text, std::vector<QQuickItem *> *list)
    		{
    			QQuickItem* object = loadQml("qrc:/ParamLabel.qml", parent);
    			object->setProperty("text", QVariant(text));
    			list->push_back(object);
    		}
    
    		//---------------------------------------------------------------------------------------------
    		void CEditor::addComboBox(QQuickItem* parent, QStringList& values, std::vector<QQuickItem*>* list, int id, int index)
    		{
    			QQuickItem* object = loadQml("qrc:/ParamComboBox.qml", parent);
    			object->setProperty("model", QVariant(values));
    			object->setProperty("identifier", QVariant(id));
    			object->setProperty("currentIndex", QVariant(index));
    			list->push_back(object);
    		}
    				
    Bon, c'était beaucoup de travail, mais maintenant on a un moyen d'ajouter des paramètres variés pour un type de mur donné.
    La prochaine fois on verra comment retrouver ces données dans le jeu pour finalement afficher des décors.