Partie 21: Objets - Partie 1: Dans l'éditeur

Téléchargements

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

Le jeu ne devrait pas fonctionner avec le nouveau format de map. Je corrigerai ça dans la prochaine partie.

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.

Stocker les objets

Il y a 4 positions dans chaque case pour poser les objets.


Chaque position peut contenir un tas avec plusieurs objets.
Mais ça serait gacher de la mémoire et de l'espace disque si on stockait un tas dans chaque position possible de
la map, car la plupart des cases ne contiendront pas d'objet.
On va plutot faire une liste de tous les tas d'objets de la map.

		class CMap
		{
			[...]

			std::vector<CObjectStack>   mObjectsStacks;
		};
				
On aura besoin de quelques fonctions pour manipuler ces tas (en ajouter un à la liste, en retirer un, trouver si
un tas existe à une position donnée dans la map).

		//---------------------------------------------------------------------------------------------
		int CMap::findObjectsStackIndex(CVec2 pos)
		{
			for (size_t i = 0; i < mObjectsStacks.size(); ++i)
				if (mObjectsStacks[i].mPos == pos)
					return i;

			return -1;
		}

		//---------------------------------------------------------------------------------------------
		CObjectStack* CMap::findObjectsStack(CVec2 pos)
		{
			int i = findObjectsStackIndex(pos);

			if (i != -1)
				return &mObjectsStacks[i];
			else
				return NULL;
		}

		//---------------------------------------------------------------------------------------------
		CObjectStack* CMap::addObjectsStack(CVec2 pos)
		{
			int i = findObjectsStackIndex(pos);

			if (i != -1)
			{
				return &mObjectsStacks[i];
			}
			else
			{
				CObjectStack    newStack;
				newStack.mPos = pos;
				mObjectsStacks.push_back(newStack);
				return &mObjectsStacks[mObjectsStacks.size() - 1];
			}
		}

		//---------------------------------------------------------------------------------------------
		void CMap::removeObjectsStack(CVec2 pos)
		{
			int i = findObjectsStackIndex(pos);

			if (i != -1)
				mObjectsStacks.erase(mObjectsStacks.begin() + i);
		}
				
Dans le fichier map, ces tas seront sauvés après les données des cases et des murs:

		void CMap::save(char* fileName)
		{
			FILE*       handle = fopen(fileName, "wb");

			if (handle != NULL)
			{
				// entête, version, révision
				[...]

				// taille de la map
				[...]

				// données de la map
				[...]

				// objets
				uint32_t    numStacks = mObjectsStacks.size();
				fwrite(&numStacks, 4, 1, handle);

				for (size_t i = 0; i < numStacks; ++i)
					mObjectsStacks[i].save(handle);

				fclose(handle);
			}
		}
				
Bien entendu, le code est similaire pour le chargement.

La classe "tas"

Maintenant définissons la classe CObjectStack:

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

			void            addObject(CObject object);
			CObject&        getObject(int index);
			void            removeObject(int index);
			size_t          getSize();
			CObjectStack&   operator=(const CObjectStack& rhs);
			void            load(FILE* handle);
			void            save(FILE* handle);

			CVec2   mPos;

		private:
			std::vector<CObject>    mObjects;
		};
				
Elle stocke sa position dans la map (mPos) et une liste de ses objets (mObjects).
Vous pouvez voir des fonctions pour ajouter un objet à la liste, récupérer un objet à un index donné, et en retirer
un de la liste.
getSize renvoie le nombre d'objets du tas.
Comme pour les cases et les murs, on surcharge l'opérateur "=" pour les outils de copie.
Et enfin, il y a les fonctions load() et save().

L'implémentation est assez facile à comprendre.

		//---------------------------------------------------------------------------------------------
		void CObjectStack::addObject(CObject object)
		{
			mObjects.push_back(object);
		}

		//---------------------------------------------------------------------------------------------
		CObject& CObjectStack::getObject(int index)
		{
			return mObjects[index];
		}

		//---------------------------------------------------------------------------------------------
		void CObjectStack::removeObject(int index)
		{
			mObjects.erase(mObjects.begin() + index);
		}

		//---------------------------------------------------------------------------------------------
		size_t CObjectStack::getSize()
		{
			return mObjects.size();
		}

		//---------------------------------------------------------------------------------------------
		CObjectStack& CObjectStack::operator=(const CObjectStack& rhs)
		{
			mPos = rhs.mPos;
			mObjects.resize(rhs.mObjects.size());

			for (size_t i = 0; i < mObjects.size(); ++i)
				mObjects[i] = rhs.mObjects[i];

			return *this;
		}

		//---------------------------------------------------------------------------------------------
		void CObjectStack::load(FILE* handle)
		{
			mObjects.clear();

			fread(&mPos.x, sizeof(int32_t), 1, handle);
			fread(&mPos.y, sizeof(int32_t), 1, handle);
			uint32_t size;
			fread(&size, sizeof(uint32_t), 1, handle);

			for (uint32_t i = 0; i < size; ++i)
			{
				CObject object;
				object.load(handle);
				mObjects.push_back(object);
			}
		}

		//---------------------------------------------------------------------------------------------
		void CObjectStack::save(FILE* handle)
		{
			fwrite(&mPos.x, sizeof(int32_t), 1, handle);
			fwrite(&mPos.y, sizeof(int32_t), 1, handle);
			uint32_t    size = mObjects.size();
			fwrite(&size, sizeof(uint32_t), 1, handle);

			for (size_t i = 0; i < mObjects.size(); ++i)
				mObjects[i].save(handle);
		}
				

La classe objet

Maintenant jetons un oeil à un objet:

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

			void        setType(uint8_t type);
			uint8_t     getType();
			CObject&    operator=(const CObject& rhs);
			void        load(FILE* handle);
			void        save(FILE* handle);

		private:
			uint8_t mType;
		};
				
Il contient seulement un type, mais j'en ai fait une classe qui ressemble à celles pour les cases et les murs,
parce qu'à l'avenir les objets auront aussi des paramètres additionnels basés sur leurs types.
Par exemple, un parchemin aura un paramètre pour son texte.

L'implémentation de cette classe devrait être facile à comprendre aussi:

		//---------------------------------------------------------------------------------------------
		CObject::CObject()
		{
			mType = 0;
		}

		//---------------------------------------------------------------------------------------------
		CObject::~CObject()
		{
		}

		//---------------------------------------------------------------------------------------------
		void CObject::setType(uint8_t type)
		{
			mType = type;
		}

		//---------------------------------------------------------------------------------------------
		uint8_t CObject::getType()
		{
			return mType;
		}

		//---------------------------------------------------------------------------------------------
		CObject&  CObject::operator=(const CObject& rhs)
		{
			setType(rhs.mType);
			return *this;
		}

		//---------------------------------------------------------------------------------------------
		void CObject::load(FILE* handle)
		{
			uint8_t type;
			fread(&type, 1, 1, handle);
			setType(type);
		}

		//---------------------------------------------------------------------------------------------
		void CObject::save(FILE* handle)
		{
			fwrite(&mType, 1, 1, handle);
		}
				

Afficher les tas dans l'éditeur

Pour représenter les tas dans l'éditeur, j'ai choisi d'afficher un point orange.


Voici le code pour l'afficher dans CEditor::displayMap():

		QImage CEditor::displayMap(CMap& map, CVec2 mousePos, ETabIndex tabIndex)
		{
			[...]

			if (map.isLoaded() == true)
			{
				// dessine les cases
				[...]

				// dessine les murs
				[...]

				// dessine les objets
				for (int y = 0; y < map.mSize.y * 2; ++y)
					for (int x = 0; x < map.mSize.x * 2; ++x)
					{
						CVec2   pos(x, y);

						if (map.findObjectsStack(pos) != NULL)
						{
							QImage  objImage = fileCache.getImage("tiles/Object.png");
							CVec2   pos2 = pos * (TILE_SIZE / 2);
							graph2D.drawImage(&image, pos2, objImage);
						}
					}
			}

			[...]
		}
				
On boucle simplement sur toutes les positions possibles de la map, et on affiche un point si on a trouvé un tas à
cette position.

La liste d'objets

On a maintenant un onglet "Objets" avec les noms de tous les différents objets du jeu.


C'est pratiquement la même que celles pour les cases et les murs que l'on a vues précédemment.
Les noms des objets sont lus à partir d'une base de données "items.xml":

		<?xml version="1.0" encoding="utf-8"?>
		<items>
			<item name="[Aucun]"/>
			<item name="Oeil du Temps"/>
			<item name="Rond d'Eclairs"/>
			<item name="Torche"/>
			[...]
		</items>
				
Le jeu utilisera une base de données différente, comme certains noms d'objets diffèrent de ceux qu'on utilise dans
l'éditeur. Par exemple, "Gourde (pleine)" est simplement appelée "Eau" dans le jeu.

Le premier objet numéroté 0 ne correspond à aucun objet parce qu'on va utiliser ce code plus tard pour dire qu'une
case dans la feuille de personnage est vide.

L'outil addObject

Quand vous êtes en mode dessin...


...et que vous cliquez sur la map, ça va ajouter l'objet actuellement sélectionné au tas correspondant, ou en créer
un s'il n'y a pas d'objet dedans.

addObjectTool est dans le fichier "tools.cpp" et il est appelé de la même manière que les autres, pour profiter de
pile d'undo/redo.
Voici le code de l'outil:

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

		bool addObjectTool::init(CVec2 pos, int type)
		{
			mPos = pos / (TILE_SIZE / 2);
			mType = type;
			return true;
		}

		void addObjectTool::undo()
		{
			CObjectStack*   stack = map.findObjectsStack(mPos);

			if (stack != NULL)
			{
				size_t  size = stack->getSize();
				stack->removeObject(size - 1);

				if (size == 1)
					map.removeObjectsStack(mPos);
			}
			bridge.updateQMLImage();
		}

		void addObjectTool::redo()
		{
			CObject newObject;
			newObject.setType(mType);
			CObjectStack*    stack = map.addObjectsStack(mPos);
			stack->addObject(newObject);
			bridge.updateQMLImage();
		}
				
Notez que dans la fonction undo() on teste si le tas est vide pour le retirer complètement de la liste des tas
dans CMap.
Dans le cas de l'ajout d'un objet, addObjectsStack() crée un tas s'il n'existe pas ou utilise un tas existant.

Selection d'un tas

Quand vous sélectionnez un tas, j'ai fait une liste détaillée des objets dans la zone de sélection pour qu'on puisse
les modifier facilement.


A coté de chaque objet, le bouton "effacer" vous permet de retirer l'objet de la liste. Et à la fin, le bouton
"ajouter" sert à ajouter un nouvel objet au tas.

Remarquez que ces boutons ne fonctionnent pas tout à fait comme l'outil addObject car ils ne vous permettent pas de
défaire/refaire les modifications.
Je changerai peut être ça dans le futur si c'est nécessaire.

Je ne vais pas détailler tout le code pour ça. Ca a nécessité que je crée un nouveau type de paramètre pour les
boutons et j'ai du utiliser une astuce de Qt pour reconstruire la liste pendant qu'on gère les clics sur les
boutons pour éviter un crash.
Mais voici un aperçu rapide du code pour créer les boutons et les "combo box"

		void    CEditor::addObjParamsItems()
		{
			CVec2           pos = mSelectStart / (TILE_SIZE / 2);
			CObjectStack*   stack = map.findObjectsStack(pos);
			size_t          size = 0;

			if (stack != NULL)
			{
				for (size_t i = 0; i < stack->getSize(); ++i)
				{
					addButton(objSelGrid, "effacer", &mObjParamItems, i);
					addComboBox(objSelGrid, bridge.itemsList, &mObjParamItems, i, stack->getObject(i).getType());
				}
				size = stack->getSize();
			}
			addButton(objSelGrid, "ajouter", &mObjParamItems, size);
		}
				
Finalement, j'ai aussi du faire beaucoup de changements à "clipboard.cpp" et dans les outils couper/coller pour
gérer tous les cas de copie et de collage d'un ou plusieurs tas.
Je vous laisse regarder le code par vous-même si vous êtes intéressés par les détails.