A 3d mockup I created just for fan after playing Harvest: Massive Encounter. As usual, 3ds Max & Vray.

A 3d mockup I created just for fan after playing Harvest: Massive Encounter. As usual, 3ds Max & Vray.
A 3d building using 3d Studio Max & Vray.
A 3d Shopping Center using 3d Studio Max & Vray. Check out and higher resolution version here.
A 3d living room using 3d Studio Max & Vray. Check out and higher resolution version here: screenshot 1, screenshot2.
A 3d bedroom using 3d Studio Max & Vray. Check out and higher resolution version here.
A 3d toilet using 3d Studio Max & Vray. Check out and higher resolution version here: screenshot 1, screenshot2, screenshot 3.
A 3d kitchen using 3d Studio Max & Vray. Check out and higher resolution version here.
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.
Here it is the complete sourcecode.
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.
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.
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.
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.
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.
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:
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.
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.
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.
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:
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.
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.
Did you like the tutorial? Then you should follow me on twitter here.
IndieLib is a c++ 2.5d engine for game development and fast game prototyping in a really easy way. Internally it uses Direct3d for hardware acceleration, but doesn’t use DirectDraw or ID3DXSprite, it directly draws textures on polygons. The engine is focused in fast 2d rendering, but also allows you to use 3d models. Check out the main IndieLib features in the FAQ.
We are going to learn how to create a Tetris clone from scratch using simple and clean C++. And this will take you less than an hour! This is the perfect tutorial for beginners. Just enjoy it and leave a comment if you want me to explain something better. I know my English sucks, so if you see some mistakes, please, tell me. Let’s go!
Updated! 03/04/2012
Here it is the complete sourcecode.
The sourcecode comes with SDL includes and libs ready to compile in Visual C++ Express Edition 2008. In «Release» folder there is also an executable file just in case you want to try it directly.
Thanks to lmelior and to Javier Santana, there is a Linux version of this tutorial. The sourcecode is platform independent and comes with a «makefile». However, under Linux, you need libsdl-gfx1.2-dev and libsdl1.2-dev (If you are using Ubuntu you can get them this way: sudo apt-get install libsdl1.2-dev libsdl-gfx1.2-dev)
ESC | Quit the game |
z | Rotate piece |
x | Drop piece |
Left, Right, Down | I will not offend your intelligence |
We are going to focus on the game logic, using only rectangle primitives (SDL) for the rendering. All the game logic is isolated from the drawing, so you can expand the tutorial easily. I’m planning making a second tutorial of how to improve this Tetris clone using sprites, background, effects, etc. But right now, let’s focus on the game logic. This is how your prototype will look after you finish the tutorial:
In this tutorial you will learn:
What you are supposed to already know:
What do you need?
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.
First, we are going to create a class for storing all the pieces. There are 7 different types of pieces: square, I, L, L-mirrored, N, N-mirrored and T. But, how can we define each piece? Just check out the figure:
As you can see, this piece is defined in a matrix of 5×5 cells. 0 means «no block», 1 means «normal block» and 2 means «pivot block». The pivot block is the rotation point: yes, the original Tetris game has a rotation point for each piece 🙂
And how can we store that using C++? Easy: using a bidimensional array of 5×5 ints (or bytes, if you are a fanatic of optimization). The previous piece is stored like that:
[c language=»++»]
{0, 0, 0, 0, 0},
{0, 0, 0, 1, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
[/c]
Now that we already now how to store each piece let’s think about rotations. We can solve the rotation problem in a lot of different ways. In other tutorials, I’ve seen them use complex rotation algebra in order to rotate the piece… but we can solve this problem easily. If we can store each piece… why don’t we just store each piece rotated too? There are four possible rotations for each piece:
As you can see, the longer piece is only 4 block widht. But we are using 5 blocks matrices in order to be able to store all the rotations respeting the pivot block. In a previous version of this tutorial, I was using 4-block matrices, but then it was necessary to store translations of the pivot to the origin. This way, we are using some bytes more but the sourcecode is cleaner. In total we only use 448 bytes to store all the pieces. That’s nothing 🙂
So, in order to store all this information we need a 4-dimensional array (wow!), in order to store the 4 possible rotations (matrices of 5×5) of each piece:
[c language=»++»]
// Pieces definition
char mPieces [7 /*kind */ ][4 /* rotation */ ][5 /* horizontal blocks */ ][5 /* vertical blocks */ ] =
{
// Square
{
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
}
},
// I
{
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 1, 1},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{1, 1, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 1, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
}
}
,
// L
{
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 1, 0},
{0, 1, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 1, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 1, 0},
{0, 1, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
}
},
// L mirrored
{
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 0, 0},
{0, 1, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 1, 0, 0, 0},
{0, 1, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 1, 0},
{0, 0, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 1, 0},
{0, 0, 0, 1, 0},
{0, 0, 0, 0, 0}
}
},
// N
{
{
{0, 0, 0, 0, 0},
{0, 0, 0, 1, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 0, 0},
{0, 0, 1, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 1, 2, 0, 0},
{0, 1, 0, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 1, 1, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
}
},
// N mirrored
{
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 0, 1, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 2, 1, 0},
{0, 1, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 1, 0, 0, 0},
{0, 1, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 1, 0},
{0, 1, 2, 0, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
}
},
// T
{
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 2, 1, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0},
{0, 1, 2, 1, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 1, 2, 0, 0},
{0, 0, 1, 0, 0},
{0, 0, 0, 0, 0}
},
{
{0, 0, 0, 0, 0},
{0, 0, 1, 0, 0},
{0, 1, 2, 1, 0},
{0, 0, 0, 0, 0},
{0, 0, 0, 0, 0}
}
}
};
[/c]
Great! Now, in order to rotate a piece we just have to choose the following stored rotated piece.
There is something important that we have to take in count. Each different piece must be correctly positioned every time it is created on the top of the screen. In other words, it needs to be translated to the correct position (in order to show ONLY one row of blocks in the board and to be centered, upper blocks should be OUTSIDE the board). Like each piece is different (some are lower or smaller than others in the matrices), each one needs a different translation every time it is created. We will store these translations in another array, one translation per rotated piece. Take your time to understand this.
The translation are two numbers (horizontal tranlastion, vertical translation) that we have to store for each piece. We will use these numbers later in «Game» class when creating the pieces each time a new piece appears, so it will be initialized in the correct position. This is the array that stores these displacements:
[c language=»++»]
// Displacement of the piece to the position where it is first drawn in the board when it is created
int mPiecesInitialPosition [7 /*kind */ ][4 /* r2otation */ ][2 /* position */] =
{
/* Square */
{
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -3}
},
/* I */
{
{-2, -2},
{-2, -3},
{-2, -2},
{-2, -3}
},
/* L */
{
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -2}
},
/* L mirrored */
{
{-2, -3},
{-2, -2},
{-2, -3},
{-2, -3}
},
/* N */
{
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -2}
},
/* N mirrored */
{
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -2}
},
/* T */
{
{-2, -3},
{-2, -3},
{-2, -3},
{-2, -2}
},
};
[/c]
And with that we have solved one of the most tricky parts of this tutorial.
We can now create our Pieces class, this file is called «Pieces.h»:
[c language=»++»]
#ifndef _PIECES_
#define _PIECES_
// ——————————————————————————–
// Pieces
// ——————————————————————————–
class Pieces
{
public:
int GetBlockType (int pPiece, int pRotation, int pX, int pY);
int GetXInitialPosition (int pPiece, int pRotation);
int GetYInitialPosition (int pPiece, int pRotation);
};
#endif // _PIECES_
[/c]
The 3 methods that you can see in the header returns some information that we will need later. Their implementation is trivial:
[c language=»++»]
/*
======================================
Return the type of a block (0 = no-block, 1 = normal block, 2 = pivot block)
Parameters:
>> pPiece: Piece to draw
>> pRotation: 1 of the 4 possible rotations
>> pX: Horizontal position in blocks
>> pY: Vertical position in blocks
======================================
*/
int Pieces::GetBlockType (int pPiece, int pRotation, int pX, int pY)
{
return mPieces [pPiece][pRotation][pX][pY];
}
/*
======================================
Returns the horizontal displacement of the piece that has to be applied in order to create it in the
correct position.
Parameters:
>> pPiece: Piece to draw
>> pRotation: 1 of the 4 possible rotations
======================================
*/
int Pieces::GetXInitialPosition (int pPiece, int pRotation)
{
return mPiecesInitialPosition [pPiece][pRotation][0];
}
/*
======================================
Returns the vertical displacement of the piece that has to be applied in order to create it in the
correct position.
Parameters:
>> pPiece: Piece to draw
>> pRotation: 1 of the 4 possible rotations
======================================
*/
int Pieces::GetYInitialPosition (int pPiece, int pRotation)
{
return mPiecesInitialPosition [pPiece][pRotation][1];
}
[/c]
Now we are going to learn how to store the pieces in the board and check collisions. This class stores a bidimensional array of N x N blocks that are initialized to POS_FREE. The pieces will be stored by filling these blocks when they fall down updating the block to POS_FILLED. In this class we need to implement methods in order to store a piece, check if a movement is possible, delete lines, etc. Our board is going to be very flexible, we will be able to choose the amount of horizontal and vertical blocks and the size of each block.
This is the header of the class («Board.h»):
[c language=»++»]
#ifndef _BOARD_
#define _BOARD_
// —— Includes —–
#include "Pieces.h"
// —— Defines —–
#define BOARD_LINE_WIDTH 6 // Width of each of the two lines that delimit the board
#define BLOCK_SIZE 16 // Width and Height of each block of a piece
#define BOARD_POSITION 320 // Center position of the board from the left of the screen
#define BOARD_WIDTH 10 // Board width in blocks
#define BOARD_HEIGHT 20 // Board height in blocks
#define MIN_VERTICAL_MARGIN 20 // Minimum vertical margin for the board limit
#define MIN_HORIZONTAL_MARGIN 20 // Minimum horizontal margin for the board limit
#define PIECE_BLOCKS 5 // Number of horizontal and vertical blocks of a matrix piece
// ——————————————————————————–
// Board
// ——————————————————————————–
class Board
{
public:
Board (Pieces *pPieces, int pScreenHeight);
int GetXPosInPixels (int pPos);
int GetYPosInPixels (int pPos);
bool IsFreeBlock (int pX, int pY);
bool IsPossibleMovement (int pX, int pY, int pPiece, int pRotation);
void StorePiece (int pX, int pY, int pPiece, int pRotation);
void DeletePossibleLines ();
bool IsGameOver ();
private:
enum { POS_FREE, POS_FILLED }; // POS_FREE = free position of the board; POS_FILLED = filled position of the board
int mBoard [BOARD_WIDTH][BOARD_HEIGHT]; // Board that contains the pieces
Pieces *mPieces;
int mScreenHeight;
void InitBoard();
void DeleteLine (int pY);
};
#endif // _BOARD_
[/c]
Now, let’s see each different method.
InitBoard method is just a nested loop that initializes all the board blocks to POS_FREE.
[c language=»++»]
/*
======================================
Init the board blocks with free positions
======================================
*/
void Board::InitBoard()
{
for (int i = 0; i < BOARD_WIDTH; i++)
for (int j = 0; j < BOARD_HEIGHT; j++)
mBoard[i][j] = POS_FREE;
}
[/c]
StorePiece method, just stores a piece in the board by filling the appropriate blocks as POS_FILLED. There is a nested loop that iterates through the piece matrix and store the blocks in the board.
[c language=»++»]
/*
======================================
Store a piece in the board by filling the blocks
Parameters:
>> pX: Horizontal position in blocks
>> pY: Vertical position in blocks
>> pPiece: Piece to draw
>> pRotation: 1 of the 4 possible rotations
======================================
*/
void Board::StorePiece (int pX, int pY, int pPiece, int pRotation)
{
// Store each block of the piece into the board
for (int i1 = pX, i2 = 0; i1 < pX + PIECE_BLOCKS; i1++, i2++)
{
for (int j1 = pY, j2 = 0; j1 < pY + PIECE_BLOCKS; j1++, j2++)
{
// Store only the blocks of the piece that are not holes
if (mPieces->GetBlockType (pPiece, pRotation, j2, i2) != 0)
mBoard[i1][j1] = POS_FILLED;
}
}
}
[/c]
IsGameOver checks if there are blocks in the first row. That means the game is over.
[c language=»++»]
/*
======================================
Check if the game is over becase a piece have achived the upper position
Returns true or false
======================================
*/
bool Board::IsGameOver()
{
//If the first line has blocks, then, game over
for (int i = 0; i < BOARD_WIDTH; i++)
{
if (mBoard[i][0] == POS_FILLED) return true;
}
return false;
}
[/c]
DeleteLine is the method that erases a line and moves all the blocks of upper positions one row down. It just starts from the line that has to be removed, and then, iterating through the board in a nested loop, moves all the blocks of the upper lines one row done.
[c language=»++»]
/*
======================================
Delete a line of the board by moving all above lines down
Parameters:
>> pY: Vertical position in blocks of the line to delete
======================================
*/
void Board::DeleteLine (int pY)
{
// Moves all the upper lines one row down
for (int j = pY; j > 0; j–)
{
for (int i = 0; i < BOARD_WIDTH; i++)
{
mBoard[i][j] = mBoard[i][j-1];
}
}
}
[/c]
DeletePossibleLines is a method that removes all the lines that should be erased from the board. It works by first checking which lines should be removed (the ones that have all their horizontal blocks filled). Then, it uses the DeleteLine method in order to erase that line and move all the upper lines one row down.
[c language=»++»]
/*
======================================
Delete all the lines that should be removed
======================================
*/
void Board::DeletePossibleLines ()
{
for (int j = 0; j < BOARD_HEIGHT; j++)
{
int i = 0;
while (i < BOARD_WIDTH)
{
if (mBoard[i][j] != POS_FILLED) break;
i++;
}
if (i == BOARD_WIDTH) DeleteLine (j);
}
}
[/c]
IsFreeBlock is a trivial method that checks out if a board block is filled or not.
[c language=»++»]
/*
======================================
Returns 1 (true) if the this block of the board is empty, 0 if it is filled
Parameters:
>> pX: Horizontal position in blocks
>> pY: Vertical position in blocks
======================================
*/
bool Board::IsFreeBlock (int pX, int pY)
{
if (mBoard [pX][pY] == POS_FREE) return true; else return false;
}
[/c]
Until now we have been always talking about «blocks». But in order to draw them to the screen we need to specify the position in pixels. So, we need two methods (GetXPosInPixels and GetYPosInPixels ) in order to obtain the horizontal and vertical position in pixels of a given block.
[c language=»++»]
/*
======================================
Returns the horizontal position (in pixels) of the block given like parameter
Parameters:
>> pPos: Horizontal position of the block in the board
======================================
*/
int Board::GetXPosInPixels (int pPos)
{
return ( ( BOARD_POSITION – (BLOCK_SIZE * (BOARD_WIDTH / 2)) ) + (pPos * BLOCK_SIZE) );
}
/*
======================================
Returns the vertical position (in pixels) of the block given like parameter
Parameters:
>> pPos: Horizontal position of the block in the board
======================================
*/
int Board::GetYPosInPixels (int pPos)
{
return ( (mScreenHeight – (BLOCK_SIZE * BOARD_HEIGHT)) + (pPos * BLOCK_SIZE) );
}
[/c]
IsPossibleMovement is the last and most complex method of Board class. This method will be used later in the main loop to check if the movement of a piece is possible or not. The method compares all the blocks of a piece with the blocks already stored in the board and with the board limits. That comparison is made by iterating through the piece matrix and comparing with the appropriate 5×5 area in the board. If there is a collision that means the movement is not possible, so it returns false. If there is no collision, the movement is possible and it returns true.
[c language=»++»]
/*
======================================
Check if the piece can be stored at this position without any collision
Returns true if the movement is possible, false if it not possible
Parameters:
>> pX: Horizontal position in blocks
>> pY: Vertical position in blocks
>> pPiece: Piece to draw
>> pRotation: 1 of the 4 possible rotations
======================================
*/
bool Board::IsPossibleMovement (int pX, int pY, int pPiece, int pRotation)
{
// Checks collision with pieces already stored in the board or the board limits
// This is just to check the 5×5 blocks of a piece with the appropriate area in the board
for (int i1 = pX, i2 = 0; i1 < pX + PIECE_BLOCKS; i1++, i2++)
{
for (int j1 = pY, j2 = 0; j1 < pY + PIECE_BLOCKS; j1++, j2++)
{
// Check if the piece is outside the limits of the board
if ( i1 < 0 ||
i1 > BOARD_WIDTH – 1 ||
j1 > BOARD_HEIGHT – 1)
{
if (mPieces->GetBlockType (pPiece, pRotation, j2, i2) != 0)
return 0;
}
// Check if the piece have collisioned with a block already stored in the map
if (j1 >= 0)
{
if ((mPieces->GetBlockType (pPiece, pRotation, j2, i2) != 0) &&
(!IsFreeBlock (i1, j1)) )
return false;
}
}
}
// No collision
return true;
}
[/c]
Now we are going to implement a general class, called «Game», that itializes the game, draws the board and pieces by drawing each block as a rectangle (using another class that we will see later called «IO» that uses SDL) and creates new random pieces.
This is the header, «Game.h»:
[c language=»++»]
#ifndef _GAME_
#define _GAME_
// —— Includes —–
#include "Board.h"
#include "Pieces.h"
#include "IO.h"
#include <time.h>
// —— Defines —–
#define WAIT_TIME 700 // Number of milliseconds that the piece remains before going 1 block down */
// ——————————————————————————–
// Game
// ——————————————————————————–
class Game
{
public:
Game (Board *pBoard, Pieces *pPieces, IO *pIO, int pScreenHeight);
void DrawScene ();
void CreateNewPiece ();
int mPosX, mPosY; // Position of the piece that is falling down
int mPiece, mRotation; // Kind and rotation the piece that is falling down
private:
int mScreenHeight; // Screen height in pixels
int mNextPosX, mNextPosY; // Position of the next piece
int mNextPiece, mNextRotation; // Kind and rotation of the next piece
Board *mBoard;
Pieces *mPieces;
IO *mIO;
int GetRand (int pA, int pB);
void InitGame();
void DrawPiece (int pX, int pY, int pPiece, int pRotation);
void DrawBoard ();
};
#endif // _GAME_
[/c]
As you can see, the current piece is defined using 4 variables: mPosX, mPosY (the position of the piece in blocks), mPiece (the type of the piece), mRotation (the current matrix that defines the piece, as we have seen, each piece has four matrices, one for each rotation).
Let’s see the implementation of the methods.
GetRand is a trivial method that returns a random number between two boundaries.
[c language=»++»]
/*
======================================
Get a random int between to integers
Parameters:
>> pA: First number
>> pB: Second number
======================================
*/
int Game::GetRand (int pA, int pB)
{
return rand () % (pB – pA + 1) + pA;
}
[/c]
InitGame, takes care of the initialization of the game by selecting the first and next piece randomly. The next piece is shown so the player can see which piece will appear next. This method also sets the position in blocks of that pieces. We use two methods that we have seen before in «Pieces» class: GetXInitialPosition and GetYInitialPosition in order to initialize the piece in the correct position.
[c language=»++»]
/*
======================================
Initial parameters of the game
======================================
*/
void Game::InitGame()
{
// Init random numbers
srand ((unsigned int) time(NULL));
// First piece
mPiece = GetRand (0, 6);
mRotation = GetRand (0, 3);
mPosX = (BOARD_WIDTH / 2) + mPieces->GetXInitialPosition (mPiece, mRotation);
mPosY = mPieces->GetYInitialPosition (mPiece, mRotation);
// Next piece
mNextPiece = GetRand (0, 6);
mNextRotation = GetRand (0, 3);
mNextPosX = BOARD_WIDTH + 5;
mNextPosY = 5;
}
[/c]
CreateNewPiece method sets the «next piece» as the current one and resets its position, then selects a new «next piece».
[c language=»++»]
/*
======================================
Create a random piece
======================================
*/
void Game::CreateNewPiece()
{
// The new piece
mPiece = mNextPiece;
mRotation = mNextRotation;
mPosX = (BOARD_WIDTH / 2) + mPieces->GetXInitialPosition (mPiece, mRotation);
mPosY = mPieces->GetYInitialPosition (mPiece, mRotation);
// Random next piece
mNextPiece = GetRand (0, 6);
mNextRotation = GetRand (0, 3);
}
[/c]
DrawPiece is a really easy method that iterates through the piece matrix and draws each block of the piece. It uses green for the normal blocks and blue for the pivot block. For drawing the rectangles it calls to DrawRectangle method of the class «IO» that we will see later.
[c language=»++»]
/*
======================================
Draw piece
Parameters:
>> pX: Horizontal position in blocks
>> pY: Vertical position in blocks
>> pPiece: Piece to draw
>> pRotation: 1 of the 4 possible rotations
======================================
*/
void Game::DrawPiece (int pX, int pY, int pPiece, int pRotation)
{
color mColor; // Color of the block
// Obtain the position in pixel in the screen of the block we want to draw
int mPixelsX = mBoard->GetXPosInPixels (pX);
int mPixelsY = mBoard->GetYPosInPixels (pY);
// Travel the matrix of blocks of the piece and draw the blocks that are filled
for (int i = 0; i < PIECE_BLOCKS; i++)
{
for (int j = 0; j < PIECE_BLOCKS; j++)
{
// Get the type of the block and draw it with the correct color
switch (mPieces->GetBlockType (pPiece, pRotation, j, i))
{
case 1: mColor = GREEN; break; // For each block of the piece except the pivot
case 2: mColor = BLUE; break; // For the pivot
}
if (mPieces->GetBlockType (pPiece, pRotation, j, i) != 0)
mIO->DrawRectangle (mPixelsX + i * BLOCK_SIZE,
mPixelsY + j * BLOCK_SIZE,
(mPixelsX + i * BLOCK_SIZE) + BLOCK_SIZE – 1,
(mPixelsY + j * BLOCK_SIZE) + BLOCK_SIZE – 1,
mColor);
}
}
}
[/c]
DrawBoard is similiar to the previous method. It draws two blue columns that are used as the limits of the boards. Then draws the board blocks that are flagged as POS_FILLED in a nested loop.
[c language=»++»]
/*
======================================
Draw board
Draw the two lines that delimit the board
======================================
*/
void Game::DrawBoard ()
{
// Calculate the limits of the board in pixels
int mX1 = BOARD_POSITION – (BLOCK_SIZE * (BOARD_WIDTH / 2)) – 1;
int mX2 = BOARD_POSITION + (BLOCK_SIZE * (BOARD_WIDTH / 2));
int mY = mScreenHeight – (BLOCK_SIZE * BOARD_HEIGHT);
// Check that the vertical margin is not to small
//assert (mY > MIN_VERTICAL_MARGIN);
// Rectangles that delimits the board
mIO->DrawRectangle (mX1 – BOARD_LINE_WIDTH, mY, mX1, mScreenHeight – 1, BLUE);
mIO->DrawRectangle (mX2, mY, mX2 + BOARD_LINE_WIDTH, mScreenHeight – 1, BLUE);
// Check that the horizontal margin is not to small
//assert (mX1 > MIN_HORIZONTAL_MARGIN);
// Drawing the blocks that are already stored in the board
mX1 += 1;
for (int i = 0; i < BOARD_WIDTH; i++)
{
for (int j = 0; j < BOARD_HEIGHT; j++)
{
// Check if the block is filled, if so, draw it
if (!mBoard->IsFreeBlock(i, j))
mIO->DrawRectangle ( mX1 + i * BLOCK_SIZE,
mY + j * BLOCK_SIZE,
(mX1 + i * BLOCK_SIZE) + BLOCK_SIZE – 1,
(mY + j * BLOCK_SIZE) + BLOCK_SIZE – 1,
RED);
}
}
}
[/c]
DrawScene, just calls the previous methods in order to draw everything.
[c language=»++»]
/*
======================================
Draw scene
Draw all the objects of the scene
======================================
*/
void Game::DrawScene ()
{
DrawBoard (); // Draw the delimitation lines and blocks stored in the board
DrawPiece (mPosX, mPosY, mPiece, mRotation); // Draw the playing piece
DrawPiece (mNextPosX, mNextPosY, mNextPiece, mNextRotation); // Draw the next piece
}
[/c]
«IO.cpp» and «IO.h» are the files that implement the «IO» class. It uses SDL in order to create the window, clear it, update the screen and take care of the keyboard input. You can check out «IO.cpp» and «IO.h» files in order to see its implementation. I’m not going to explain the methods that are SDL related. You can change this class in order to use a different renderer (like IndieLib, Allegro, OpenGL, Direct3d, etc).
This is the header («IO.h»):
[c language=»++»]
#ifndef _IO_
#define _IO_
// —— Includes —–
#ifndef LINUX
#include "SDL/include/SDL.h"
#include "SDL/SDL_GfxPrimitives/SDL_gfxPrimitives.h"
#else
#include <SDL/SDL.h>
#include "SDL/SDL_GfxPrimitives/sdl_gfxprimitives.h"
#endif
#pragma comment (lib, "SDL/lib/SDL.lib")
#pragma comment (lib, "SDL/SDL_GfxPrimitives/SDL_GfxPrimitives_Static.lib")
// —— Enums —–
enum color {BLACK, RED, GREEN, BLUE, CYAN, MAGENTA, YELLOW, WHITE, COLOR_MAX}; // Colors
// ——————————————————————————–
// IO
// ——————————————————————————–
class IO
{
public:
IO ();
void DrawRectangle (int pX1, int pY1, int pX2, int pY2, enum color pC);
void ClearScreen ();
int GetScreenHeight ();
int InitGraph ();
int Pollkey ();
int Getkey ();
int IsKeyDown (int pKey);
void UpdateScreen ();
};
#endif // _IO_
[/c]
The main loop is quite simple. In each frame we draw everything. Later, we use keyboard input in order to move the piece. Before each movement, we first check out if it is possible. We also measure the time in order to move the piece down every n milliseconds. When the piece fall down one block, we check out if that movement is possible, if not, we store the piece in the board. We also check out if there are blocks in the upper row, if so, the game is over.
Let’s see «Main.cpp» step by step:
First, we initialize all the classes. Then, we get the actual milliseconds, which will be used to determine when the piece should move down.
[c language=»++»]
#include "Game.h"
#ifndef LINUX
#include <windows.h>
#endif
/*
==================
Main
==================
*/
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
// —– Vars —–
// Class for drawing staff, it uses SDL for the rendering. Change the methods of this class
// in order to use a different renderer
IO mIO;
int mScreenHeight = mIO.GetScreenHeight();
// Pieces
Pieces mPieces;
// Board
Board mBoard (&mPieces, mScreenHeight);
// Game
Game mGame (&mBoard, &mPieces, &mIO, mScreenHeight);
// Get the actual clock milliseconds (SDL)
unsigned long mTime1 = SDL_GetTicks();
[/c]
This is the main loop. We can exit by pressing ESC. In each frame we clear and update the screen and draw everything.
[c language=»++»]
// —– Main Loop —–
while (!mIO.IsKeyDown (SDLK_ESCAPE))
{
// —– Draw —–
mIO.ClearScreen (); // Clear screen
mGame.DrawScene (); // Draw staff
mIO.UpdateScreen (); // Put the graphic context in the screen
[/c]
We start with the input. If we press left, down or right we try to move the piece in that directions. We only move the piece if the movement is possible.
[c language=»++»]
// —– Input —–
int mKey = mIO.Pollkey();
switch (mKey)
{
case (SDLK_RIGHT):
{
if (mBoard.IsPossibleMovement (mGame.mPosX + 1, mGame.mPosY, mGame.mPiece, mGame.mRotation))
mGame.mPosX++;
break;
}
case (SDLK_LEFT):
{
if (mBoard.IsPossibleMovement (mGame.mPosX – 1, mGame.mPosY, mGame.mPiece, mGame.mRotation))
mGame.mPosX–;
break;
}
case (SDLK_DOWN):
{
if (mBoard.IsPossibleMovement (mGame.mPosX, mGame.mPosY + 1, mGame.mPiece, mGame.mRotation))
mGame.mPosY++;
break;
}
[/c]
By pressing «x», the piece will fall down directly to the ground. This is really easy to implement by trying to move the piece down until the movement is not possible. Then we store the piece, delete possible lines and check out if the game is over, if not, we create a new piece.
[c language=»++»]
case (SDLK_x):
{
// Check collision from up to down
while (mBoard.IsPossibleMovement(mGame.mPosX, mGame.mPosY, mGame.mPiece, mGame.mRotation)) { mGame.mPosY++; }
mBoard.StorePiece (mGame.mPosX, mGame.mPosY – 1, mGame.mPiece, mGame.mRotation);
mBoard.DeletePossibleLines ();
if (mBoard.IsGameOver())
{
mIO.Getkey();
exit(0);
}
mGame.CreateNewPiece();
break;
}
[/c]
By pressing «z» we rotate the piece. With the methods that we have already implement this is an easy task. The rotation is in fact to change to the next rotated stored piece. We first should check that the rotated piece will be drawn without colliding, if so, we sets this rotation as the current one.
[c language=»++»]
case (SDLK_z):
{
if (mBoard.IsPossibleMovement (mGame.mPosX, mGame.mPosY, mGame.mPiece, (mGame.mRotation + 1) % 4))
mGame.mRotation = (mGame.mRotation + 1) % 4;
break;
}
}
[/c]
If WAIT_TIME passed, the piece should fall down one block. We have to check out if the movement is possible, if not, the piece should be stored and we have to check if we can delete lines. We also see if the game is over, if not, we create a new piece.
[c language=»++»]
// —– Vertical movement —–
unsigned long mTime2 = SDL_GetTicks();
if ((mTime2 – mTime1) > WAIT_TIME)
{
if (mBoard.IsPossibleMovement (mGame.mPosX, mGame.mPosY + 1, mGame.mPiece, mGame.mRotation))
{
mGame.mPosY++;
}
else
{
mBoard.StorePiece (mGame.mPosX, mGame.mPosY, mGame.mPiece, mGame.mRotation);
mBoard.DeletePossibleLines ();
if (mBoard.IsGameOver())
{
mIO.Getkey();
exit(0);
}
mGame.CreateNewPiece();
}
mTime1 = SDL_GetTicks();
}
}
return 0;
}
[/c]
And that’s all! Please leave a comment if you see some mistakes, language errors or if you have any doubts… or just to say thanks! 🙂
Don’t forget to play with the «defines». Crazy example:
[c language=»++»]
#define BLOCK_SIZE 5 // Width and Height of each block of a
#define BOARD_WIDTH 90 // Board width in blocks
#define BOARD_HEIGHT 90 // Board height in blocks
[/c]
Did you like the tutorial? Then you should follow me on twitter here.