Part 21: Objects - Part 1: In the editor

Downloads

Source code
Executable for the map editor (Windows 32bits)

The game should not work properly with the new map format. I'll fix it in the next part.
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.

Storing the objects

There are 4 places in each tile to put objects.


Each place can contain a stack with several objects.
But it would be a waste of memory and disk space if we stored a stack for every possible position in the map,
as most of the tiles won't have objects in them.
We will rather make a list of all the objects stacks in the map.

		class CMap
		{
			[...]

			std::vector<CObjectStack>   mObjectsStacks;
		};
				
We will need a few functions to handle those stacks - add a new one to the list, remove one, find if a stack
exists at a given position in the 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);
		}
				
In the map file, these stacks will be saved after the tiles and walls datas:

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

			if (handle != NULL)
			{
				// header, version, revision
				[...]

				// map size
				[...]

				// map data
				[...]

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

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

				fclose(handle);
			}
		}
				
Obviously the code is similar for the loading.

The stack class

Now let's define the CObjectStack class:

		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;
		};
				
It holds its position in the map - mPos - and the list of its objects - mObjects.
You can see functions to add an object to the list, get an object at a given index, and remove one from the list.
getSize returns the number of objects in the stack.
As for the tiles and walls, we overloaded the "=" operator for the copy tools.
An finally, there are the load and save functions.

The implementation is pretty simple to understand.

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

The object class

Now let's have a look at an object:

		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;
		};
				
It only hold a type but I made a class that looks like the ones for the tiles or the walls,
because in the future objects will also have additionnal parameters based on their types.
For example, a scroll will have a parameter with its text.

The implementation of this class should be easy to understand too:

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

Displaying the stacks in the editor

To represent the stacks in the editor, I chose to display an orange dot.


Here is the code to display this in CEditor::displayMap():

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

			if (map.isLoaded() == true)
			{
				// draw tiles
				[...]

				// draw walls
				[...]

				// draw objects
				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);
						}
					}
			}

			[...]
		}
				
We simply loop through all of the possible positions in the map, and display a dot if we found a stack
at this position.

The objects list

We now have an objects tab with the names of all the different objects in the game.


This is pretty much the same as the ones for the tiles and walls that we saw in a previous part.
The names of the objects are read from an "items.xml" database:

		<?xml version="1.0" encoding="utf-8"?>
		<items>
			<item name="[None]"/>
			<item name="Eye Of Time"/>
			<item name="Stormring"/>
			<item name="Torch"/>
			[...]
		</items>
				
The game will use a different database as some object names in the game differs frome the ones we use in the editor
I.e. "Waterskin (Full)" is simply called "WATER" in the game.

The first item numbered 0 corresponds to no object because we will use this code later to tell if a slot in the character's
sheet is empty.

The addObject tool

When you are in draw mode...


...and you click on the map, it will add the currently selected object to the corresponding stack, or create one if there is no
object in it.

addObjectTool is in the tools.cpp file and it's called in the same way as the other ones to take advantage of the undo/redo stack.
Here is the code for the tool:

		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();
		}
				
Note that in the undo() function, we check if the stack is empty to remove it completely from the stacks list in CMap.
For the case of adding an object, addObjectsStack() creates a stack if it doesn't exist, or returns an existing one.

Selection of a stack

When you select a stack, I made detailed list of the objects in the "selection" area to be able to change it easily.


Next to each object, the "delete" button allows you to remove the object from the list, and at the end, the "add" button
is to add a new object in the stack.

Note that these buttons don't really work like the addObject tool, as they do not allow you to undo/redo the modifications.
I will perhabs change that in the future if it's needed.

I won't detail all the code for that, it implied that I create a new type of parameter for the buttons and I had to use a Qt
trick to rebuild the list while the buttons click were processed to avoid a crash.
But here is a quick look at the code to create the buttons and the combo boxes

		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, "delete", &mObjParamItems, i);
					addComboBox(objSelGrid, bridge.itemsList, &mObjParamItems, i, stack->getObject(i).getType());
				}
				size = stack->getSize();
			}
			addButton(objSelGrid, "add", &mObjParamItems, size);
		}
				
Finally I also had to make a lot of changes to "clipboard.cpp" and to the cut and paste tools to handle all the cases of
copying and pasting one or several stacks. I let you look at the code by yourself if you are interested in the details.