The file we will be parsing is the following (test.xml
in source code downloads):
<?xml version="1.0" ?> <STATES> <MENU> <TEXTURES> <texture filename="assets/button.png" ID="playbutton"/> <texture filename="assets/exit.png" ID="exitbutton"/> </TEXTURES> <OBJECTS> <object type="MenuButton" x="100" y="100" width="400" height="100" textureID="playbutton" numFrames="0" callbackID="1"/> <object type="MenuButton" x="100" y="300" width="400" height="100" textureID="exitbutton" numFrames="0" callbackID="2"/> </OBJECTS> </MENU> <PLAY> </PLAY> <GAMEOVER> </GAMEOVER> </STATES>
We are going to create a new class that parses our states for us called StateParser
. The StateParser
class has no data members, it is to be used once in the onEnter
function of a state and then discarded when it goes out of scope. Create a StateParser.h
file and add the following code:
#include <iostream> #include <vector> #include "tinyxml.h" class GameObject; class StateParser { public: bool parseState(const char* stateFile, std::string stateID, std::vector<GameObject*> *pObjects); private: void parseObjects(TiXmlElement* pStateRoot, std::vector<GameObject*> *pObjects); void parseTextures(TiXmlElement* pStateRoot, std::vector<std::string> *pTextureIDs); };
We have three functions here, one public and two private. The parseState
function takes the filename of an XML file as a parameter, along with the current stateID
value and a pointer to std::vector
of GameObject*
for that state. The StateParser.cpp
file will define this function:
bool StateParser::parseState(const char *stateFile, string stateID, vector<GameObject *> *pObjects, std::vector<std::string> *pTextureIDs) { // create the XML document TiXmlDocument xmlDoc; // load the state file if(!xmlDoc.LoadFile(stateFile)) { cerr << xmlDoc.ErrorDesc() << " "; return false; } // get the root element TiXmlElement* pRoot = xmlDoc.RootElement(); // pre declare the states root node TiXmlElement* pStateRoot = 0; // get this states root node and assign it to pStateRoot for(TiXmlElement* e = pRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { if(e->Value() == stateID) { pStateRoot = e; } } // pre declare the texture root TiXmlElement* pTextureRoot = 0; // get the root of the texture elements for(TiXmlElement* e = pStateRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { if(e->Value() == string("TEXTURES")) { pTextureRoot = e; } } // now parse the textures parseTextures(pTextureRoot, pTextureIDs); // pre declare the object root node TiXmlElement* pObjectRoot = 0; // get the root node and assign it to pObjectRoot for(TiXmlElement* e = pStateRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { if(e->Value() == string("OBJECTS")) { pObjectRoot = e; } } // now parse the objects parseObjects(pObjectRoot, pObjects); return true; }
There is a lot of code in this function so it is worth covering in some depth. We will note the corresponding part of the XML file, along with the code we use, to obtain it. The first part of the function attempts to load the XML file that is passed into the function:
// create the XML document TiXmlDocument xmlDoc; // load the state file if(!xmlDoc.LoadFile(stateFile)) { cerr << xmlDoc.ErrorDesc() << " "; return false; }
It displays an error to let you know what happened if the XML loading fails. Next we must grab the root node of the XML file:
// get the root element TiXmlElement* pRoot = xmlDoc.RootElement(); // <STATES>
The rest of the nodes in the file are all children of this root node. We must now get the root node of the state we are currently parsing; let's say we are looking for MENU
:
// declare the states root node TiXmlElement* pStateRoot = 0; // get this states root node and assign it to pStateRoot for(TiXmlElement* e = pRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { if(e->Value() == stateID) { pStateRoot = e; } }
This piece of code goes through each direct child of the root node and checks if its name is the same as stateID
. Once it finds the correct node it assigns it to pStateRoot
. We now have the root node of the state we want to parse.
<MENU> // the states root node
Now that we have a pointer to the root node of our state we can start to grab values from it. First we want to load the textures from the file so we look for the <TEXTURE>
node using the children of the pStateRoot
object we found before:
// pre declare the texture root TiXmlElement* pTextureRoot = 0; // get the root of the texture elements for(TiXmlElement* e = pStateRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { if(e->Value() == string("TEXTURES")) { pTextureRoot = e; } }
Once the <TEXTURE>
node is found, we can pass it into the private parseTextures
function (which we will cover a little later).
parseTextures(pTextureRoot, std::vector<std::string> *pTextureIDs);
The function then moves onto searching for the <OBJECT>
node and, once found, it passes it into the private parseObjects
function. We also pass in the pObjects
parameter:
// pre declare the object root node TiXmlElement* pObjectRoot = 0; // get the root node and assign it to pObjectRoot for(TiXmlElement* e = pStateRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { if(e->Value() == string("OBJECTS")) { pObjectRoot = e; } } parseObjects(pObjectRoot, pObjects); return true; }
At this point our state has been parsed. We can now cover the two private functions, starting with parseTextures
.
void StateParser::parseTextures(TiXmlElement* pStateRoot, std::vector<std::string> *pTextureIDs) { for(TiXmlElement* e = pStateRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { string filenameAttribute = e->Attribute("filename"); string idAttribute = e->Attribute("ID"); pTextureIDs->push_back(idAttribute); // push into list TheTextureManager::Instance()->load(filenameAttribute, idAttribute, TheGame::Instance()->getRenderer()); } }
This function gets the filename
and ID
attributes from each of the texture values in this part of the XML:
<TEXTURES> <texture filename="button.png" ID="playbutton"/> <texture filename="exit.png" ID="exitbutton"/> </TEXTURES>
It then adds them to TextureManager
.
TheTextureManager::Instance()->load(filenameAttribute, idAttribute, TheGame::Instance()->getRenderer());
The parseObjects
function is quite a bit more complicated. It creates objects using our GameObjectFactory
function and reads from this part of the XML file:
<OBJECTS> <object type="MenuButton" x="100" y="100" width="400" height="100" textureID="playbutton" numFrames="0" callbackID="1"/> <object type="MenuButton" x="100" y="300" width="400" height="100" textureID="exitbutton" numFrames="0" callbackID="2"/> </OBJECTS>
The
parseObjects
function is defined like so:
void StateParser::parseObjects(TiXmlElement *pStateRoot, std::vector<GameObject *> *pObjects) { for(TiXmlElement* e = pStateRoot->FirstChildElement(); e != NULL; e = e->NextSiblingElement()) { int x, y, width, height, numFrames, callbackID, animSpeed; string textureID; e->Attribute("x", &x); e->Attribute("y", &y); e->Attribute("width",&width); e->Attribute("height", &height); e->Attribute("numFrames", &numFrames); e->Attribute("callbackID", &callbackID); e->Attribute("animSpeed", &animSpeed); textureID = e->Attribute("textureID"); GameObject* pGameObject = TheGameObjectFactory::Instance() ->create(e->Attribute("type")); pGameObject->load(new LoaderParams (x,y,width,height,textureID,numFrames,callbackID, animSpeed)); pObjects->push_back(pGameObject); } }
First we get any values we need from the current node. Since XML files are pure text, we cannot simply grab ints or floats from the file. TinyXML has functions with which you can pass in the value you want to be set and the attribute name. For example:
e->Attribute("x", &x);
This sets the variable x
to the value contained within attribute "x"
. Next comes the creation of a GameObject
* class using the factory.
GameObject* pGameObject = TheGameObjectFactory::Instance()->create(e->Attribute("type"));
We pass in the value from the type
attribute and use that to create the correct object from the factory. After this we must use the load
function of GameObject
to set our desired values using the values loaded from the XML file.
pGameObject->load(new LoaderParams(x,y,width,height,textureID,numFrames,callbackID));
And finally we push pGameObject
into the pObjects
array, which is actually a pointer to the current state's object vector.
pObjects->push_back(pGameObject);
3.147.79.84