We are going to learn how to create an in game map editor that dosen’t use tiles, but directly backdrop images that you can translate, tint, rotate, scale, etc, in different parallax layers and camera zooming. You can have several tilesets and you can of course to save / load the maps using XML files. This type of editor is used in games like Braid or Aquaria. For this tutorial we are going to use IndieLib C++ 2d engine, because its entity system is great for having graphical objects which attributes can be modified in real time. And we are going to do it with style, using graphics from lostgarden.com prototyping challenges and Aquaria game.
Some screenshots of the editor
![slide 1](https://javilop.com/files/editor-01.jpg)
![slide 2](https://javilop.com/files/editor-02.jpg)
![slide 3](https://javilop.com/files/editor-03.jpg)
![slide 4](https://javilop.com/files/editor-04.jpg)
Video of the editor
Download sourcecode
Here it is the complete sourcecode.
Windows platforms
The sourcecode is ready to be compiled under Visual C++ Express Edition 2008. In «Release» folder there is also an executable file just in case you want to try the editor directly.
Other platforms
IndieLib , the 2d engine used for this tutorial, has only a Direct3d renderer, so it is not portable yet. Nevertheless, IndieLib is an Open Source project, so if you have knowledge in OpenGl you can join the development team in order to help to port it.
Keys
TAB | Toggles between «in game» and «editing» modes. Currently «in game» mode only hides brushes and showes FPS, it should be expanded with your game logic. |
w, a, s, d | Move camera |
Key up, Key down | Zoom in / out |
R | Reset zoom |
F1 | Toogle full screen |
F2-F4 | Different resolutions |
CTRL + L | Load map. You have already some maps created in \tiless\Release\resources\maps. |
CTRL + S | Save map |
CTRL + N | Clean current map and let you choose a different tileset. You have some tilesets you can use in \tiless\Release\resources. |
Mouse wheel | Cycle through tiles |
Left Click | Drop tile |
Delete | Delete tile |
T, Y | Horizontal / Vertical Flip tile |
U, I | Increase / decrease the transparency level of the tile. |
Z, X | Send tile to front / to back |
Right Mouse Drag | Rotate tile |
L | Toggle on / off «texture tiling» |
Shift + Right Mouse Drag | If «tiling» is off, the scale in / out the tile, if «tiling» is on the tile will be repeated (tiled) |
Shift + Left Mouse Drag | Clone |
Space + Mouse Position | Modify piece tinting. This should be expanded with a proper color picker tool. |
1-9 | Select layer 0-9 |
B N M | Select parallax layers |
J | Select dark layer |
F, G, C, V | Move the tile only one pixel |
Yes, I know is not very user friendly, but that wasn’t the purpose of the tutorial.
Step 0: Introduction
First of all I want to thank Alec Holowka and Derek Yu for letting me use some of their incredible tileset that Derek drew for Aquaria. If you haven’t played Aquaria yet I don’t know what are you waiting for. I want also to thank Daniel Cook for his incredible tilesets that he offers for free to developers in his prototype challenges. Last but not least, one of the tilesets that comes with the editor (the one of the ufos) has been rendered from scratch by me but I was inspired by Oxeye’s game Harvest Massive Encounter and I want to mention it here because is a great game.
The main porpuse of this tutorial is to create a clean editor of «free tiles», with the ability of exporting / importing XML files using C++. We are not going to deal with Direct3d, OpenGl programming or with any gui system. We are going to focus directly on the editor logic, using IndieLib engine because it is great for rapid game development & prototyping and because I’m his creator 🙂 and I want people to start using it. I think this is the appropiate way of abstraction, in order to keep things easier.
Like the main feature of these editor is having graphical objects which attributes can be modified separatly, we will have lot of work done using IndieLib and its graphical entities system. IndieLib will be also helpful for the collision system (used for checking if the mouse is over a rotated / scaled piece), layering (for having different layers and parallax scrolling) and cameras (for being able just to position the pieces in the world and then to move our camera freely and being able to make zooming). Like I said, we are not going to use any gui system. Everything is going to be directly managed using keyboard shortcuts and mouse. I didn’t wanted to complicate the tutorial adding gui layouts, buttons, etc.
After making the tutorial, if you want to have a deeper insight, you can check out IndieLib source code.
In this tutorial you will learn:
- How to have graphical objects in different layers.
- How to add parallax scrolling and parallax zooming to the layers.
- How to change the attributes of the editor pieces. Each piece can be scaled, rotated, clipped, tinted, etc.
- How to export / import XML map files.
What you are supposed to already know:
- C++
- Some knowledge of IndieLib engine. Don’t worry, IndieLib engine is focused in rapid game prototyping using C++ so it is really easy to learn. It is fully documented and also has lot of tutorials. You can start here.
- Some knowledge of STL vectors.
What do you need?
- A compiler or programming IDE. I’ve used Visual C++ Express Edition for this tutorial, that is a free C++ IDE. But you can use the one of your choice, of course.
- IndieLib SDK (optional) & Direct3d SDK (needed in order to compile the tutorial): you have already IndieLib files in the tutorial zip, so you don’t really need the IndieLib SDK, but I recommend you to download it for having the engine tutorial examples and documentation. For compiling the tutorial you will need Direct3d SDK. Just start by making the first IndieLib tutorial that comes with links and explanations about both SDKs: Installing.
- Desire to learn 😀
What is the license of the sourcecode?
The sourcecode is under the «Creative Commons – Attribution 3.0 Unported». That means you can copy, distribute and transmit the work and to adapt it. But you must attribute the work (but not in any way that suggests that they endorse you or your use of the work). The manner of attribution is up to you. You can just mention me (Javier López). A backlink would be also appreciated. Remember that the sprites used are copyright of Derek, Alec and Daniel Cook.
Step 1: Explaining the behaviour of each class
CIndieLib is just a singleton class that initializes IndieLib engine. It is the same class that is used in all the IndieLib tutorials. TinyXML files are the classes that implements the XML parser we are using.
Now, lets focus on the editor classes. We are going to use only 4 classes:
- Resources class: for loading the initial tileset of brushes and the editor graphics.
- Node class: each node is a backdrop piece of the map that contains an IndieLib entity that holds all its attributes like tint, scale, rotation, etc.
- Map class: for loading and saving maps and creating, cloning and deleting nodes. A map is just a vector of nodes.
- Listener class: interface between the user and the editor using keyboard and mouse Input. It also holds the main variables, such camera position, editor modes, brushes, etc.
Step 2: Loading resources
First of all, we are going to create a class for loading / freeing all the graphical resources: all the tiles of the default tileset, the bitmap font, etc. For defining the tilesets we are going to use XML files, and we will parse them using TinyXML, a simple and small parser for XML files.
Let’s start by the header:
[c language=»++»]
#ifndef _RESOURCES_
#define _RESOURCES_
// —— Includes —–
#include "CIndieLib.h"
#include "common/TinyXml/tinyxml.h"
#include "Defines.h"
#include <vector>
using namespace std;
// ——————————————————————————–
// Resources
// ——————————————————————————–
class Resources
{
public:
Resources ();
void Free ();
bool LoadResources (char *pResourceFile);
bool LoadTileset (char *pTilesetFile);
IND_Entity2d *GetMouseEntity () { return &mMouseEntity; }
IND_Entity2d *GetFontEntity () { return &mFontEntity; }
char *GetTilesetPath () { return mTilesetPath; }
vector <SURFACE*> *GetVectorTiles () { return &mVectorTiles; }
IND_Surface *GetSurfaceById (int pId);
private:
// IndieLib pointer
CIndieLib *mI;
// Vector of the surfaces (tiles) that can be used in the editor.
vector <SURFACE*> mVectorTiles;
// Mouse pointer
IND_Surface mMouseSurface;
IND_Entity2d mMouseEntity;
// Font
IND_Font mFont;
IND_Entity2d mFontEntity;
// Current tileset location
char mTilesetPath [2048];
// —– Methods —–
bool LoadEditorElements ();
void FreeTileset ();
};
#endif // _RESOURCES_
[/c]
As you can see we are using a vector of «SURFACES». This is just a vector of all the backdrop pieces. A «SURFACE» is an structure, you can find it on «defines.h»:
[c language=»++»]
// Brush surface, used for having a vector of [surfaces, id] of elements loaded in "Resources" class. This surfaces
// can be converted later to brushes
struct structBackDropSurfaces
{
IND_Surface mSurface; // Surface
int mId; // Surface Id
};
typedef struct structBackDropSurfaces SURFACE;
[/c]
A SURFACE is an structure containing an IndieLib surface and Id number. This Id will be used later, when saving the map, for identifying the sprite we are saving. The ids are defined in the tileset XML file together with each tile. When saving a map, we also save the Id of each tile. So when we are loading the map, we know which sprites corresponds to each node. Don’t worry about this now, this will be explained better on the following steps.
And now let’s see the implementaion of each method:
[c language=»++»]
/*
======================================
Init
======================================
*/
Resources::Resources()
{
// Get IndieLib instante
mI = CIndieLib::Instance();
}
/*
======================================
End
======================================
*/
void Resources::Free()
{
// Free all the loaded tiles
FreeTileset();
// Note, the font bitmap and cursor are freed in Main.cpp calling to mI->End()
}
[/c]
Initialization and destructions methods. The constructor recieves the IndieLib singleton instance, so we can call IndieLib methods. The Free() method is used for freeing all the loaded pieces.
[c language=»++»]
/*
======================================
Load Resources
======================================
*/
bool Resources::LoadResources (char *pTilesetFile)
{
if (!LoadEditorElements()) return false;
if (!LoadTileset (pTilesetFile)) return false;
return true;
}
[/c]
This method just makes two calls to the methods that load the resources. It return false if the resources are not correctly loaded.
[c language=»++»]
/*
======================================
Return the IndieLib surface that corresponds with the ID passed as parameter. It returns -1 if the ID is not found
======================================
*/
IND_Surface *Resources::GetSurfaceById (int pId)
{
vector <SURFACE*>::iterator mIter;
// Iterate the vector
for (mIter = mVectorTiles.begin();
mIter != mVectorTiles.end();
mIter++)
{
// Check if the ID is the correct one
if ((*mIter)->mId == pId)
return &(*mIter)->mSurface;
}
// Returns error
return (IND_Surface *) -1;
}
[/c]
This method receives an ID. Then, it iterates the SURFACEs vector searching the SURFACE that has that id. If it is found, it returns a pointer to the SURFACE. We will use this method later when loading the map, for knowing which surface we have to asign to the node when parsing the XML file (each backdrop piece (each node) will have this Id specified on the map file). We will also explain this better on the following steps.
[c language=»++»]
/*
======================================
Load Resources
======================================
*/
bool Resources::LoadEditorElements ()
{
// Load the mouse pointer, it is loaded to and IndieLib surface
if (!mI->SurfaceManager->Add (&mMouseSurface, "resources\\images\\editor\\cursor.png", IND_ALPHA, IND_32)) return 0;
// Add the Mouse entity to the IndieLib Entity Manager
mI->Entity2dManager->Add (BRUSH_LAYER, &mMouseEntity);
mMouseEntity.SetSurface (&mMouseSurface);
// Create a collision bounding rectangle for the mouse. It will be used in order to be able to select
// the editor elements (by checking if there is a collision
mMouseEntity.SetBoundingRectangle ("cursor", 0, 0, 20, 20);
mMouseEntity.SetHotSpot (0.5f, 0.5f);
// Font loading
if (!mI->FontManager->Add (&mFont, "resources\\fonts\\font_small.png", "resources\\fonts\\font_small.xml", IND_ALPHA, IND_32)) return 0;
mI->Entity2dManager->Add (GUI_LAYER, &mFontEntity);
mFontEntity.SetFont (&mFont);
mFontEntity.SetLineSpacing (18);
mFontEntity.SetCharSpacing (-8);
mFontEntity.SetPosition (0, 0, 1);
mFontEntity.SetAlign (IND_LEFT);
return true;
}
[/c]
This method loads the cursor graphic and the bitmap font. These surfaces are assigned to IndieLib entities on different IndieLib layers. IndieLib layers are used for drawing sets of graphics over others. Each different layer can have a different cameras asigned. The cursor is asigned to the BRUSH_LAYER, that it the same layer that the backdrop brushes will be rendered. The font is asigned to the GUI_LAYER, that will be renderer the last, this way the editor texts will be always renderer over all the other stuff.
[c language=»++»]
/*
======================================
Parse file and load backdrop images
======================================
*/
bool Resources::LoadTileset (char *pTilesetFile)
{
// If there is a tileset already, free it
if (!mVectorTiles.empty()) FreeTileset();
strcpy (mTilesetPath, pTilesetFile);
TiXmlDocument mXmlDoc (mTilesetPath);
// Fatal error, cannot load
if (!mXmlDoc.LoadFile())
{
return false;
}
// Document root
TiXmlElement *mXResources = 0;
mXResources = mXmlDoc.FirstChildElement("resources");
if (!mXResources)
{
// No "<resources>" tag
return false;
}
// —————– Parse surfaces from the file and load them into a vector —————–
// Surfaces
TiXmlElement *mXSurfaces = 0;
mXSurfaces = mXResources->FirstChildElement("surfaces");
if (!mXSurfaces)
{
// No "<surfaces>" tag
return false;
}
TiXmlElement *mXSurface = 0;
mXSurface = mXSurfaces->FirstChildElement("surface");
if (!mXSurface)
{
// No surfaces to parse
return false;
}
// Parse all the surfaces
char mFileName [1024];
mFileName [0] = 0;
while (mXSurface)
{
SURFACE *mNewSurface = new SURFACE;
// Id
if (mXSurface->Attribute("id"))
{
mNewSurface->mId = atoi (mXSurface->Attribute("id"));
}
else
{
delete mNewSurface;
return false;
}
// Path to the image
if (mXSurface->Attribute("image"))
{
strcpy (mFileName, mXSurface->Attribute("image"));
// Load surface
if (!mI->SurfaceManager->Add (&mNewSurface->mSurface, mFileName, IND_ALPHA, IND_32)) return 0;
}
else
{
delete mNewSurface;
return false;
}
// Push the surface into the surfaces vector
mVectorTiles.push_back (mNewSurface);
// Move to the next declaration
mXSurface = mXSurface->NextSiblingElement("surface");
}
// Note: XML nodes are deleted by TiXmlDocument destructor
return true;
}
[/c]
This method parses a tileset XML file (using TinyXML), the memory used by the previous tileset is freed. In the tileset XML file are defined the tiles that the user is going to use in order to create the map. All these tiles will be loaded into IndieLib surfaces. Each tile has a different Id that it stored in a SURFACE structure together with the IND_Surface (the sprite itself). All these SURFACES are added to a vector, called mVectorTiles.
Here you have an example of an XML file (you can create your own files, for adding new tilesets to the editor, remember that you can load a tileset using CTRL+N, but the working map would be deleted):
[xml]
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!– surfaces declaration –>
<surfaces>
<surface id="0" image="resources\images\set1\g1.png" />
<surface id="1" image="resources\images\set1\p1.png" />
<surface id="2" image="resources\images\set1\p2.png" />
<surface id="3" image="resources\images\set1\g.png" />
</surfaces>
</resources>
[/xml]
Easy to understand, isn’t it? On that file you just define which tiles are going to be loaded and you give a different numeric Id to each one.
[c language=»++»]
/*
======================================
Free current tileset
======================================
*/
void Resources::FreeTileset ()
{
vector <SURFACE*>::iterator mIter;
for (mIter = mVectorTiles.begin();
mIter != mVectorTiles.end();
mIter++)
{
mI->SurfaceManager->Delete (&(*mIter)->mSurface);
delete (*mIter);
}
mVectorTiles.clear();
}
[/c]
This method frees all the memory used by the surfaces and clear all them from the vector.
Step 3: Pieces, backdrops, tiles and nodes: different names for the same thing
On this step we are going to check out the Node class. Each node is a backdrop (tile) of the map. Internally, this class contains and IndieLib entity that holds all the attributes of the graphical object. When we translate, rotate, tint or scale a node, we use the methods of the IndieLib 2d entity for changing its attributes.
The header of the class is as follows:
[c language=»++»]
#ifndef _NODE_
#define _NODE_
// —— Includes —–
#include "CIndieLib.h"
#include <vector>
// ——————————————————————————–
// Node
// ——————————————————————————–
class Node
{
public:
Node (int pX, int pY, int pZ, int pId, int pLayer, IND_Surface *pSurface);
~Node ();
IND_Entity2d *GetEntity () { return &mEntity; }
int GetLayer () { return mLayer; }
int GetSurfaceId () { return mId; }
private:
IND_Entity2d mEntity;
int mId; // Surface id
CIndieLib *mI; // Pointer to IndieLib class
int mLayer; // Layer where the node with the backdrop image is created
};
#endif // _NODE_
[/c]
Let’s see the implementation of these methods.
[c language=»++»]
/*
======================================
Init
======================================
*/
Node::Node (int pX, int pY, int pZ, int pId, int pLayer, IND_Surface *pSurface)
{
// Get IndieLib instante
mI = CIndieLib::Instance();
// Surface id
mId = pId;
// First, we have to add the entity to the manager
mI->Entity2dManager->Add (pLayer, &mEntity);
mEntity.SetSurface (pSurface); // Set the surface into the entity
mEntity.SetHotSpot (0.5f, 0.5f); // Set the hotspot (pivot point) centered
mEntity.SetPosition ((float) pX, (float) pY, pZ); // Set the position
mLayer = pLayer; // Layer where it is created
// Set a collision bounding rectangle. This is used for checking collisions between the mouse pointer
// and the entity, in order to interact with it
mEntity.SetBoundingRectangle ("editor", 0, 0, pSurface->GetWidth(), pSurface->GetHeight());
mEntity.ShowGridAreas (false);
}
/*
======================================
End
======================================
*/
Node::~Node()
{
mI->Entity2dManager->Delete (&mEntity);
}
[/c]
The constructor creates a new IndieLib entity and initializes its attributes using the parameters. We also create a bounding rectangle for checking collisions between the entity and the mouse. This way we will now if the mouse is over a piece. The destructor just deletes the entity for the Entity Manager.
Step 5: The Map
The map concept is really easy to understand. It is just a vector of nodes. The Map class is used for storing these nodes. There are also methods for loading and saving maps and creating, cloning and deleting nodes.
This is the class header:
[c language=»++»]
#ifndef _MAP_
#define _MAP_
// —— Includes —–
#include <vector>
#include "common/TinyXml/tinyxml.h"
#include "Resources.h"
#include "Node.h"
#include "CIndieLib.h"
#include "Defines.h"
using namespace std;
// ——————————————————————————–
// Map
// ——————————————————————————–
class Map
{
public:
Map ();
void Free ();
void CreateNode (int pX, int pY, int pZ, int pId, int pLayer, IND_Surface *pSurface);
void DeleteNode (Node *pNode);
void CloneNode (Node *pNode);
bool SaveMap (char *pTilesetPath);
char *GetPathToTileset ();
bool LoadMap (Resources *pResources);
vector <Node*> *GetVectorNodes () { return &mVectorNodes; }
void FreeMap ();
private:
CIndieLib *mI; // IndieLib pointer
vector <Node*> mVectorNodes; // Map nodes
// —– Private methods —–
char *OpenFileDialog (char *pFilter, bool pAction, char *pTitle);
};
#endif // _MAP_
[/c]
As you can see, there is a vector of nodes, called mVectorNodes. Everytime a piece is dropped on the editor it would be added to this vector.
Let’s explain the methods:
[c language=»++»]
/*
======================================
Init
======================================
*/
Map::Map()
{
// Get IndieLib instante
mI = CIndieLib::Instance();
}
/*
======================================
Free memory
======================================
*/
void Map::Free()
{
// Free all the map nodes
FreeMap ();
}
[/c]
The constructor just get the instance to IndieLib class. The Free() method call to another method that deletes all the nodes from the map vector.
[c language=»++»]
/*
======================================
Create node
======================================
*/
void Map::CreateNode (int pX, int pY, int pZ, int pId, int pLayer, IND_Surface *pSurface)
{
Node *mNode = new Node (pX, pY, pZ, pId, pLayer, pSurface);
mNode->GetEntity()->SetPosition ((float) pX, (float) pY, 0);
mVectorNodes.push_back (mNode);
}
[/c]
This method allocates memory for a new node, initializes the IndieLib entity of that node. After that, the node is added to the vector of nodes.
[c language=»++»]
/*
======================================
Clone node
======================================
*/
void Map::CloneNode (Node *pNode)
{
// Create a new node
Node *mNewNode = new Node ((int) pNode->GetEntity()->GetPosX(),
(int) pNode->GetEntity()->GetPosY(),
(int) pNode->GetEntity()->GetPosZ() – 1,
pNode->GetSurfaceId(),
pNode->GetLayer(),
pNode->GetEntity()->GetSurface());
// Copy attributes from the source entity (original node) to the new cloned entity (new node)
mNewNode->GetEntity()->SetAngleXYZ (0, 0, pNode->GetEntity()->GetAngleZ());
mNewNode->GetEntity()->SetScale (pNode->GetEntity()->GetScaleX(), pNode->GetEntity()->GetScaleX());
mNewNode->GetEntity()->SetTransparency (pNode->GetEntity()->GetTransparency());
mNewNode->GetEntity()->SetMirrorX (pNode->GetEntity()->GetMirrorX());
mNewNode->GetEntity()->SetMirrorY (pNode->GetEntity()->GetMirrorY());
mNewNode->GetEntity()->SetTint (pNode->GetEntity()->GetTintR(), pNode->GetEntity()->GetTintG(), pNode->GetEntity()->GetTintB());
mNewNode->GetEntity()->ToggleWrap (pNode->GetEntity()->IfWrap());
mNewNode->GetEntity()->SetRegion (0, 0, pNode->GetEntity()->GetRegionWidth(), pNode->GetEntity()->GetRegionHeight());
// Add the node to the vector
mVectorNodes.push_back (mNewNode);
}
[/c]
This method is similar to the previous one, but this time we receive a node as a parameter and we have to copy all its attributes on a new node. We will use this method for cloning the tiles.
[c language=»++»]
/*
======================================
Delete node
======================================
*/
void Map::DeleteNode (Node *pNode)
{
vector <Node*>::iterator mIter;
Node *mTemp;
for (mIter = mVectorNodes.begin();
mIter != mVectorNodes.end();
mIter++)
{
if ((*mIter) == pNode)
{
mTemp = *mIter;
mIter = mVectorNodes.erase (mIter);
delete mTemp;
break;
}
}
}
[/c]
This method finds the node passed as parameter and deletes it from the vector freeing the memory it was using.
[c language=»++»]
/*
======================================
Save the map (the vector of nodes containing each entity) to an XML file
======================================
*/
bool Map::SaveMap (char *pTilesetPath)
{
char *mPath = OpenFileDialog ("XML\0*.xml;", false, "Choose a location to save the map file");
if (!strcmp (mPath, ""))
{
delete [] mPath;
return false;
}
TiXmlDocument mXmlDoc;
// XML Declaration
TiXmlDeclaration *mDecl = new TiXmlDeclaration ("1.0", "", "");
mXmlDoc.LinkEndChild (mDecl);
// Map tag
TiXmlElement *mXRoot = new TiXmlElement ("map");
mXmlDoc.LinkEndChild (mXRoot);
// Tileset
TiXmlElement *mXTileset = new TiXmlElement ("tileset");
mXRoot->LinkEndChild (mXTileset);
char mCurrentDir[MAX_PATH];
GetCurrentDirectory (MAX_PATH, mCurrentDir); // Get current directory path
mXTileset->SetAttribute("tileset_file", pTilesetPath + strlen (mCurrentDir)); // Only gets the relative path to the tileset
// Iterate the nodes vector and write them to the XML
TiXmlElement *mXNodes = new TiXmlElement ("nodes");
mXRoot->LinkEndChild (mXNodes);
vector <Node*>::iterator mIter;
for (mIter = mVectorNodes.begin();
mIter != mVectorNodes.end();
mIter++)
{
TiXmlElement *mXNode;
mXNode = new TiXmlElement ("node");
mXNode->SetAttribute ("surface_id", (*mIter)->GetSurfaceId());
mXNode->SetAttribute ("x", (int) (*mIter)->GetEntity()->GetPosX());
mXNode->SetAttribute ("y", (int) (*mIter)->GetEntity()->GetPosY());
mXNode->SetAttribute ("z", (*mIter)->GetEntity()->GetPosZ());
mXNode->SetAttribute ("layer", (*mIter)->GetLayer());
mXNode->SetDoubleAttribute ("angle", (*mIter)->GetEntity()->GetAngleZ());
mXNode->SetDoubleAttribute ("scale", (*mIter)->GetEntity()->GetScaleX());
mXNode->SetAttribute ("trans", (*mIter)->GetEntity()->GetTransparency());
mXNode->SetAttribute ("mirror_x", (*mIter)->GetEntity()->GetMirrorX());
mXNode->SetAttribute ("mirror_y", (*mIter)->GetEntity()->GetMirrorY());
mXNode->SetAttribute ("tint_r", (*mIter)->GetEntity()->GetTintR());
mXNode->SetAttribute ("tint_g", (*mIter)->GetEntity()->GetTintG());
mXNode->SetAttribute ("tint_b", (*mIter)->GetEntity()->GetTintB());
mXNode->SetAttribute ("if_wrap", (*mIter)->GetEntity()->IfWrap());
mXNode->SetAttribute ("region_width", (*mIter)->GetEntity()->GetRegionWidth());
mXNode->SetAttribute ("region_height", (*mIter)->GetEntity()->GetRegionHeight());
mXNodes->LinkEndChild (mXNode);
}
// Save the map to an XML file
mXmlDoc.SaveFile (mPath);
// Free memory (XML nodes are deleted by TiXmlDocument destructor)
delete mPath;
return true;
}
[/c]
This class itarates through the vector of nodes and write all the attributes of each node into an XML file selected trough a Windows save dialog. It also writes into the map the relative path to the tileset that the user has used for creating the map.
[c language=»++»]
/*
======================================
Load a Map from a XML file and create a node vector
======================================
*/
bool Map::LoadMap (Resources *pResources)
{
char *mPath = OpenFileDialog ("XML\0*.xml;", true, "Choose a map file");
if (!strcmp (mPath, ""))
{
delete [] mPath;
return true;
}
// Free current map
FreeMap();
// Initializa XML doc
TiXmlDocument mXmlDoc (mPath);
// Fatal error, cannot load
if (!mXmlDoc.LoadFile())
{
return false;
}
// Document root
TiXmlElement *mXMap = 0;
mXMap = mXmlDoc.FirstChildElement("map");
if (!mXMap)
{
// No "<map>" tag
return false;
}
// —————– Parse tileset —————–
// Tileset
TiXmlElement *mXTileset = 0;
mXTileset = mXMap->FirstChildElement("tileset");
if (!mXTileset)
{
// No "<tileset>" tag
return false;
}
// Id
if (mXTileset->Attribute("tileset_file"))
{
char mPath [MAX_PATH];
GetCurrentDirectory (MAX_PATH, mPath); // Get the path to the current directory
strcat (mPath, (char *) mXTileset->Attribute("tileset_file")); // Add the name of the tileset to the path
if (!pResources->LoadTileset (mPath)) return 0; // It tries to load the tiles
}
// —————– Parse nodes —————–
// Nodes
TiXmlElement *mXNodes = 0;
mXNodes = mXMap->FirstChildElement("nodes");
if (!mXNodes)
{
// No "<nodes>" tag
return false;
}
TiXmlElement *mXNode = 0;
mXNode = mXNodes->FirstChildElement("node");
if (!mXNode)
{
// No nodes to parse
return false;
}
// Parse all the nodes
while (mXNode)
{
// Parameters to parse
int mSurfaceId, mX, mY, mZ, mLayer, mTrans, mTintR, mTintG, mTintB, mRegionWidth, mRegionHeight;
bool mMirrorX, mMirrorY, mIfWrap;
float mAngle, mScale;
// Surface Id
if (mXNode->Attribute("surface_id"))
{
mSurfaceId = atoi (mXNode->Attribute("surface_id"));
}
else
{
return false;
}
// Pos X
if (mXNode->Attribute("x"))
{
mX = atoi (mXNode->Attribute("x"));
}
else
{
return false;
}
// Pos Y
if (mXNode->Attribute("y"))
{
mY = atoi (mXNode->Attribute("y"));
}
else
{
return false;
}
// Pos Z
if (mXNode->Attribute("z"))
{
mZ = atoi (mXNode->Attribute("z"));
}
else
{
return false;
}
// Layer
if (mXNode->Attribute("layer"))
{
mLayer = atoi (mXNode->Attribute("layer"));
}
else
{
return false;
}
// Angle
if (mXNode->Attribute("angle"))
{
mAngle = (float) atof (mXNode->Attribute("angle"));
}
else
{
return false;
}
// Scale
if (mXNode->Attribute("scale"))
{
mScale = (float) atof (mXNode->Attribute("scale"));
}
else
{
return false;
}
// Transparency
if (mXNode->Attribute("trans"))
{
mTrans = atoi (mXNode->Attribute("trans"));
}
else
{
return false;
}
// Mirror x
if (mXNode->Attribute("mirror_x"))
{
mMirrorX = (bool) atoi (mXNode->Attribute("mirror_x"));
}
else
{
return false;
}
// Mirror y
if (mXNode->Attribute("mirror_y"))
{
mMirrorY = atoi (mXNode->Attribute("mirror_y"));
}
else
{
return false;
}
// Tint r
if (mXNode->Attribute("tint_r"))
{
mTintR = atoi (mXNode->Attribute("tint_r"));
}
else
{
return false;
}
// Tint g
if (mXNode->Attribute("tint_g"))
{
mTintG = atoi (mXNode->Attribute("tint_g"));
}
else
{
return false;
}
// Tint b
if (mXNode->Attribute("tint_b"))
{
mTintB = atoi (mXNode->Attribute("tint_b"));
}
else
{
return false;
}
// If wrap
if (mXNode->Attribute("if_wrap"))
{
mIfWrap = atoi (mXNode->Attribute("if_wrap"));
}
else
{
return false;
}
// Region width
if (mXNode->Attribute("region_width"))
{
mRegionWidth = atoi (mXNode->Attribute("region_width"));
}
else
{
return false;
}
// Region height
if (mXNode->Attribute("region_height"))
{
mRegionHeight = atoi (mXNode->Attribute("region_height"));
}
else
{
return false;
}
// Get the surface of the tile already loaded in Resources class by passing the Id
IND_Surface *mSurface = pResources->GetSurfaceById(mSurfaceId);
if (mSurface == (IND_Surface *) -1) return 0;
// Node creation, we have to set the surface later
Node *mNewNode = new Node (mX, mY, mZ, mSurfaceId, mLayer, mSurface);
// Set new node attributes
mNewNode->GetEntity()->SetAngleXYZ (0, 0, mAngle);
mNewNode->GetEntity()->SetScale (mScale, mScale);
mNewNode->GetEntity()->SetTransparency (mTrans);
mNewNode->GetEntity()->SetMirrorX (mMirrorX);
mNewNode->GetEntity()->SetMirrorY (mMirrorY);
mNewNode->GetEntity()->SetTint (mTintR, mTintG, mTintB);
mNewNode->GetEntity()->ToggleWrap (mIfWrap);
mNewNode->GetEntity()->SetRegion (0, 0, mRegionWidth, mRegionHeight);
// Push the node into the nodes vector
mVectorNodes.push_back (mNewNode);
// Move to the next declaration
mXNode = mXNode->NextSiblingElement("node");
}
// Free memory, XML nodes are deleted by TiXmlDocument destructor
delete mPath;
return true;
}
[/c]
This method parses an XML map file, selected trough a Windows open dialog, creating all the nodes of the map. Do you remember the LoadTileset() method that we have already seem? It is used here in order to load the map tiles.
Each map node has a surface_id attribute on the map file that is the same Id that we specified in the tileset XML file. This way we can set the correct sprite to each node.
[c language=»++»]
/*
======================================
Gets the path to the tileset file that we are going to load
======================================
*/
char *Map::GetPathToTileset ()
{
char *mPath = OpenFileDialog ("XML\0*.xml;", true, "Choose a Tileset file");
if (!strcmp (mPath, ""))
{
delete [] mPath;
return 0;
}
return mPath;
}
[/c]
This method gets the path to the tileset file using a Windows open file dialog.
[c language=»++»]
/*
======================================
Delete all the map nodes
======================================
*/
void Map::FreeMap ()
{
vector <Node*>::iterator mIter;
for (mIter = mVectorNodes.begin();
mIter != mVectorNodes.end();
++mIter)
{
mI->Entity2dManager->Delete ((*mIter)->GetEntity());
delete (*mIter);
}
mVectorNodes.clear();
}
[/c]
This method deletes all the map nodes, cleaning the map.
[c language=»++»]
/*
==================
Open a Windows open / save dialog
pAction = if true => open dialog if false => save dialog
==================
*/
char *Map::OpenFileDialog (char *pFilter, bool pAction, char *pTitle)
{
// If we are in full screen mode, we have to change to screen mode
bool mKeepFullScreen = false;
if (mI->Window->IsFullScreen())
{
mI->Render->ToggleFullScreen ();
mKeepFullScreen = true;
}
OPENFILENAME mOpenFileName;
char szFile[MAX_PATH];
char mCurrentDir[MAX_PATH];
szFile[0] = 0;
GetCurrentDirectory (MAX_PATH, mCurrentDir);
mOpenFileName.lStructSize = sizeof (OPENFILENAME);
mOpenFileName.hwndOwner = mI->Window->GetWnd();
mOpenFileName.lpstrFilter = pFilter;
mOpenFileName.lpstrCustomFilter = NULL;
mOpenFileName.nMaxCustFilter = 0;
mOpenFileName.nFilterIndex = 0;
mOpenFileName.lpstrFile = szFile;
mOpenFileName.nMaxFile = sizeof( szFile );
mOpenFileName.lpstrFileTitle = 0;
mOpenFileName.nMaxFileTitle = 0;
mOpenFileName.lpstrInitialDir = mCurrentDir;
mOpenFileName.lpstrTitle = pTitle;
mOpenFileName.nFileOffset = 0;
mOpenFileName.nFileExtension = 0;
mOpenFileName.lpstrDefExt = NULL;
mOpenFileName.lCustData = 0;
mOpenFileName.lpfnHook = NULL;
mOpenFileName.lpTemplateName = NULL;
mOpenFileName.Flags = OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; // Keep the relative part intact.
char *mResult = new char [MAX_PATH];
if (pAction)
{
if (GetOpenFileName (&mOpenFileName))
{
if (mKeepFullScreen) mI->Render->ToggleFullScreen ();
strcpy (mResult, szFile);
return mResult;
}
else
return "";
}
else
{
if (GetSaveFileName (&mOpenFileName))
{
if (mKeepFullScreen) mI->Render->ToggleFullScreen ();
strcpy (mResult, szFile);
return mResult;
}
else
return "";
}
}
[/c]
This method is used for showing opening and saving dialogs that are useful for selecting the path to a file.
Step 4: Listener class: Input and editor flags
Listener class is the interface between the user and the editor using keyboard and mouse Input. It also holds the editor states and flags.
[c language=»++»]
#ifndef _LISTENER_
#define _LISTENER_
// —– Includes —–
#include "CIndieLib.h"
#include "Resources.h"
#include "Map.h"
// ——————————————————————————–
// Listener
// ——————————————————————————–
class Listener
{
public:
Listener (Resources *pResources, Map *mMap);
void Free ();
void Listen ();
IND_Camera2d *GetCameraLayers () { return mCameraLayers; }
IND_Camera2d *GetCameraB () { return mCameraB; }
IND_Camera2d *GetCameraN () { return mCameraN; }
IND_Camera2d *GetCameraM () { return mCameraM; }
IND_Camera2d *GetCameraGui () { return mCameraGui; }
int GetCurrentLayer () { return mLayer; }
bool GetMode () { return mMode; }
private:
// Classes
CIndieLib *mI;
Resources *mResources;
Map *mMap;
// Time measurement
float mDelta;
// Mouse position
int mMouseX, mMouseY;
// Screen dimensions
int mScreenWidth, mScreenHeight;
// Editing mode (true = editing, false = in game)
bool mMode;
// Current working layer (there are 13 layers) => b, n, m, 1-9, j (coded from 0 to 12; layers 13 and 14
// are for the Brushes and the Gui Text
int mLayer;
// Cameras (one camera per each parallax layer)
float mCameraZoomB, mCameraZoomN, mCameraZoomM, mCameraZoomLayers;
IND_Camera2d *mCameraB;
IND_Camera2d *mCameraN;
IND_Camera2d *mCameraM;
IND_Camera2d *mCameraLayers;
float mCameraBX, mCameraBY;
float mCameraNX, mCameraNY;
float mCameraMX, mCameraMY;
float mCameraLayersX, mCameraLayersY;
int mCameraSpeedB, mCameraSpeedN, mCameraSpeedM, mCameraSpeedLayers;
// Camera used for showing the menu options, fps, etc (this camera dosen’t change)
IND_Camera2d *mCameraGui;
// Editor texts
char mText [1024];
char mModeName [64];
char mLayerName [64];
// Backdrops
BRUSH *mBackDropBrushes; // Array of entities, one for each brush, for the editing mode
int mNumBackDropBrushes; // Number of backdrops images that we can use to draw as brushes
// Backdrop brush
float mPosBrushX, mPosBrushY; // Position of the brush
int mCurrentBackDropBrush; // Current backdrop image that could be dropped (brush image)
// Backdrop node that is currently selected (the mouse is over it)
Node *mBackDropNodeOver;
// Backdrop editing flags
bool mIsRotatingBackDrop;
bool mIsTranslatingBackDrop;
bool mIsScalingBackDrop;
bool mIsTintingBackDrop;
// —– Private methods —–
void ListenCommon ();
void ListenEditing ();
void ListenInGame ();
void ChangeLayer (int pNumLayer, char *pLayerName);
Node *CheckMouseCollisions (int pLayer);
void ListenHoverBackDrop ();
void ListenBackDropBrush ();
void CreateBackDropBrushes ();
void DeleteBackDropBrushes ();
void ResetZoom ();
bool ResetScreen (int pScreenWidth, int pScreenHeight);
};
#endif // _LISTENER_
[/c]
As you can see there are quite a lot of flags to be considered, like flags for knowing if a backdrop is being rotated, scaled, etc. There is an important array called mBackDropBrushes, it holds all the graphical pieces that could be dropped, I mean, the tiles that appears on the mouse position and could be changed using the mouse wheel. This array is similiar to the vector of SURFACEs structures that we used on Resources class but the BRUSH structure dosen’t hold an IND_Surface, it holds an IndieLib entity:
[c language=»++»]
// Brush entity, used for each of the brushes for droping backdrops to the screen
struct structBackDropBrushes
{
IND_Entity2d mEntity; // Entity
int mId; // Brush Id
};
typedef struct structBackDropBrushes BRUSH;
[/c]
We use a simple array because we know how many tiles are loaded from the tileset, so we don’t need a vector object. The IndieLib entities of the array of brushes get the tile sprites by getting instances of the IND_Surface objects of the SURFACE vector in Resources class. This is made in a method called CreateBackDropBrushes() that we will explain later and that is called everythime a tilemap file is loaded.
The numerical Id is the same Id we have used on the map and tileset files: it identifies the sprite. When a piece is droped into the editor and stored as a node in the map, the node will get that Id. And like we have already seen, that Id will be finally stored into the map file when saving.
Let’s check out the implementation of this class:
[c language=»++»]
/*
======================================
Init
======================================
*/
Listener::Listener(Resources *pResources, Map *pMap)
{
// Get IndieLib instante
mI = CIndieLib::Instance();
// Classes
mResources = pResources;
mMap = pMap;
// Screen dimensions and fullscreen flag
mScreenWidth = mI->Window->GetWidth();
mScreenHeight = mI->Window->GetHeight();
// Backdrop
mBackDropNodeOver = 0; // The mouse cursor is over this backdrop
mIsRotatingBackDrop = false; // Flag used when rotating a backdrop
mIsTranslatingBackDrop = false; // Flag used when translating a backdrop
mIsScalingBackDrop = false; // Flag used when scaling a backdrop
mIsTintingBackDrop = false; // Flag used when tinting a backdrop
// Text showed
mText [0] = 0;
// Init mode = editing
mMode = true; // Editing / In game
strcpy (mModeName, "Editing mode | ");
// All the backdrop objects in this editor are on one of 13 layers.
// The first 3 layers are parallax layers, mapped to "B", "N" and "M" keys in they
// keyboard.
//
// The following nine layers are mapped to the 1-9 keys. Layers 8-9 are the only layers that
// go over active entities, like the main player!
//
// The final layer is the "dark layer", layer mapped to "J". Is another layer that you
// could use in order to draw semi-translucent dark sprites in order to simulate darkness.
//
// For coding all this in the editor we only need a variable called mLayer that stores
// the number of the layer we are working on. Later, when dropping a piece, we just have
// to check in which layer we are working and to add the IndieLib entity of the dropped node
// to the proper layer user IndieLib Entity2dManger class, that let you choose in which layer
// you want to add an entity. Later, in the main loop, we just render each layer, starting
// from 0 to the last layer, using Entity2dManger::RenderEntities2d() method.
//
// On the following list you can see in the first colum the number for coding the layer
// and in the second colum its real name. Don’t make a mistake thinking for example
// than the layer named "1" is coded in mLayer variable as 1:
//
// 0 = b
// 1 = n
// 2 = m
// 3-11 = 1-9
// 12 = j
mLayer = 3; // (Layer 1)
strcpy (mLayerName, "Layer 1 (Tiled surfaces)");
// Camera initialization (middle of the screen area)
mCameraLayersX = mCameraBX = mCameraNX = mCameraMX = (float) mI->Window->GetWidth () / 2;
mCameraLayersY = mCameraBY = mCameraNY = mCameraMY = (float) mI->Window->GetHeight () / 2;
// We use 4 cameras, one for each parallax scroll and one for the rest of layers
mCameraB = new IND_Camera2d ((int) mCameraLayersX, (int) mCameraLayersY);
mCameraN = new IND_Camera2d ((int) mCameraLayersX, (int) mCameraLayersY);
mCameraM = new IND_Camera2d ((int) mCameraLayersX, (int) mCameraLayersY);
mCameraLayers = new IND_Camera2d ((int) mCameraLayersX, (int) mCameraLayersY);
mCameraGui = new IND_Camera2d ((int) mCameraLayersX, (int) mCameraLayersY);
// Set the camera zoom
ResetZoom ();
// Create the backdrop entities that are used to show on the screen which backdrop
// sprite is currently as a brush and ready to be dropped.
CreateBackDropBrushes ();
}
[/c]
The constructor initializes all the flags and sets the initial values for the camera position. Please, read the long comment about layers. Talking about layers, and to sum up, we use IndieLib layers using Entity2dManager for adding the dropped node to the current working layer. On the main loop we render first the layers that we want to appear under the rest using Entity2dManager::RenderEntities2d().
[c language=»++»]
/*
======================================
Free memory
======================================
*/
void Listener::Free ()
{
// Free cameras
delete mCameraB;
delete mCameraN;
delete mCameraM;
delete mCameraLayers;
delete mCameraGui;
// Delete Brushes
DeleteBackDropBrushes ();
}
[/c]
This method just frees the cameras and delete all the brushes.
[c language=»++»]
/*
======================================
Get the input from keyboard and mouse and calls to the appropiate methods. This class takes care of
every action that the user calls.
======================================
*/
void Listener::Listen()
{
// Input that is the same for "editing" and "in game" modes
ListenCommon ();
// Get input for the appropiate mode
if (mMode)
// Input for "editing" mode
ListenEditing();
else
// Input for "in game" mode
ListenInGame();
}
[/c]
This is the main method of Listener class. It just call three methods that get the input from keyboard and mouse, and take care of the editor actions: ListenCommon(), ListenEditing() and ListenInGame().
[c language=»++»]
/*
======================================
Getting input for actions that are the same for "editing mode" and "in game mode"
======================================
*/
void Listener::ListenCommon()
{
// ——————– Text ——————–
// Update info-text
strcpy (mText, mModeName);
// ——————– Time measurement ——————–
mDelta = mI->Render->GetFrameTime() / 1000.0f;
// ——————– Get mouse position ——————–
mMouseX = mI->Input->GetMouseX();
mMouseY = mI->Input->GetMouseY();
// ——————– Update IndieLib cameras ——————–
// Update the position of the cameras
mCameraB->SetPosition ((int) mCameraBX, (int) mCameraBY);
mCameraN->SetPosition ((int) mCameraNX, (int) mCameraNY);
mCameraM->SetPosition ((int) mCameraMX, (int) mCameraMY);
mCameraLayers->SetPosition ((int) mCameraLayersX, (int) mCameraLayersY);
// Update the zoom factor of the cameras
mCameraB->SetZoom (mCameraZoomB);
mCameraN->SetZoom (mCameraZoomN);
mCameraM->SetZoom (mCameraZoomM);
mCameraLayers->SetZoom (mCameraZoomLayers);
// ——————– Switch between "editing" and "in game" mode when pressing tab ——————–
if (mI->Input->OnKeyRelease (IND_TAB))
{
if (mMode)
{
strcpy (mModeName, "In game mode"); // Set text
if (mBackDropNodeOver) mBackDropNodeOver->GetEntity()->ShowGridAreas (false);
mMode = false;
}
else
{
strcpy (mModeName, "Editing mode | "); // Set text
mMode = true;
}
}
// ——————– Save Map ——————–
if (mI->Input->IsKeyPressed (IND_LCTRL) && mI->Input->OnKeyPress (IND_S))
mMap->SaveMap (mResources->GetTilesetPath());
// ——————– New Map ——————–
if (mI->Input->IsKeyPressed (IND_LCTRL) && mI->Input->OnKeyPress (IND_N))
{
char *mTilesetPath = mMap->GetPathToTileset ();
if (mTilesetPath)
{
// We try to load a new tileset
if (mResources->LoadTileset (mTilesetPath))
{
DeleteBackDropBrushes(); // Delete old brushes. TODO-> Check is the tileset is already loaded
CreateBackDropBrushes(); // Create the new brushes using the new loaded tileset
mMap->FreeMap(); // Free old map
}
else
exit (0); // Just exit if we can’t load it: TODO -> Show a message
}
}
// ——————– Load Map ——————–
if (mI->Input->IsKeyPressed (IND_LCTRL) && mI->Input->OnKeyPress (IND_L))
{
// Try to load the map
if (mMap->LoadMap (mResources))
{
DeleteBackDropBrushes(); // Delete old brushes. TODO-> Check is the tileset is already loaded
CreateBackDropBrushes(); // Create the new brushes using the new loaded tileset
}
else
exit (0); // Just exit if we can’t load it: TODO -> Show a message
}
// ——————– Camera position ——————–
// The camera movement is time dependent, each camera has a different speed
if (mI->Input->IsKeyPressed (IND_A))
{
mCameraBX -= CAMERA_SPEED_B * mDelta;
mCameraNX -= CAMERA_SPEED_N * mDelta;
mCameraMX -= CAMERA_SPEED_M * mDelta;
mCameraLayersX -= CAMERA_SPEED_LAYERS * mDelta;
}
if (mI->Input->IsKeyPressed (IND_D))
{
mCameraBX += CAMERA_SPEED_B * mDelta;
mCameraNX += CAMERA_SPEED_N * mDelta;
mCameraMX += CAMERA_SPEED_M * mDelta;
mCameraLayersX += CAMERA_SPEED_LAYERS * mDelta;
}
if (mI->Input->IsKeyPressed (IND_W))
{
mCameraBY -= CAMERA_SPEED_B * mDelta;
mCameraNY -= CAMERA_SPEED_N * mDelta;
mCameraMY -= CAMERA_SPEED_M * mDelta;
mCameraLayersY -= CAMERA_SPEED_LAYERS * mDelta;
}
if (mI->Input->IsKeyPressed (IND_S))
{
mCameraBY += CAMERA_SPEED_B * mDelta;
mCameraNY += CAMERA_SPEED_N * mDelta;
mCameraMY += CAMERA_SPEED_M * mDelta;
mCameraLayersY += CAMERA_SPEED_LAYERS * mDelta;
}
// ——————– Camera zoom ——————–
// Zoom in
if (mI->Input->IsKeyPressed (IND_KEYUP))
{
mCameraZoomB += (DIST_CAMERA_B * mDelta) / 1000;
mCameraZoomN += (DIST_CAMERA_N * mDelta) / 1000;
mCameraZoomM += (DIST_CAMERA_M * mDelta) / 1000;
mCameraZoomLayers += (DIST_CAMERA_LAYERS * mDelta) / 1000;
}
// Zoom out
if (mCameraZoomB > 0) // Avoid too much zoom-out
{
if (mI->Input->IsKeyPressed (IND_KEYDOWN))
{
mCameraZoomB -= (DIST_CAMERA_B * mDelta) / 1000;
mCameraZoomN -= (DIST_CAMERA_N * mDelta) / 1000;
mCameraZoomM -= (DIST_CAMERA_M * mDelta) / 1000;
mCameraZoomLayers -= (DIST_CAMERA_LAYERS * mDelta) / 1000;
}
}
// Reset zoom
if (mI->Input->OnKeyPress (IND_R)) ResetZoom ();
// ——————– Toggle full screen——————–
if (mI->Input->OnKeyPress (IND_F1))
{
// Change to window mode and set Vsync off
if (mI->Window->IsFullScreen())
mI->Render->Reset ("", mScreenWidth, mScreenHeight, 32, 0, 0);
// Change to full screen and set Vsync on
else
mI->Render->Reset ("", mScreenWidth, mScreenHeight, 32, 1, 1);
}
// ——————– Change screen resolution ——————–
if (mI->Input->OnKeyPress (IND_F2))
if (!ResetScreen (640, 480)) exit (0);
if (mI->Input->OnKeyPress (IND_F3))
if (!ResetScreen (800, 600)) exit (0);
if (mI->Input->OnKeyPress (IND_F4))
if (!ResetScreen (1024, 768)) exit (0);
if (mI->Input->OnKeyPress (IND_F5))
if (!ResetScreen (1440, 900)) exit (0);
}
[/c]
In this method we check if the user is making some actions than can be used both on «in game» mode and on «editing» mode. Read the sourcecode comments in order to get a brief description of all these actions. Notice that when pressing «CTRL + N» and Open Windows Dialog appears asking for a tileset XML file. We try to load it calling to Resources::LoadTileset(). After that, we have to free the existing brushes (DeleteBackDropBrushes()) and then to set the new ones getting the surface instances from the loaded tileset (CreateBackDropBrushes()). Finally we can free all the map nodes. A similar action is taken when loading a map by when pressing «CTRL + L», but this time calling to Map::LoadMap().
[c language=»++»]
/*
======================================
Input for "editing" mode
======================================
*/
void Listener::ListenEditing()
{
// ——————– Text ————-
strcat (mText, mLayerName);
mResources->GetFontEntity ()->SetText (mText);
// ——————– Brush position ————-
// Here we calculate the position of the brush. We first translate it to the origin (the screen center)
// then we scale it using the zooming factor. Finally we translate it back to the camera position
// You should use the zoom and coordiante for the specific layer you are editing // (Fixed by Metadon)
switch (mLayer)
{
case 0:
mPosBrushX = ( ( mMouseX – (mScreenWidth / 2) ) * ( 1 / (mCameraB->GetZoom()) ) ) + mCameraB->GetPosX();
mPosBrushY = ( ( mMouseY – (mScreenHeight / 2) ) * ( 1 / (mCameraB->GetZoom()) ) ) + mCameraB->GetPosY();
break;
case 1:
mPosBrushX = ( ( mMouseX – (mScreenWidth / 2) ) * ( 1 / (mCameraN->GetZoom()) ) ) + mCameraN->GetPosX();
mPosBrushY = ( ( mMouseY – (mScreenHeight / 2) ) * ( 1 / (mCameraN->GetZoom()) ) ) + mCameraN->GetPosY();
break;
case 2:
mPosBrushX = ( ( mMouseX – (mScreenWidth / 2) ) * ( 1 / (mCameraM->GetZoom()) ) ) + mCameraM->GetPosX();
mPosBrushY = ( ( mMouseY – (mScreenHeight / 2) ) * ( 1 / (mCameraM->GetZoom()) ) ) + mCameraM->GetPosY();
break;
default:
mPosBrushX = ( ( mMouseX – (mScreenWidth / 2) ) * ( 1 / mCameraZoomLayers ) ) + mCameraLayersX;
mPosBrushY = ( ( mMouseY – (mScreenHeight / 2) ) * ( 1 / mCameraZoomLayers ) ) + mCameraLayersY;
break;
}
// Sets mouse position and scale in the brush position
mResources->GetMouseEntity()->SetPosition (mPosBrushX, mPosBrushY, 1);
mResources->GetMouseEntity()->SetScale (1 / mCameraZoomLayers, 1 / mCameraZoomLayers);
// ——————– Check collisions between the mouse an the backdrop images of the current layer ——————–
// We are currently making a backdrop transformation
if (mIsTranslatingBackDrop || mIsRotatingBackDrop || mIsScalingBackDrop || mIsTintingBackDrop)
{
ListenHoverBackDrop ();
}
// Only check collisions if we are not already making a transformation over a backdrop
else
{
// Check a collision between the mouse a node
Node *mNewBackDropNodeOver = CheckMouseCollisions (mLayer);
// The cursor is over a backdrop image
if (mNewBackDropNodeOver)
{
// New backdrop
if (mNewBackDropNodeOver != mBackDropNodeOver)
{
if (mBackDropNodeOver)
mBackDropNodeOver->GetEntity()->ShowGridAreas (false);
mBackDropNodeOver = mNewBackDropNodeOver;
}
// Show red rectangle
mBackDropNodeOver->GetEntity()->ShowGridAreas (true);
// Hide backdrop brush
mBackDropBrushes [mCurrentBackDropBrush].mEntity.SetShow (false);
// Listen for possible changes that the selected backdrop can suffer
ListenHoverBackDrop ();
}
// The cursor is not over any backdrop (brush mode por droping images)
else
{
if (mBackDropNodeOver)
mBackDropNodeOver->GetEntity()->ShowGridAreas (false);
// Show backdrop brush
mBackDropBrushes [mCurrentBackDropBrush].mEntity.SetShow (true);
// Listen for switching between backdrop images on the brush, drop them in the map, etc
ListenBackDropBrush ();
}
}
// ——————– Switch between layers ——————–
// Parallax layer (b, n, m)
if (mI->Input->OnKeyPress (IND_B)) ChangeLayer (0, "Layer B (1st Parallax Layer)");
if (mI->Input->OnKeyPress (IND_N) && !mI->Input->IsKeyPressed (IND_LCTRL)) ChangeLayer (1, "Layer N (2nd Parallax Layer)");
if (mI->Input->OnKeyPress (IND_M)) ChangeLayer (2, "Layer M (3rd Parallax Layer)");
// Layers 1-9
if (mI->Input->OnKeyPress (IND_1)) ChangeLayer (3, "Layer 1 (Tiled surfaces)");
if (mI->Input->OnKeyPress (IND_2)) ChangeLayer (4, "Layer 2");
if (mI->Input->OnKeyPress (IND_3)) ChangeLayer (5, "Layer 3");
if (mI->Input->OnKeyPress (IND_4)) ChangeLayer (6, "Layer 4");
if (mI->Input->OnKeyPress (IND_5)) ChangeLayer (7, "Layer 5");
if (mI->Input->OnKeyPress (IND_6)) ChangeLayer (8, "Layer 6");
if (mI->Input->OnKeyPress (IND_7)) ChangeLayer (9, "Layer 7");
if (mI->Input->OnKeyPress (IND_8)) ChangeLayer (10, "Layer 8");
if (mI->Input->OnKeyPress (IND_9)) ChangeLayer (11, "Layer 9");
// Dark layer (j)
if (mI->Input->OnKeyPress (IND_J)) ChangeLayer (12, "Layer J (Dark Layer)");
}
[/c]
This method takes cares of the actions that happens when we are on «editing mode».
First, we need to know where is the brush. Before updating the position of the IndieLib brush entity, we have to calculate the position of the brush in the map. This is a tricky part, because we can not just draw the brush directly in the mouse coordinates. We have to translate mouse coordianates to map coordinates. For that, we have to take in count these parameters: camera translation, camera zooming, layer, screen width and screen height. In order to accomplish this task we can follow the typical transformation approach. We first translate the brush to the origin (the screen center), then we scale it using the zooming factor (the one of the working layer), and finally we translate it back to the camera position.
Notice that when we are in «editing mode» there are two possible states. On the one hand, if the mouse is over a backdrop piece, we have to call to ListenHoverBackDrop() in order to take care of the actions applied to the piece, on the other hand, if the mouse is not over any piece, we just call to ListenBackDropBrush() method, that let’s the user change the piece attributes (rotation, scaling, etc). So, we need to call to CheckMouseCollisions() method in order to know if the mouse is over a piece or not.
There are some important details:
- If a piece is being rotated, translated, scaled or tinted we directly have to call to ListenHoverBackDrop () method. This is because sometimes, when dragging the mouse, it can go out of the backdrop collision area, but we still want to keep editing the backdrop.
- We have used the IndieLib methods ShowCollisionAreas() and ShowGridAreas() for showing the backdrops that are on the same layer and the backdrop that is currently being modified. These methods works together with the methods Render::RenderGridAreas() and Render::RenderCollisionAreas() that we use on Main.cpp.
After managing the brush behaviour, we have some lines that let the user switch between layers.
[c language=»++»]
/*
======================================
Listen for possible changes that the selected backdrop (the one where the cursos is over) can suffer
======================================
*/
void Listener::ListenHoverBackDrop ()
{
static int mMouseClickX;
static int mMouseClickY;
IND_Entity2d *mBackDropOver = mBackDropNodeOver->GetEntity();
// ——————– Translate the selected backdrop image ——————–
static float mInitialPosX, mInitialPosY;
float mNewPosX, mNewPosY;
if (!mIsRotatingBackDrop && !mIsScalingBackDrop)
{
if (mI->Input->OnMouseButtonPress(IND_MBUTTON_LEFT))
{
mMouseClickX = (int) mPosBrushX;
mMouseClickY = (int) mPosBrushY;
mInitialPosX = mBackDropOver->GetPosX();
mInitialPosY = mBackDropOver->GetPosY();
}
if (mI->Input->IsMouseButtonPressed (IND_MBUTTON_LEFT))
{
mNewPosX = mInitialPosX + (mPosBrushX – mMouseClickX);
mNewPosY = mInitialPosY + (mPosBrushY – mMouseClickY);
mBackDropOver->SetPosition (mNewPosX,
mNewPosY,
mBackDropOver->GetPosZ());
mIsTranslatingBackDrop = true;
}
if (mI->Input->OnMouseButtonRelease(IND_MBUTTON_LEFT))
{
mIsTranslatingBackDrop = false;
}
}
// ——————– Translate pixel by pixel ——————–
if (mI->Input->OnKeyPress (IND_F))
{
mBackDropOver->SetPosition (mBackDropOver->GetPosX() – 1,
mBackDropOver->GetPosY(),
mBackDropOver->GetPosZ());
}
if (mI->Input->OnKeyPress (IND_G))
{
mBackDropOver->SetPosition (mBackDropOver->GetPosX() + 1,
mBackDropOver->GetPosY(),
mBackDropOver->GetPosZ());
}
if (mI->Input->OnKeyPress (IND_C))
{
mBackDropOver->SetPosition (mBackDropOver->GetPosX(),
mBackDropOver->GetPosY() – 1,
mBackDropOver->GetPosZ());
}
if (mI->Input->OnKeyPress (IND_V))
{
mBackDropOver->SetPosition (mBackDropOver->GetPosX(),
mBackDropOver->GetPosY() + 1,
mBackDropOver->GetPosZ());
}
// ——————– Scale the selected backdrop image ——————–
static float mInitialScale;
float mNewScale;
static int mInitialRegionX, mInitialRegionY;
int mNewRegionX, mNewRegionY;
if (!mIsRotatingBackDrop && !mIsTranslatingBackDrop)
{
if (mI->Input->OnMouseButtonPress (IND_MBUTTON_RIGHT) && mI->Input->IsKeyPressed (IND_LSHIFT))
{
mMouseClickX = (int) mPosBrushX;
if (!mBackDropOver->IfWrap())
{
mInitialScale = mBackDropOver->GetScaleX();
}
else
{
mInitialRegionX = mBackDropOver->GetRegionWidth();
mInitialRegionY = mBackDropOver->GetRegionHeight();
}
}
if (mI->Input->IsMouseButtonPressed (IND_MBUTTON_RIGHT) && mI->Input->IsKeyPressed(IND_LSHIFT))
{
mIsScalingBackDrop = true;
if (!mBackDropOver->IfWrap())
{
mNewScale = mInitialScale + ((mPosBrushX – mMouseClickX) / 1000);
if (mNewScale < 0.05f) mNewScale = 0.1f;
mBackDropOver->SetScale (mNewScale, mNewScale);
}
else
{
mNewRegionX = mInitialRegionX + ((int) mPosBrushX – mMouseClickX);
mNewRegionY = mInitialRegionY + ((int) mPosBrushX – mMouseClickX);
mBackDropOver->SetRegion (0,
0,
(int) mNewRegionX,
(int) mNewRegionY);
}
}
if (mI->Input->OnMouseButtonRelease (IND_MBUTTON_RIGHT))
{
mIsScalingBackDrop = false;
}
}
// ——————– Rotate the selected backdrop image ——————–
static float mInitialAngle;
float mNewAngle;
if (!mIsTranslatingBackDrop && !mIsScalingBackDrop)
{
if (mI->Input->OnMouseButtonPress (IND_MBUTTON_RIGHT))
{
mMouseClickX = (int) mPosBrushX;
mInitialAngle = mBackDropOver->GetAngleZ();
}
if (mI->Input->IsMouseButtonPressed (IND_MBUTTON_RIGHT))
{
mNewAngle = mInitialAngle + (mPosBrushX – mMouseClickX);
mBackDropOver->SetAngleXYZ (0, 0, mNewAngle);
mIsRotatingBackDrop = true;
}
if (mI->Input->OnMouseButtonRelease (IND_MBUTTON_RIGHT))
{
mIsRotatingBackDrop = false;
}
}
// ——————– Flip backdrop image ——————–
if (mI->Input->OnKeyPress(IND_T))
{
(mBackDropOver->GetMirrorX() == true) ? mBackDropOver->SetMirrorX (false) : mBackDropOver->SetMirrorX (true);
}
if (mI->Input->OnKeyPress(IND_Y))
{
(mBackDropOver->GetMirrorY() == true) ? mBackDropOver->SetMirrorY (false) : mBackDropOver->SetMirrorY (true);
}
// ——————– Transparency ——————–
if (mI->Input->IsKeyPressed (IND_U, 5))
{
int mTrans = mBackDropOver->GetTransparency();
if (mBackDropOver->GetTransparency() > 0) mBackDropOver->SetTransparency (mBackDropOver->GetTransparency() – 1);
}
if (mI->Input->IsKeyPressed (IND_I, 5))
{
int mTrans = mBackDropOver->GetTransparency();
if (mBackDropOver->GetTransparency() < 255) mBackDropOver->SetTransparency (mBackDropOver->GetTransparency() + 1);
}
// ——————– Tiling ——————–
if (mI->Input->OnKeyPress(IND_L) && !mI->Input->IsKeyPressed (IND_LCTRL))
{
(mBackDropOver->IfWrap() == true) ? mBackDropOver->ToggleWrap (false) : mBackDropOver->ToggleWrap (true);
mBackDropOver->SetRegion (0,
0,
mBackDropOver->GetSurface()->GetWidth(),
mBackDropOver->GetSurface()->GetHeight());
}
// ——————– Tinting ——————–
if (mI->Input->IsKeyPressed(IND_SPACE))
{
mBackDropOver->SetTint (mMouseX, mMouseY, mMouseX);
mIsTintingBackDrop = true;
}
if (mI->Input->OnKeyRelease(IND_SPACE))
{
mIsTintingBackDrop = false;
}
// ——————– Z ordering ——————–
if (mI->Input->OnKeyPress(IND_Z))
{
mBackDropOver->SetPosition (mBackDropOver->GetPosX(), mBackDropOver->GetPosY(), mBackDropOver->GetPosZ() – 1);
}
if (mI->Input->OnKeyPress(IND_X))
{
mBackDropOver->SetPosition (mBackDropOver->GetPosX(), mBackDropOver->GetPosY(), mBackDropOver->GetPosZ() + 1);
}
// ——————– Clone ——————–
if (mI->Input->IsKeyPressed(IND_LSHIFT) && mI->Input->OnMouseButtonPress (IND_MBUTTON_LEFT))
{
mMap->CloneNode (mBackDropNodeOver); // Clone node
}
// ——————– Delete ——————–
if (mI->Input->OnKeyPress(IND_DELETE))
{
mMap->DeleteNode (mBackDropNodeOver); // Erase the node from the map vector
}
}
[/c]
This method takes care of the actions taken over a backdrop piece when the mouse cursor is over it. There are several methods for translating, scaling, rotating, mirroring, changing transparency level, tiling, changing the depth value of the current entity (only affects entities of the same layer), cloning, and deleting the map nodes.
For the mouse dragging methods, we have to get the brush position when the user makes the first click using OnMouseButtonPress(). We store these values in static members so the next time we enter in the function they will be still the same. We also have to activate a flag for knowing that the user is currently running an action.Then, we use the method IsMouseButtonPressed () for changing the attributes by checking out the mouse displacement from the original clicked position. Finally, we use the method OnMouseButtonRelease() in order to deactivate the flag.
Tinting action is currently just a test, it should be expanded with a proper color picker.
[c language=»++»]
/*
======================================
Listen for switching between backdrop images, drop them in the map, etc
======================================
*/
void Listener::ListenBackDropBrush()
{
// ——————– Switch between brush backdrop images (previous) ——————–
// Switch between brush backdrop images (previous)
if (mI->Input->OnMouseButtonPress (IND_MBUTTON_WHEELDOWN ))
{
// Hide current backdrop, we are going to change to another one
mBackDropBrushes [mCurrentBackDropBrush].mEntity.SetShow (false);
// Set previous backdrop as the current one
mCurrentBackDropBrush–;
if (mCurrentBackDropBrush < 0) mCurrentBackDropBrush = mNumBackDropBrushes – 1;
// Show current backdrop
mBackDropBrushes [mCurrentBackDropBrush].mEntity.SetShow (true);
}
// Switch between brush backdrop images (next)
if (mI->Input->OnMouseButtonPress (IND_MBUTTON_WHEELUP ))
{
// Hide current backdrop, we are going to change to another one
mBackDropBrushes [mCurrentBackDropBrush].mEntity.SetShow (false);
// Set next backdrop as the current one
mCurrentBackDropBrush++;
if (mCurrentBackDropBrush > mNumBackDropBrushes – 1) mCurrentBackDropBrush = 0;
// Show current backdrop
mBackDropBrushes [mCurrentBackDropBrush].mEntity.SetShow (true);
}
// ——————– Updating current backdrop "brush" position ——————–
// Position of the backdrop brush in the mouse position already calculated in ListenCommon().
mBackDropBrushes [mCurrentBackDropBrush].mEntity.SetPosition (mPosBrushX, mPosBrushY, 0);
// ——————– Drop back drop 🙂 ——————–
if (mI->Input->OnMouseButtonRelease(IND_MBUTTON_LEFT))
{
mMap->CreateNode ((int) mPosBrushX, (int) mPosBrushY, 0, mBackDropBrushes [mCurrentBackDropBrush].mId, mLayer, mBackDropBrushes [mCurrentBackDropBrush].mEntity.GetSurface());
}
}
[/c]
This method is called when we are on editing mode and the brush is not over any specific backdrop. Using the mouse wheel we can cycle through tiles. This is just as simple as hiding the previous brush entity and showing the next one using SetShow() method.
When a piece is dropped we have to create a new map node.
[c language=»++»]
/*
======================================
Input for "in game" mode. This method should be expanded with the game logic
======================================
*/
void Listener::ListenInGame()
{
// ——————– Text ——————–
strcat (mText, "\nFps: ");
strcat (mText, mI->Render->GetFpsString());
strcat (mText, "\nRendered: ");
char mDiscardedStr [64];
itoa (mI->Render->GetNumRenderedObjectsInt() – 43, (char *) &mDiscardedStr, 10);
strcat (mText, mDiscardedStr);
strcat (mText, "\nDiscarded: ");
strcat (mText, mI->Render->GetNumDiscardedObjectsString());
}
[/c]
This method only showes the frame rate and the amount of discarded pieces. If we were using this editor like an «in game» application this would be the place for calling to the game logic loop.
[c language=»++»]
/*
======================================
Resets screen resolution
======================================
*/
bool Listener::ResetScreen (int pScreenWidth, int pScreenHeight)
{
if (mI->Window->IsFullScreen())
{
if (!mI->Render->Reset ("", pScreenWidth, pScreenHeight, 32, 1, 1)) return false;
}
else
{
if (!mI->Render->Reset ("", pScreenWidth, pScreenHeight, 32, 0, 0)) return false;
}
mScreenWidth = pScreenWidth;
mScreenHeight = pScreenHeight;
mCameraGui->SetPosition (mScreenWidth / 2, mScreenHeight / 2);
return true;
}
[/c]
Just toggles to full screen mode using IndieLib methods. We haven’t used Render::ToggleFullScreen() method because I prefered to activate full screen Vsync (you will see that the fps will be limited to your screen frencuency) and to deactivate it on screen mode. This way the scrolling will be smoother in full screen.
[c language=»++»]
/*
======================================
This method changes the current layer number and name
======================================
*/
void Listener::ChangeLayer (int pNumLayer, char *pLayerName)
{
mLayer = pNumLayer;
strcpy (mLayerName, pLayerName);
}
[/c]
Trivial method for updating the layer name.
[c language=»++»]
/*
======================================
Reset zooms
======================================
*/
void Listener::ResetZoom ()
{
mCameraZoomB = 1.0f / ((float) DIST_CAMERA_B / (float) DIST_CAMERA_LAYERS);
mCameraZoomN = 1.0f / ((float) DIST_CAMERA_N / (float) DIST_CAMERA_LAYERS);
mCameraZoomM = 1.0f / ((float) DIST_CAMERA_M / (float) DIST_CAMERA_LAYERS);
mCameraZoomLayers = 1.0f;
}
[/c]
Reset the zooming factors to their initial values.
[c language=»++»]
/*
======================================
Check if the mouse is over a backdrop piece and returns the a pointer to the appropiate node
======================================
*/
Node *Listener::CheckMouseCollisions (int pLayer)
{
vector <Node*>::iterator mIter;
int mCurrentDist = NUM_INFINITE;
Node *mCandidate = 0;
// Iterate the vector of backdrop images already loaded and create
for (mIter = mMap->GetVectorNodes()->begin();
mIter != mMap->GetVectorNodes()->end();
mIter++)
{
// Only take in count the backdrops images of the current layer
if ((*mIter)->GetLayer() == pLayer)
{
// Check collision between the mouse cursor and a backdrop image
if (mI->Entity2dManager->IsCollision ((*mIter)->GetEntity(), "editor", mResources->GetMouseEntity(), "cursor"))
{
int mDistToBrush = (int) abs (mPosBrushX – (*mIter)->GetEntity()->GetPosX ());
if (mDistToBrush < mCurrentDist)
{
mCurrentDist = mDistToBrush;
mCandidate = (*mIter);
}
}
}
}
if (mCandidate)
return mCandidate;
// If the mouse is not over any backdrop of the current layer it return 0
return 0;
}
[/c]
This methods checks the collision between the mouse and the nodes. It dosen’t returns the first node that collides but the one that is nearear to the mouse cursor. This way, the user can always select the specific node although it would be inside other nodes.
[c language=»++»]
/*
======================================
Create all the backdrop brushes using the tileset loaded in Resouces class
======================================
*/
void Listener::CreateBackDropBrushes ()
{
// Initialize the array of backdrop brushes to the number of surfaces we have
// loaded in "Resources" class.
mNumBackDropBrushes = mResources->GetVectorTiles()->size();
mBackDropBrushes = new BRUSH [mNumBackDropBrushes];
// Iterate the vector of backdrop images already loaded and create 2d entities
vector <SURFACE*>::iterator mIter;
int i = 0;
for (mIter = mResources->GetVectorTiles()->begin();
mIter != mResources->GetVectorTiles()->end();
mIter++)
{
mBackDropBrushes [i].mId = (*mIter)->mId; // Id
mBackDropBrushes [i].mEntity.SetSurface (&(*mIter)->mSurface); // Set the surface (brush image) into the entity
mI->Entity2dManager->Add (BRUSH_LAYER, &mBackDropBrushes [i].mEntity); // Add the entity to the IndieLib manager
mBackDropBrushes [i].mEntity.SetHotSpot (0.5f, 0.5f); // Pivot point in the middle of the surface
mBackDropBrushes [i].mEntity.SetShow (false); // Hide the brush
mBackDropBrushes [i].mEntity.SetTransparency (128); // Brushes are 50% Transparent
i++;
}
// We only show the current brush
mCurrentBackDropBrush = 0;
mBackDropBrushes [0].mEntity.SetShow (true);
}
[/c]
This method creates a set of brushes getting the IND_Surface instances (the images) from the tileset vector of Resources class.
[c language=»++»]
/*
======================================
Free the memory used by the brushes
======================================
*/
void Listener::DeleteBackDropBrushes ()
{
// Delete all the brushes
for (int i = 0; i < mNumBackDropBrushes; i++)
{
mI->Entity2dManager->Delete (&mBackDropBrushes[i].mEntity);
}
// Delete the array of brushes
delete [] mBackDropBrushes;
}
[/c]
This method deletes all the brushes.
Main Loop
In Main.cpp you will find the initialization of all the classes and the main loop for managing the user actions and render the scene.
[c language=»++»]
#include "CIndieLib.h"
#include "Resources.h"
#include "Map.h"
#include "Listener.h"
/*
==================
Main
==================
*/
int IndieLib()
{
// —– IndieLib intialization —–
CIndieLib *mI = CIndieLib::Instance();
if (!mI->Init ()) return 0;
// —– Editor classes initialization —–
// Resource loading
Resources mResources;
char mPath [MAX_PATH];
GetCurrentDirectory (MAX_PATH, mPath); // Get the path to the current directory
strcat (mPath, "\\resources\\tileset_01.xml"); // Add the name of the tileset to the path
if (!mResources.LoadResources (mPath)) exit (0);
// Map initialization
Map mMap;
// Listener initialization
Listener mListener (&mResources, &mMap);
// —– Main Loop —–
while (!mI->Input->OnKeyPress (IND_ESCAPE) && !mI->Input->Quit())
{
// —– Input Update —-
mI->Input->Update ();
// ——– Atributes ——-
mListener.Listen ();
// ——– Render ——-
// Reset the counting of rendered and discarded objects
mI->Render->ResetNumDiscardedObjects();
mI->Render->ResetNumRenderedObject();
mI->Render->BeginScene ();
// Render banckground (two triangles)
mI->Render->BlitColoredTriangle (0, 0, mI->Window->GetWidth(), 0, 0, mI->Window->GetHeight(),
255, 128, 128,
255, 128, 128,
27, 27, 204,
255);
mI->Render->BlitColoredTriangle (0, mI->Window->GetHeight(), mI->Window->GetWidth(), 0, mI->Window->GetWidth(), mI->Window->GetHeight(),
27, 27, 204,
255, 128, 128,
27, 27, 204,
255);
// — Render parallax layer B —
mI->Render->SetCamera2d (mListener.GetCameraB());
mI->Entity2dManager->RenderEntities2d (0);
// — Render parallax layer N —
mI->Render->SetCamera2d (mListener.GetCameraN());
mI->Entity2dManager->RenderEntities2d (1);
// — Render parallax layer M —
mI->Render->SetCamera2d (mListener.GetCameraM());
mI->Entity2dManager->RenderEntities2d (2);
// — Render backdrop elements of Layers from 1 to 9 —
mI->Render->SetCamera2d (mListener.GetCameraLayers());
for (int i = 3; i < NUM_EDITOR_LAYERS; i++)
mI->Entity2dManager->RenderEntities2d (i);
// — Render editor elements (like the brush and areas) —
// If editing mode
if (mListener.GetMode())
{
switch (mListener.GetCurrentLayer())
{
case 0: mI->Render->SetCamera2d (mListener.GetCameraB()); break;
case 1: mI->Render->SetCamera2d (mListener.GetCameraN()); break;
case 2: mI->Render->SetCamera2d (mListener.GetCameraM()); break;
default: mI->Render->SetCamera2d (mListener.GetCameraLayers()); break;
}
// Render
mI->Entity2dManager->RenderEntities2d (BRUSH_LAYER);
// Render the collision areas of the working layer
mI->Entity2dManager->RenderCollisionAreas (mListener.GetCurrentLayer(), 255, 255, 255, 30);
mI->Entity2dManager->RenderGridAreas (mListener.GetCurrentLayer(), 255, 0, 0, 255);
}
// — Render texts —
// Render gui elements (text, mouse cursor)
mI->Render->SetCamera2d (mListener.GetCameraGui());
mI->Entity2dManager->RenderEntities2d (GUI_LAYER);
// — End Scene —
mI->Render->EndScene ();
//mI->Render->ShowFpsInWindowTitle();
}
// —– Free memory (we don’t use destructors becase IndieLib pointers would be pointing to null —–
mListener.Free ();
mResources.Free ();
mMap.Free ();
// —– Indielib End —–
mI->End ();
return 0;
}
[/c]
We first have to initialize all the classes. By default, we load the tileset located at \resources\tileset_01.xml. After that, we can see the main loop of the application. The loop is quite easy to understand. On each frame, we make a call to Listener::Listen() in order to manage the editor actions. After that, we draw all the stuff, starting from the background a them rendering layer by layer. We have to render first the layers that we want to appear under the rest. Before rendering the entities of each layer, we have to set its specific camera.
Finally, when the program flow goes out the main loop, we free the all the memory.
Credits
- Javier López
- Special thanks: Derek Yu and Alec Holowka for letting me use some tiles from his aweasome Aquaria game.
- Special thanks: Daniel Cook for his incredible tilesets that he offers for free to developers in his prototype challenges.
- Special thanks: Metadon, for fixing parallax layer brush position bug.
You should follow me on Twitter
Did you like the tutorial? Then you should follow me on twitter here.
Very useful tutorial. Thank you!
You are welcome. Yep, it takes some time to load the page, hehe.
First of all I want to thank you for this tutorial. It is always interesting to learn how other people do things and is a pleasure to learn new tricks.
I also would like to make some comments about the code presented here. Please, take all my comments as positive criticism since is what I intend to do, but is not always easy. So, if something seems to harsh, please excuse me.
I’ll start with a non-code issue: the past of draw is «drew» not «drawed». I know it because I’ve done this mistake so many times 😉
I see that you put «using namespace» directives in you headers. For me, this is a bad practice since it defeats the purpose of namespaces. Imagine that I have a class called ‘vector’ to descrive math vectors. If now I include your header I’m importing in my local namespace all the std namespace that I have included before your header and probably I will get duplicate name errors. In this case I would use directly std::vector directly.
The std::vector in Resources is used to look up surfaces by id and to traverse, regardless of order, the list of tiles in Listener::CreateBackDropBrushes(). I wonder, why didn’t you use an std::map instead? From my point of view we would get the same (and id linked to a surface) but without the need of a separate structure (SURFACE), faster look up, and easier clean up. Oh, and in C++ there’s no need to use the ‘typedef struct structBackDropSurfaces SURFACE’ trick. Just declare the struct as SURFACE and use it as it.
Probably I am nitpicking here, but I normally use ++mIter instead of mIter++ when using iterators (when using iterators «by hand» instead of using standard algorithms like std::for_each and such.) The reason is because in user defined types (like iterators) it is somewhat better to use pre-increment instead of post-increment for performance reasons (i.e., the post-increment operator needs to create a copy before incrementing in order to return the original value.) My rule of thumb is: if I don’t need to original value use pre-increment. Of course, your mileage may vary.
I would be a little more const-correct in some methods. For example, Node::GetLayer() and Node::GetSurfaceId() don’t modify the Node object, so I would declare them const.
More nitpicking here: please, use initialization list in constructors. An initialization list doesn’t get worse performance but sometimes it gets better performance, since a constructor doesn’t have to create objects just to assign new values later in the constructor’s body.
In Map::CloneNode() I don’t see why you are making all these calls, since they all use information from the original node that we are trying to copy. Wouldn’t be better to declare a copy constuctor in Node that already does this? After all this is a Node’s responsability, isn’t it? Cloning a node, from my point of view, should be: Node *mNewNode = new Node (*pNode);
Is there a reason because OpenFileDialog returns a char * instead of an std::string? Using an std::string is easier (mPath.empty() instead of !strcmp(mPath, «»)) and harder to make mistakes, as you do in Map::SaveMap() and Map::LoadMap(): the last delete of mPath should be delete[] ;-). With std::string I don’t care and the responsability of who to delete the data is clearer (i.e., the string object.)
Maybe this is a design issue, but when loading and saving Nodes I would let the node object to use a TiXmlElement to save itself. That way, If we have different Node types, by using inheritance, we can have a virtual method that does the correct thing for each subtype. I am not sure right now, but I thing this is called the «visitor pattern». Again, your mileage may vary here too 🙂
I prefer the new C++-style castings (static_cast, const_cast, reinterpret_cast, and dynamic_cast) instead of the C-style casts ( (type)var .) They are easier and safer to use (but no to type, I know.)
And I am stopping here 🙂
I really appreciate your work, I just wanted to throw in a little disscusion and possible improvements in an already wonderful work.
Thanks for all!
Hello Jordi!
Thank you for taking your time in order to read the sourcecode! I really appreciate your effort.
I think you are right in EVERYTHING you said. Hopefully it would help in order to improve the tutorial.
Currently I’m working in the next tutorial, and I think I should keep working on it and not to go back to this one. Maybe I’m asking too much, but do you have enough time in order to make the improvements you commented in the sourcecode and to send it to my mail (javierlopezpro [ at ] gmail [ dot ] com).
I will of course credit you and put a link to your blog.
Thank you again!
HUGE TUTORIAL.
Awesome thanks for sharing 😀
Hello Javi,
I just wanted to give a very big thanks for releasing the source code for this great editor.
I’ve been googling for a time to find open sourced (or source released) 2D Map Editors and I’ve only found tile based ones. Finally I’ve got the link to your page, and I’m very surprised with the great potential your editor can provide!
A very big thanks!
This editor is absolutely great, I created a little import library so people and myself could use your editor with any project we wanted, I felt that I should inform you that I have posted it on my website at http://www.antsportfolio.co.uk and have fully credited you for the editor it’s self.
Thanks for your work.
tnx tnx tnx
Please can I have a compiled version of this?
I just need the editor, for now!
that’s a great editor! i love it~ thanks for your code!
Really useful
Just wondering but is the editor free to use in our own projects? Looks really nice!
Sure! Feel free to use it un LGPL license 🙂
Hi, your tutorial is awesome, but after making the editor what technique do you use to handle the game collisions?
Thanks for sharing!