Explorar el Código

Merge pull request #1713 from rcmaniac25/encoder_2D

Encoder support for Tiled (*.tmx) to Gameplay Scene (*.scene) conversion
Sean Taylor hace 11 años
padre
commit
2b809b191b

+ 9 - 1
gameplay/src/SceneLoader.cpp

@@ -99,7 +99,8 @@ Scene* SceneLoader::loadInternal(const char* url)
         SceneNodeProperty::SCRIPT |
         SceneNodeProperty::SPRITE |
         SceneNodeProperty::TILESET |
-        SceneNodeProperty::TEXT);
+        SceneNodeProperty::TEXT |
+        SceneNodeProperty::ENABLED);
     applyNodeProperties(sceneProperties, SceneNodeProperty::COLLISION_OBJECT);
 
     // Apply node tags
@@ -439,6 +440,9 @@ void SceneLoader::applyNodeProperty(SceneNode& sceneNode, Node* node, const Prop
         case SceneNodeProperty::SCRIPT:
             node->addScript(snp._value.c_str());
             break;
+        case SceneNodeProperty::ENABLED:
+            node->setEnabled(snp._value.compare("true") == 0);
+            break;
         default:
             GP_ERROR("Unsupported node property type (%d).", snp._type);
             break;
@@ -852,6 +856,10 @@ void SceneLoader::parseNode(Properties* ns, SceneNode* parent, const std::string
         {
             addSceneNodeProperty(sceneNode, SceneNodeProperty::SCRIPT, ns->getString());
         }
+        else if (strcmp(name, "enabled") == 0)
+        {
+            addSceneNodeProperty(sceneNode, SceneNodeProperty::ENABLED, ns->getString());
+        }
         else
         {
             GP_ERROR("Unsupported node property: %s = %s", name, ns->getString());

+ 2 - 1
gameplay/src/SceneLoader.h

@@ -61,7 +61,8 @@ private:
             SCRIPT = 2048,
             SPRITE = 4096,
             TILESET = 8192,
-            TEXT = 16384
+            TEXT = 16384,
+            ENABLED = 32768
         };
 
         SceneNodeProperty(Type type, const std::string& value, int index, bool isUrl);

+ 190 - 190
samples/browser/res/common/sprites/sprite.scene

@@ -1,193 +1,193 @@
 scene spriteSample
 {
-	// Width and height are expected to be 1280x720
-	node camera
-	{
-		camera
-		{
-			type = ORTHOGRAPHIC
-			nearPlane = 0
-			farPlane = 100
-
-			// zoomX default is game width
-			// zoomY default is game height
-			// aspectRatio default is game width / game height
-		}
-		// width and height are divided in half
-		translate = 640, 360, 0
-	}
-
-	// Background sprite
-	node background
-	{
-		sprite
-		{
-			path = res/common/sprites/background.png
-
-			// game width * 5
-			width = 6400
-			height = 720
-		}
-	}
-
-	// Level floor
-	node floor
-	{
-		tileset
-		{
-			path = res/common/sprites/level.png
-
-			tileWidth = 70
-			tileHeight = 70
-			rows = 3
-			columns = 7
-
-			tile
-			{
-				cell = 0, 0
-				source = 568, 284
-			}
-			tile
-			{
-				cell = 1, 0
-				source = 568, 284
-			}
-			tile
-			{
-				cell = 2, 0
-				source = 568, 284
-			}
-			tile
-			{
-				cell = 3, 0
-				source = 568, 284
-			}
-			tile
-			{
-				cell = 4, 0
-				source = 497, 284
-			}
-
-			tile
-			{
-				cell = 0, 1
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 1, 1
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 2, 1
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 3, 1
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 4, 1
-				source = 710, 142
-			}
-			tile
-			{
-				cell = 5, 1
-				source = 497, 284
-			}
-
-			tile
-			{
-				cell = 0, 2
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 1, 2
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 2, 2
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 3, 2
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 4, 2
-				source = 568, 0
-			}
-			tile
-			{
-				cell = 5, 2
-				source = 710, 142
-			}
-			tile
-			{
-				cell = 6, 2
-				source = 497, 284
-			}
-		}
-	}
-
-	node player
-	{
-		sprite
-		{
-			path = res/common/sprites/player1.png
-
-			width = 72
-			height = 97
-			source = 67, 196, 66, 92
-			frameCount = 13
-		}
-
-		// Position player at lower-left. Y position is floor's tileset height (tileHeight * rows)
-		translate = 0, 210, 0
-	}
-
-	node rocket
-	{
-		sprite
-		{
-			path = res/common/sprites/rocket.png
-
-			width = 128
-			height = 128
-			blendMode = BLEND_ADDITIVE
-			anchor = 0.5, 0.3
-			offset = OFFSET_ANCHOR
-		}
-
-		translate = 1280, 0, 0
-		rotate = 0, 0, 1, -45
-	}
-
-	node water
-	{
-		// Sprite drawable set in code because Effect isn't supported
-		translate = 0, -50, 0
-	}
-
-	node text
-	{
-		text
-		{
-			font = res/ui/arial.gpb
-
-			text = P1
-			size = 18
-			color = 0, 0, 1, 1
-		}
-	}
-
-	// Set active camera
-	activeCamera = camera
+    // Width and height are expected to be 1280x720
+    node camera
+    {
+        camera
+        {
+            type = ORTHOGRAPHIC
+            nearPlane = 0
+            farPlane = 100
+
+            // zoomX default is game width
+            // zoomY default is game height
+            // aspectRatio default is game width / game height
+        }
+        // width and height are divided in half
+        translate = 640, 360, 0
+    }
+
+    // Background sprite
+    node background
+    {
+        sprite
+        {
+            path = res/common/sprites/background.png
+
+            // game width * 5
+            width = 6400
+            height = 720
+        }
+    }
+
+    // Level floor
+    node floor
+    {
+        tileset
+        {
+            path = res/common/sprites/level.png
+
+            tileWidth = 70
+            tileHeight = 70
+            rows = 3
+            columns = 7
+
+            tile
+            {
+                cell = 0, 0
+                source = 568, 284
+            }
+            tile
+            {
+                cell = 1, 0
+                source = 568, 284
+            }
+            tile
+            {
+                cell = 2, 0
+                source = 568, 284
+            }
+            tile
+            {
+                cell = 3, 0
+                source = 568, 284
+            }
+            tile
+            {
+                cell = 4, 0
+                source = 497, 284
+            }
+
+            tile
+            {
+                cell = 0, 1
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 1, 1
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 2, 1
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 3, 1
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 4, 1
+                source = 710, 142
+            }
+            tile
+            {
+                cell = 5, 1
+                source = 497, 284
+            }
+
+            tile
+            {
+                cell = 0, 2
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 1, 2
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 2, 2
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 3, 2
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 4, 2
+                source = 568, 0
+            }
+            tile
+            {
+                cell = 5, 2
+                source = 710, 142
+            }
+            tile
+            {
+                cell = 6, 2
+                source = 497, 284
+            }
+        }
+    }
+
+    node player
+    {
+        node text
+        {
+            text
+            {
+                font = res/ui/arial.gpb
+
+                text = P1
+                size = 18
+                color = 0, 0, 1, 1
+            }
+        }
+
+        sprite
+        {
+            path = res/common/sprites/player1.png
+
+            width = 72
+            height = 97
+            source = 67, 196, 66, 92
+            frameCount = 13
+        }
+
+        // Position player at lower-left. Y position is floor's tileset height (tileHeight * rows)
+        translate = 0, 210, 0
+    }
+
+    node rocket
+    {
+        sprite
+        {
+            path = res/common/sprites/rocket.png
+
+            width = 128
+            height = 128
+            blendMode = BLEND_ADDITIVE
+            anchor = 0.5, 0.3
+            offset = OFFSET_ANCHOR
+        }
+
+        translate = 1280, 0, 0
+        rotate = 0, 0, 1, -45
+    }
+
+    node water
+    {
+        // Sprite drawable set in code because Effect isn't supported
+        translate = 0, -50, 0
+    }
+
+    // Set active camera
+    activeCamera = camera
 }

+ 1 - 6
samples/browser/src/SpriteSample.cpp

@@ -51,16 +51,11 @@ void SpriteSample::initialize()
     _playerAnimation->play("idle");
 
     // Setup player text
-    Node* playerTextNode = _scene->findNode("text");
-    playerTextNode->addRef();
-    _scene->removeNode(playerTextNode); //XXX This is because SceneLoader doesn't support loading child nodes for other nodes
-    _playerNode->addChild(playerTextNode);
-
+    Node* playerTextNode = _playerNode->findNode("text");
     playerTextNode->translateY(_playerSprite->getHeight());
     Text* playerText = dynamic_cast<Text*>(playerTextNode->getDrawable());
     playerText->setJustify(Font::ALIGN_TOP_HCENTER);
     playerText->setWidth(_playerSprite->getWidth());
-    SAFE_RELEASE(playerTextNode);
 
     // Custom Effect in sprite
     Effect* waterEffect = Effect::createFromFile("res/shaders/sprite.vert", "res/common/sprites/water2d.frag");

+ 4 - 0
tools/encoder/gameplay-encoder.vcxproj

@@ -50,6 +50,8 @@
     <ClCompile Include="src\Sampler.cpp" />
     <ClCompile Include="src\Scene.cpp" />
     <ClCompile Include="src\StringUtil.cpp" />
+    <ClCompile Include="src\TMXSceneEncoder.cpp" />
+    <ClCompile Include="src\TMXTypes.cpp" />
     <ClCompile Include="src\Transform.cpp" />
     <ClCompile Include="src\TTFFontEncoder.cpp" />
     <ClCompile Include="src\Vector2.cpp" />
@@ -98,6 +100,8 @@
     <ClInclude Include="src\Scene.h" />
     <ClInclude Include="src\StringUtil.h" />
     <ClInclude Include="src\Thread.h" />
+    <ClInclude Include="src\TMXSceneEncoder.h" />
+    <ClInclude Include="src\TMXTypes.h" />
     <ClInclude Include="src\Transform.h" />
     <ClInclude Include="src\TTFFontEncoder.h" />
     <ClInclude Include="src\Vector2.h" />

+ 12 - 0
tools/encoder/gameplay-encoder.vcxproj.filters

@@ -139,6 +139,12 @@
     <ClCompile Include="src\edtaa3func.c">
       <Filter>src</Filter>
     </ClCompile>
+    <ClCompile Include="src\TMXSceneEncoder.cpp">
+      <Filter>src</Filter>
+    </ClCompile>
+    <ClCompile Include="src\TMXTypes.cpp">
+      <Filter>src</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="src\VertexElement.h">
@@ -279,6 +285,12 @@
     <ClInclude Include="src\edtaa3func.h">
       <Filter>src</Filter>
     </ClInclude>
+    <ClInclude Include="src\TMXSceneEncoder.h">
+      <Filter>src</Filter>
+    </ClInclude>
+    <ClInclude Include="src\TMXTypes.h">
+      <Filter>src</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <None Include="src\Vector2.inl">

+ 122 - 0
tools/encoder/src/Base.cpp

@@ -23,4 +23,126 @@ std::string getBaseName(const std::string& filepath)
     return output;
 }
 
+/*
+base64.cpp and base64.h (combined into base.h/base.cpp)
+
+Copyright (C) 2004-2008 René Nyffenegger
+
+This source code is provided 'as-is', without any express or implied
+warranty. In no event will the author be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this source code must not be misrepresented; you must not
+claim that you wrote the original source code. If you use this source code
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original source code.
+
+3. This notice may not be removed or altered from any source distribution.
+
+René Nyffenegger [email protected]
+
+*/
+
+static const std::string base64_chars =
+"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+"abcdefghijklmnopqrstuvwxyz"
+"0123456789+/";
+
+
+static inline bool is_base64(unsigned char c) {
+    return (isalnum(c) || (c == '+') || (c == '/'));
+}
+
+std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_len) {
+    std::string ret;
+    int i = 0;
+    int j = 0;
+    unsigned char char_array_3[3];
+    unsigned char char_array_4[4];
+
+    while (in_len--) {
+        char_array_3[i++] = *(bytes_to_encode++);
+        if (i == 3) {
+            char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+            char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
+            char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
+            char_array_4[3] = char_array_3[2] & 0x3f;
+
+            for (i = 0; (i <4); i++)
+                ret += base64_chars[char_array_4[i]];
+            i = 0;
+        }
+    }
+
+    if (i)
+    {
+        for (j = i; j < 3; j++)
+            char_array_3[j] = '\0';
+
+        char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
+        char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
+        char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
+        char_array_4[3] = char_array_3[2] & 0x3f;
+
+        for (j = 0; (j < i + 1); j++)
+            ret += base64_chars[char_array_4[j]];
+
+        while ((i++ < 3))
+            ret += '=';
+
+    }
+
+    return ret;
+
+}
+
+std::string base64_decode(std::string const& encoded_string) {
+    int in_len = (int)encoded_string.size(); //XXX Added cast
+    int i = 0;
+    int j = 0;
+    int in_ = 0;
+    unsigned char char_array_4[4], char_array_3[3];
+    std::string ret;
+
+    while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
+        char_array_4[i++] = encoded_string[in_]; in_++;
+        if (i == 4) {
+            for (i = 0; i <4; i++)
+                char_array_4[i] = (unsigned char)base64_chars.find(char_array_4[i]); //XXX Added cast
+
+            char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
+            char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
+            char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
+
+            for (i = 0; (i < 3); i++)
+                ret += char_array_3[i];
+            i = 0;
+        }
+    }
+
+    if (i) {
+        for (j = i; j <4; j++)
+            char_array_4[j] = 0;
+
+        for (j = 0; j <4; j++)
+            char_array_4[j] = (unsigned char)base64_chars.find(char_array_4[j]); //XXX Added cast
+
+        char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
+        char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
+        char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
+
+        for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
+    }
+
+    return ret;
+}
+
+
 }

+ 3 - 0
tools/encoder/src/Base.h

@@ -101,6 +101,9 @@ void fillArray(float values[], float value, size_t length);
  */
 std::string getBaseName(const std::string& filepath);
 
+std::string base64_encode(unsigned char const*, unsigned int len);
+std::string base64_decode(std::string const& s);
+
 #define ISZERO(x) (fabs(x) < MATH_EPSILON)
 #define ISONE(x) ((x - 1.0f) < MATH_EPSILON)
 

+ 38 - 5
tools/encoder/src/EncoderArguments.cpp

@@ -28,7 +28,8 @@ EncoderArguments::EncoderArguments(size_t argc, const char** argv) :
     _textOutput(false),
     _optimizeAnimations(false),
     _animationGrouping(ANIMATIONGROUP_PROMPT),
-    _outputMaterial(false)
+    _outputMaterial(false),
+    _generateTextureGutter(false)
 {
     __instance = this;
 
@@ -81,11 +82,23 @@ const std::string& EncoderArguments::getFilePath() const
     return _filePath;
 }
 
+const std::string EncoderArguments::getFileDirPath() const
+{
+    int pos = _filePath.find_last_of('/');
+    return (pos == -1 ? _filePath : _filePath.substr(0, pos));
+}
+
 const char* EncoderArguments::getFilePathPointer() const
 {
     return _filePath.c_str();
 }
 
+const std::string EncoderArguments::getFileName() const
+{
+    int pos = _filePath.find_last_of('/');
+    return (pos == -1 ? _filePath : _filePath.substr(pos + 1));
+}
+
 std::string EncoderArguments::getOutputDirPath() const
 {
     if (_fileOutputPath.size() > 0)
@@ -104,6 +117,8 @@ std::string EncoderArguments::getOutputFileExtension() const
 {
     switch (getFileFormat())
     {
+    case FILEFORMAT_TMX:
+        return ".scene";
     case FILEFORMAT_PNG:
     case FILEFORMAT_RAW:
         if (_normalMap)
@@ -271,6 +286,11 @@ void EncoderArguments::printUsage() const
         "\t\tMultiple -h arguments can be supplied to generate more than one \n" \
         "\t\theightmap. For 24-bit packed height data use -hp instead of -h.\n" \
     "\n" \
+    "TMX file options:\n" \
+    "  -tg\tEnable texture gutter's around tiles. This will modify any referenced\n" \
+    "  \ttile sets to add a 1px border around it to prevent seams.\n"
+    "  -tg:none\tDo not priduce a texture gutter.\n"
+    "\n" \
     "Normal map options:\n" \
         "  -n\t\tGenerate normal map (requires input file of type PNG or RAW)\n" \
         "  -s\t\tSize/resolution of the input heightmap image (required for RAW files)\n" \
@@ -319,6 +339,11 @@ bool EncoderArguments::outputMaterialEnabled() const
     return _outputMaterial;
 }
 
+bool EncoderArguments::generateTextureGutter() const
+{
+    return _generateTextureGutter;
+}
+
 const char* EncoderArguments::getNodeId() const
 {
     if (_nodeId.length() == 0)
@@ -350,14 +375,14 @@ EncoderArguments::FileFormat EncoderArguments::getFileFormat() const
         ext[i] = (char)tolower(ext[i]);
     
     // Match every supported extension with its format constant
-    if (ext.compare("dae") == 0)
-    {
-        return FILEFORMAT_DAE;
-    }
     if (ext.compare("fbx") == 0)
     {
         return FILEFORMAT_FBX;
     }
+    if (ext.compare("tmx") == 0)
+    {
+        return FILEFORMAT_TMX;
+    }
     if (ext.compare("ttf") == 0)
     {
         return FILEFORMAT_TTF;
@@ -652,6 +677,14 @@ void EncoderArguments::readOption(const std::vector<std::string>& options, size_
                 _tangentBinormalId.insert(nodeId);
             }
         }
+        else if (str.compare("-textureGutter:none") == 0 || str.compare("-tg:none") == 0)
+        {
+            _generateTextureGutter = false;
+        }
+        else if (str.compare("-textureGutter") == 0 || str.compare("-tg") == 0)
+        {
+            _generateTextureGutter = true;
+        }
         break;
     case 'v':
         (*index)++;

+ 15 - 1
tools/encoder/src/EncoderArguments.h

@@ -18,8 +18,8 @@ public:
     enum FileFormat
     {
         FILEFORMAT_UNKNOWN,
-        FILEFORMAT_DAE,
         FILEFORMAT_FBX,
+        FILEFORMAT_TMX,
         FILEFORMAT_TTF,
         FILEFORMAT_GPB,
         FILEFORMAT_PNG,
@@ -74,11 +74,22 @@ public:
      */
     const std::string& getFilePath() const;
 
+    /**
+     * Returns the path/folder.
+     * Example: "C:/dir"
+     */
+    const std::string getFileDirPath() const;
+
     /**
      * Returns the char pointer to the file path string.
      */
     const char* getFilePathPointer() const;
 
+    /**
+     * Get the file name of the input file.
+     */
+    const std::string getFileName() const;
+
     /**
      * Returns the output path/folder.
      * Example: "C:/dir"
@@ -164,6 +175,8 @@ public:
 
     bool outputMaterialEnabled() const;
 
+    bool generateTextureGutter() const;
+
     const char* getNodeId() const;
 
     static std::string getRealPath(const std::string& filepath);
@@ -210,6 +223,7 @@ private:
     bool _optimizeAnimations;
     AnimationGroupOption _animationGrouping;
     bool _outputMaterial;
+    bool _generateTextureGutter;
 
     std::vector<std::string> _groupAnimationNodeId;
     std::vector<std::string> _groupAnimationAnimationId;

+ 1000 - 0
tools/encoder/src/TMXSceneEncoder.cpp

@@ -0,0 +1,1000 @@
+#include <string>
+
+#include <zlib.h>
+
+#include "TMXSceneEncoder.h"
+
+using namespace gameplay;
+using namespace tinyxml2;
+using std::string;
+
+#ifdef GP_4_SPACE_TABS
+#define TAB_STRING(count) string((count) * 4, ' ')
+#else
+#define TAB_STRING(count) string((count), '\t')
+#endif
+
+#define BUFFER_SIZE 256
+
+#ifdef WIN32
+#define snprintf(s, n, fmt, ...) sprintf((s), (fmt), __VA_ARGS__)
+#endif
+
+TMXSceneEncoder::TMXSceneEncoder() :
+    _tabCount(0)
+{
+}
+
+TMXSceneEncoder::~TMXSceneEncoder()
+{
+}
+
+void TMXSceneEncoder::write(const EncoderArguments& arguments)
+{
+    XMLDocument xmlDoc;
+    XMLError err;
+    if ((err = xmlDoc.LoadFile(arguments.getFilePath().c_str())) != XML_NO_ERROR)
+    {
+        LOG(1, "Call to XMLDocument::LoadFile() failed.\n");
+        LOG(1, "Error returned: %d\n\n", err);
+        return;
+    }
+    
+    // Parse the Tiled map
+    TMXMap map;
+    string inputDirectory = arguments.getFileDirPath();
+
+    LOG(2, "Parsing .tmx file.\n");
+    if (!parseTmx(xmlDoc, map, inputDirectory))
+    {
+        return;
+    }
+
+    // Apply a gutter, or skirt, around the tiles to prevent gaps
+    if (arguments.generateTextureGutter())
+    {
+        LOG(2, "Bulding gutter tilesets.\n");
+        buildTileGutter(map, inputDirectory, arguments.getOutputDirPath());
+    }
+
+    // Write the tile map
+    string fileName = arguments.getFileName();
+    int pos = fileName.find_last_of('.');
+
+    LOG(2, "Writing .scene file.\n");
+    writeScene(map, arguments.getOutputFilePath(), (pos == -1 ? fileName : fileName.substr(0, pos)));
+}
+
+bool TMXSceneEncoder::parseTmx(const XMLDocument& xmlDoc, TMXMap& map, const string& inputDirectory) const
+{
+    const XMLElement* xmlMap = xmlDoc.FirstChildElement("map");
+    if (!xmlMap)
+    {
+        LOG(1, "Missing root <map> element.\n");
+        return false;
+    }
+
+    // Read in the map values //XXX should compact this so XML attribute parsing is a little nicer
+    unsigned int uiValue;
+    int iValue;
+    const char* attValue = xmlMap->Attribute("width");
+    sscanf(attValue, "%u", &uiValue);
+    map.setWidth(uiValue);
+
+    attValue = xmlMap->Attribute("height");
+    sscanf(attValue, "%u", &uiValue);
+    map.setHeight(uiValue);
+
+    float fValue;
+    attValue = xmlMap->Attribute("tilewidth");
+    sscanf(attValue, "%f", &fValue);
+    map.setTileWidth(fValue);
+
+    attValue = xmlMap->Attribute("tileheight");
+    sscanf(attValue, "%f", &fValue);
+    map.setTileHeight(fValue);
+
+    // Now we load all tilesets
+    const XMLElement* xmlTileSet = xmlMap->FirstChildElement("tileset");
+    while (xmlTileSet)
+    {
+        TMXTileSet tileSet;
+
+        attValue = xmlTileSet->Attribute("firstgid");
+        sscanf(attValue, "%u", &uiValue);
+        tileSet.setFirstGid(uiValue);
+
+        XMLDocument sourceXmlDoc;
+        const XMLElement* xmlTileSetToLoad;
+        attValue = xmlTileSet->Attribute("source");
+        if (attValue)
+        {
+            XMLError err;
+            string tsxLocation = buildFilePath(inputDirectory, attValue);
+            if ((err = sourceXmlDoc.LoadFile(tsxLocation.c_str())) != XML_NO_ERROR)
+            {
+                LOG(1, "Could not load tileset's source TSX.\n");
+                return false;
+            }
+            xmlTileSetToLoad = sourceXmlDoc.RootElement();
+        }
+        else
+        {
+            xmlTileSetToLoad = xmlTileSet;
+        }
+
+        // Maximum tile size
+        attValue = xmlTileSetToLoad->Attribute("tilewidth");
+        if (attValue)
+        {
+            sscanf(attValue, "%u", &uiValue);
+            tileSet.setMaxTileWidth(uiValue);
+        }
+        else
+        {
+            tileSet.setMaxTileWidth(map.getTileWidth());
+        }
+        attValue = xmlTileSetToLoad->Attribute("tileheight");
+        if (attValue)
+        {
+            sscanf(attValue, "%u", &uiValue);
+            tileSet.setMaxTileHeight(uiValue);
+        }
+        else
+        {
+            tileSet.setMaxTileHeight(map.getTileHeight());
+        }
+
+        // Spacing and margin
+        attValue = xmlTileSetToLoad->Attribute("spacing");
+        if (attValue)
+        {
+            sscanf(attValue, "%u", &uiValue);
+            tileSet.setSpacing(uiValue);
+        }
+        attValue = xmlTileSetToLoad->Attribute("margin");
+        if (attValue)
+        {
+            sscanf(attValue, "%u", &uiValue);
+            tileSet.setMargin(uiValue);
+        }
+
+        // Tile offset
+        const XMLElement* xmlTileOffset = xmlTileSetToLoad->FirstChildElement("tileoffset");
+        if (xmlTileOffset)
+        {
+            Vector2 offset;
+
+            attValue = xmlTileOffset->Attribute("x");
+            sscanf(attValue, "%d", &iValue);
+            offset.x = iValue;
+            attValue = xmlTileOffset->Attribute("y");
+            sscanf(attValue, "%d", &iValue);
+            offset.y = iValue;
+
+            tileSet.setOffset(offset);
+        }
+
+        // Load image source. Don't worry about <data> or trans
+        const XMLElement* xmlTileSetImage = xmlTileSetToLoad->FirstChildElement("image");
+        if (!xmlTileSetImage)
+        {
+            LOG(1, "Could not find <image> element for tileset.\n");
+            return false;
+        }
+        tileSet.setImagePath(xmlTileSetImage->Attribute("source"));
+
+        if (xmlTileSetImage->Attribute("width") && xmlTileSetImage->Attribute("height"))
+        {
+            attValue = xmlTileSetImage->Attribute("width");
+            sscanf(attValue, "%u", &uiValue);
+            tileSet.setImageWidth(uiValue);
+
+            attValue = xmlTileSetImage->Attribute("height");
+            sscanf(attValue, "%u", &uiValue);
+            tileSet.setImageHeight(uiValue);
+        }
+        else
+        {
+            // Load the image itself to get the width
+            string imageLocation = buildFilePath(inputDirectory, tileSet.getImagePath());
+            int pos = imageLocation.find_last_of('.');
+            if (imageLocation.substr(pos).compare(".png") != 0)
+            {
+                LOG(1, "TileSet image must be a PNG. %s\n", imageLocation.c_str());
+                return false;
+            }
+
+            Image* img = Image::create(imageLocation.c_str());
+            if (!img)
+            {
+                LOG(1, "Could not load TileSet image. %s\n", imageLocation.c_str());
+                return false;
+            }
+            tileSet.setImageWidth(img->getWidth());
+            tileSet.setImageHeight(img->getHeight());
+            SAFE_DELETE(img);
+        }
+
+        //TODO: tiles (specifically, tile animations) if possible. May require marking tiles as special tiles, and spinning them off to Sprites
+        //<tile id="relId"><animation><frame tileid="relId" duration="numInMS"></...></...></...>
+
+        // Save the tileset
+        map.addTileSet(tileSet);
+
+        xmlTileSet = xmlTileSet->NextSiblingElement("tileset");
+    }
+
+    // Load the layers
+    const XMLElement* xmlLayer = xmlMap->FirstChildElement("layer");
+    while (xmlLayer)
+    {
+        TMXLayer* layer = new TMXLayer();
+
+        parseBaseLayerProperties(xmlLayer, layer);
+
+        // Load properties
+        attValue = xmlLayer->Attribute("width");
+        if (attValue)
+        {
+            sscanf(attValue, "%u", &uiValue);
+            layer->setWidth(uiValue);
+        }
+        else
+        {
+            layer->setWidth(map.getWidth());
+        }
+
+        attValue = xmlLayer->Attribute("height");
+        if (attValue)
+        {
+            sscanf(attValue, "%u", &uiValue);
+            layer->setHeight(uiValue);
+        }
+        else
+        {
+            layer->setHeight(map.getHeight());
+        }
+
+        // Load tiles
+        layer->setupTiles();
+        auto data = loadDataElement(xmlLayer->FirstChildElement("data"));
+        size_t dataSize = data.size();
+        for (int i = 0; i < dataSize; i++)
+        {
+            //XXX this might depend on map's renderorder... not sure
+            unsigned int x = i % layer->getWidth();
+            unsigned int y = i / layer->getWidth();
+            layer->setTile(x, y, data[i]);
+        }
+
+        // Save layer
+        map.addLayer(layer);
+
+        xmlLayer = xmlLayer->NextSiblingElement("layer");
+    }
+
+    // Load image layers
+    const XMLElement* xmlImgLayer = xmlMap->FirstChildElement("imagelayer");
+    while (xmlImgLayer)
+    {
+        TMXImageLayer* imgLayer = new TMXImageLayer();
+
+        parseBaseLayerProperties(xmlImgLayer, imgLayer);
+
+        // Load properties
+        attValue = xmlImgLayer->Attribute("x");
+        if (attValue)
+        {
+            sscanf(attValue, "%d", &iValue);
+            imgLayer->setX(iValue);
+        }
+        attValue = xmlImgLayer->Attribute("y");
+        if (attValue)
+        {
+            sscanf(attValue, "%d", &iValue);
+            imgLayer->setY(iValue);
+        }
+
+        // Load image source. Don't worry about <data>, trans, width, or height
+        const XMLElement* xmlImage = xmlImgLayer->FirstChildElement("image");
+        if (!xmlImage)
+        {
+            LOG(1, "Could not find <image> element for the image layer.\n");
+            return false;
+        }
+        imgLayer->setImagePath(xmlImage->Attribute("source"));
+
+        // Save image layer
+        map.addLayer(imgLayer);
+
+        xmlImgLayer = xmlImgLayer->NextSiblingElement("imagelayer");
+    }
+
+    return true;
+}
+
+void TMXSceneEncoder::parseBaseLayerProperties(const tinyxml2::XMLElement* xmlBaseLayer, gameplay::TMXBaseLayer* layer) const
+{
+    layer->setName(xmlBaseLayer->Attribute("name"));
+
+    float fValue;
+    unsigned int uiValue;
+
+    const char* attValue = xmlBaseLayer->Attribute("opacity");
+    if (attValue)
+    {
+        sscanf(attValue, "%f", &fValue);
+        layer->setOpacity(fValue);
+    }
+
+    attValue = xmlBaseLayer->Attribute("visible");
+    if (attValue)
+    {
+        sscanf(attValue, "%u", &uiValue);
+        layer->setVisible(uiValue == 1);
+    }
+
+    const XMLElement* xmlProperties = xmlBaseLayer->FirstChildElement("properties");
+    if (xmlProperties)
+    {
+        TMXProperties& properties = layer->getProperties();
+        const XMLElement* xmlProperty = xmlProperties->FirstChildElement("property");
+        while (xmlProperty)
+        {
+            properties[xmlProperty->Attribute("name")] = xmlProperty->Attribute("value");
+
+            xmlProperty = xmlProperty->NextSiblingElement("property");
+        }
+    }
+}
+
+void TMXSceneEncoder::buildTileGutter(TMXMap& map, const string& inputDirectory, const string& outputDirectory)
+{
+#define ACTUAL_ADJUST_TILESET(imgPath, spacing, margin) tileset.setImagePath((imgPath)); \
+    tileset.setSpacing(tileset.getSpacing() + (spacing), false); \
+    tileset.setMargin(tileset.getMargin() + (margin), false); \
+    tileset.setImageWidth(TMXTileSet::calculateImageDimension(tileset.getHorizontalTileCount(), tileset.getMaxTileWidth(), tileset.getSpacing(), tileset.getMargin())); \
+    tileset.setImageHeight(TMXTileSet::calculateImageDimension(tileset.getVerticalTileCount(), tileset.getMaxTileHeight(), tileset.getSpacing(), tileset.getMargin()))
+#define ADJUST_TILESET(imgPath) ACTUAL_ADJUST_TILESET(imgPath, 2, 1)
+#define UNDO_ADJUST_TILESET(imgPath) ACTUAL_ADJUST_TILESET(imgPath, -2, -1)
+
+    std::unordered_map<std::string, std::string> processedFiles;
+    std::unordered_map<std::string, std::string> reverseLookupProcessedFiles;
+
+    unsigned int tilesetCount = map.getTileSetCount();
+    for (unsigned int i = 0; i < tilesetCount; i++)
+    {
+        TMXTileSet& tileset = map.getTileSet(i);
+
+        // See if the tileset was already processed. Then we can skip it
+        auto potentialEasyProcess = processedFiles.find(tileset.getImagePath());
+        if (potentialEasyProcess != processedFiles.end())
+        {
+            ADJUST_TILESET(potentialEasyProcess->second);
+            continue;
+        }
+
+        // Process tileset
+        string orgImgPath = tileset.getImagePath();
+        string imgPath = orgImgPath;
+        string inputFile = buildFilePath(inputDirectory, imgPath);
+        int pos = imgPath.find_last_of('.');
+        if (pos == -1)
+        {
+            imgPath = imgPath + "_guttered";
+        }
+        else
+        {
+            imgPath = imgPath.substr(0, pos) + "_guttered" + imgPath.substr(pos);
+        }
+        string outputFile = buildFilePath(outputDirectory, imgPath);
+
+        if (buildTileGutterTileset(tileset, inputFile, outputFile))
+        {
+            // Update tileset
+            ADJUST_TILESET(imgPath);
+
+            // Remember image paths
+            processedFiles[orgImgPath] = imgPath;
+            reverseLookupProcessedFiles[imgPath] = orgImgPath;
+        }
+        else
+        {
+            // Revert all processed tilemaps
+            for (unsigned int k = 0; k < i; k++)
+            {
+                tileset = map.getTileSet(k);
+
+                // Revert to original image and settings
+                orgImgPath = reverseLookupProcessedFiles[tileset.getImagePath()];
+                UNDO_ADJUST_TILESET(orgImgPath);
+
+                // Delete the processed image, if they exist
+                auto it = processedFiles.find(orgImgPath);
+                if (it != processedFiles.end())
+                {
+                    outputFile = buildFilePath(outputDirectory, it->second);
+                    if (remove(outputFile.c_str()) != 0)
+                    {
+                        LOG(3, "Could not remove '%s' during tileset revert.\n", outputFile.c_str());
+                    }
+                    processedFiles.erase(it);
+                }
+            }
+            LOG(1, "Failed to process '%s'. Reverting all tileset gutters.\n", orgImgPath.c_str());
+            return;
+        }
+    }
+
+#undef UNDO_ADJUST_TILESET
+#undef ADJUST_TILESET
+#undef ACTUAL_ADJUST_TILESET
+}
+
+bool TMXSceneEncoder::buildTileGutterTileset(const TMXTileSet& tileset, const string& inputFile, const string& outputFile)
+{
+#define COPY_CONTENTS(sx, sy, dx, dy, w, h) copyImage(outputData, static_cast<unsigned char*>(inputImage->getData()), \
+    inputImageWidth, outputImageWidth, bpp, \
+    (sx), (sy), (dx), (dy), (w), (h))
+
+    // Setup images
+    const Image* inputImage = Image::create(inputFile.c_str());
+    if (!inputImage)
+    {
+        return false;
+    }
+
+    unsigned int tilesetWidth = tileset.getHorizontalTileCount();
+    unsigned int tilesetHeight = tileset.getVerticalTileCount();
+    Vector2 tileSize = Vector2(tileset.getMaxTileWidth(), tileset.getMaxTileHeight());
+    unsigned int bpp = 0;
+    switch (inputImage->getFormat())
+    {
+        case Image::LUMINANCE:
+            bpp = 1;
+            break;
+        case Image::RGB:
+            bpp = 3;
+            break;
+        case Image::RGBA:
+            bpp = 4;
+            break;
+        default:
+            LOG(4, "Unknown image format. Possibly need update by developer.\n")
+            return false;
+    }
+
+    Image* outputImage = Image::create(inputImage->getFormat(),
+        TMXTileSet::calculateImageDimension(tilesetWidth, tileSize.x, tileset.getSpacing() + 2, tileset.getMargin() + 1),
+        TMXTileSet::calculateImageDimension(tilesetHeight, tileSize.y, tileset.getSpacing() + 2, tileset.getMargin() + 1));
+
+    // Get a couple variables so we don't constantly call functions (they aren't inline)
+    unsigned int inputImageWidth = inputImage->getWidth();
+    unsigned int inputImageHeight = inputImage->getHeight();
+    unsigned int outputImageWidth = outputImage->getWidth();
+    unsigned int outputImageHeight = outputImage->getHeight();
+    unsigned char* outputData = static_cast<unsigned char*>(outputImage->getData());
+    unsigned int tilesetSpacing = tileset.getSpacing();
+    unsigned int tilesetMargin = tileset.getMargin();
+
+    // Copy margin
+    if (tilesetMargin != 0)
+    {
+        // Horizontal
+        unsigned int avaliable = inputImageWidth;
+        unsigned int remaining = outputImageWidth;
+        unsigned int use = min(avaliable, remaining);
+
+        while (remaining > 0)
+        {
+            // Top
+            COPY_CONTENTS(0, 0, outputImageWidth - remaining, 0, use, tilesetMargin);
+
+            // Bottom
+            COPY_CONTENTS(0, inputImageHeight - tilesetMargin - 1, outputImageWidth - remaining, outputImageHeight - tilesetMargin - 1, use, tilesetMargin);
+
+            remaining -= use;
+            use = min(avaliable, remaining);
+        }
+
+        // Vertical
+        avaliable = inputImageHeight;
+        remaining = outputImageHeight;
+        use = min(avaliable, remaining);
+
+        while (remaining > 0)
+        {
+            // Left
+            COPY_CONTENTS(0, 0, 0, outputImageHeight - remaining, tilesetMargin, use);
+
+            // Right
+            COPY_CONTENTS(inputImageWidth - tilesetMargin - 1, 0, inputImageWidth - tilesetMargin - 1, outputImageHeight - remaining, tilesetMargin, use);
+
+            remaining -= use;
+            use = min(avaliable, remaining);
+        }
+    }
+
+    // Copy the contents of each tile
+    for (unsigned int y = 0; y < tilesetHeight; y++)
+    {
+        for (unsigned int x = 0; x < tilesetWidth; x++)
+        {
+            Vector2 src = TMXTileSet::calculateTileOrigin(Vector2(x, y), tileSize, tilesetSpacing, tilesetMargin);
+            Vector2 dst = TMXTileSet::calculateTileOrigin(Vector2(x, y), tileSize, tilesetSpacing + 2, tilesetMargin + 1);
+
+            COPY_CONTENTS(src.x, src.y, dst.x, dst.y, tileSize.x, tileSize.y);
+        }
+    }
+    
+    // Extend the edges of the tiles to produce the "gutter"
+    unsigned int gutterContents = outputImageHeight - ((tilesetMargin + 1) * 2);
+    for (unsigned int x = 0; x < tilesetWidth; x++)
+    {
+        Vector2 pos = TMXTileSet::calculateTileOrigin(Vector2(x, 0), tileSize, tilesetSpacing + 2, tilesetMargin + 1);
+        
+        // Left
+        copyImage(outputData, outputData,
+            outputImageWidth, outputImageWidth, bpp,
+            pos.x, pos.y, pos.x - 1, pos.y, 1, gutterContents);
+
+        // Right
+        copyImage(outputData, outputData,
+            outputImageWidth, outputImageWidth, bpp,
+            pos.x + tileSize.x - 1, pos.y, pos.x + tileSize.x, pos.y, 1, gutterContents);
+    }
+    gutterContents = outputImageWidth - ((tilesetMargin + 1) * 2);
+    for (unsigned int y = 0; y < tilesetHeight; y++)
+    {
+        Vector2 pos = TMXTileSet::calculateTileOrigin(Vector2(0, y), tileSize, tilesetSpacing + 2, tilesetMargin + 1);
+
+        // Top
+        copyImage(outputData, outputData,
+            outputImageWidth, outputImageWidth, bpp,
+            pos.x, pos.y, pos.x, pos.y - 1, gutterContents, 1);
+
+        // Bottom
+        copyImage(outputData, outputData,
+            outputImageWidth, outputImageWidth, bpp,
+            pos.x, pos.y + tileSize.y - 1, pos.x, pos.y + tileSize.y, gutterContents, 1);
+    }
+
+    // Save and cleanup
+    outputImage->save(outputFile.c_str());
+    SAFE_DELETE(inputImage);
+    SAFE_DELETE(outputImage);
+
+    return true;
+
+#undef COPY_CONTENTS
+}
+
+//XXX could probably seperate the writing process to a seperate class (PropertyWriter...)
+#define WRITE_PROPERTY_BLOCK_START(str) writeLine(file, (str)); \
+    writeLine(file, "{"); \
+    _tabCount++
+#define WRITE_PROPERTY_BLOCK_END() _tabCount--; \
+    writeLine(file, "}")
+#define WRITE_PROPERTY_BLOCK_VALUE(name, value) writeLine(file, string(name) + " = " + (value))
+#define WRITE_PROPERTY_DIRECT(value) writeLine(file, (value))
+#define WRITE_PROPERTY_NEWLINE() file << std::endl
+
+void TMXSceneEncoder::writeScene(const TMXMap& map, const string& outputFilepath, const string& sceneName)
+{
+    // Prepare for writing the scene
+    std::ofstream file(outputFilepath.c_str(), std::ofstream::out | std::ofstream::trunc);
+
+    unsigned int layerCount = map.getLayerCount();
+
+    // Write initial scene
+    WRITE_PROPERTY_BLOCK_START("scene " + sceneName);
+
+    // Write all layers
+    for (unsigned int i = 0; i < layerCount; i++)
+    {
+        const TMXBaseLayer* layer = map.getLayer(i);
+        TMXLayerType type = layer->getType();
+        if (type == TMXLayerType::NormalLayer)
+        {
+            writeTileset(map, dynamic_cast<const TMXLayer*>(layer), file);
+            WRITE_PROPERTY_NEWLINE();
+        }
+        else if (type == TMXLayerType::ImageLayer)
+        {
+            writeSprite(dynamic_cast<const TMXImageLayer*>(layer), file);
+            WRITE_PROPERTY_NEWLINE();
+        }
+    }
+
+    WRITE_PROPERTY_BLOCK_END();
+
+    // Cleanup
+    file.flush();
+    file.close();
+}
+
+// This is actually a misnomer. What is a Layer in Tiled/TMX is a TileSet for GamePlay3d. TileSet in Tiled/TMX is something different.
+void TMXSceneEncoder::writeTileset(const TMXMap& map, const TMXLayer* tileset, std::ofstream& file)
+{
+    if (!tileset || !tileset->hasTiles())
+    {
+        return;
+    }
+
+    std::set<unsigned int> tilesets = tileset->getTilesetsUsed(map);
+    if (tilesets.size() == 0)
+    {
+        return;
+    }
+
+    char buffer[BUFFER_SIZE];
+    WRITE_PROPERTY_BLOCK_START("node " + tileset->getName());
+
+    if (tilesets.size() > 1)
+    {
+        unsigned int i = 0;
+        for (auto it = tilesets.begin(); it != tilesets.end(); it++, i++)
+        {
+            snprintf(buffer, BUFFER_SIZE, "node tileset_%d", i);
+            WRITE_PROPERTY_BLOCK_START(buffer);
+
+            const TMXTileSet& tmxTileset = map.getTileSet(*it);
+            writeSoloTileset(map, tmxTileset, *tileset, file, *it);
+
+            const Vector2& tileOffset = tmxTileset.getOffset();
+            if (!(tileOffset == Vector2::zero()))
+            {
+                // Tile offset moves the tiles, not the origin of each tile
+                snprintf(buffer, BUFFER_SIZE, "translate = %d, %d, 0", static_cast<int>(tileOffset.x), static_cast<int>(tileOffset.y));
+                WRITE_PROPERTY_NEWLINE();
+                WRITE_PROPERTY_DIRECT(buffer);
+            }
+
+            WRITE_PROPERTY_BLOCK_END();
+            if ((i + 1) != tilesets.size())
+            {
+                WRITE_PROPERTY_NEWLINE();
+            }
+        }
+    }
+    else
+    {
+        const TMXTileSet& tmxTileset = map.getTileSet(*(tilesets.begin()));
+        writeSoloTileset(map, tmxTileset, *tileset, file);
+
+        const Vector2& tileOffset = tmxTileset.getOffset();
+        if (!(tileOffset == Vector2::zero()))
+        {
+            // Tile offset moves the tiles, not the origin of each tile
+            snprintf(buffer, BUFFER_SIZE, "translate = %d, %d, 0", static_cast<int>(tileOffset.x), static_cast<int>(tileOffset.y));
+            WRITE_PROPERTY_NEWLINE();
+            WRITE_PROPERTY_DIRECT(buffer);
+        }
+    }
+
+    writeNodeProperties(tileset->getVisible(), tileset->getProperties(), file);
+    WRITE_PROPERTY_BLOCK_END();
+}
+
+void TMXSceneEncoder::writeSoloTileset(const TMXMap& map, const gameplay::TMXTileSet& tmxTileset, const TMXLayer& tileset, std::ofstream& file, unsigned int resultOnlyForTileset)
+{
+    WRITE_PROPERTY_BLOCK_START("tileset");
+
+    // Write tile path
+    WRITE_PROPERTY_BLOCK_VALUE("path", tmxTileset.getImagePath());
+    WRITE_PROPERTY_NEWLINE();
+
+    // Write tile size
+    //XXX if tile sizes are incorrect, make sure to update TMXLayer::getTileStart too
+    char buffer[BUFFER_SIZE];
+    snprintf(buffer, BUFFER_SIZE, "tileWidth = %u", tmxTileset.getMaxTileWidth());
+    WRITE_PROPERTY_DIRECT(buffer);
+    snprintf(buffer, BUFFER_SIZE, "tileHeight = %u", tmxTileset.getMaxTileHeight());
+    WRITE_PROPERTY_DIRECT(buffer);
+
+    // Write tileset size
+    snprintf(buffer, BUFFER_SIZE, "columns = %u", tileset.getWidth());
+    WRITE_PROPERTY_DIRECT(buffer);
+    snprintf(buffer, BUFFER_SIZE, "rows = %u", tileset.getHeight());
+    WRITE_PROPERTY_DIRECT(buffer);
+    WRITE_PROPERTY_NEWLINE();
+
+    // Write opacity
+    if (tileset.getOpacity() < 1.0f)
+    {
+        snprintf(buffer, BUFFER_SIZE, "opacity = %f", tileset.getOpacity());
+        WRITE_PROPERTY_DIRECT(buffer);
+        WRITE_PROPERTY_NEWLINE();
+    }
+
+    // Write tiles
+    unsigned int tilesetHeight = tileset.getHeight();
+    unsigned int tilesetWidth = tileset.getWidth();
+    for (unsigned int y = 0; y < tilesetHeight; y++)
+    {
+        bool tilesWritten = false;
+        for (unsigned int x = 0; x < tilesetWidth; x++)
+        {
+            Vector2 startPos = tileset.getTileStart(x, y, map, resultOnlyForTileset);
+            if (startPos.x < 0 || startPos.y < 0)
+            {
+                continue;
+            }
+
+            tilesWritten = true;
+            WRITE_PROPERTY_BLOCK_START("tile");
+            snprintf(buffer, BUFFER_SIZE, "cell = %u, %u", x, y);
+            WRITE_PROPERTY_DIRECT(buffer);
+            snprintf(buffer, BUFFER_SIZE, "source = %u, %u", static_cast<unsigned int>(startPos.x), static_cast<unsigned int>(startPos.y));
+            WRITE_PROPERTY_DIRECT(buffer);
+            WRITE_PROPERTY_BLOCK_END();
+        }
+        if (tilesWritten && ((y + 1) != tilesetHeight))
+        {
+            WRITE_PROPERTY_NEWLINE();
+        }
+    }
+
+    WRITE_PROPERTY_BLOCK_END();
+}
+
+void TMXSceneEncoder::writeSprite(const gameplay::TMXImageLayer* imageLayer, std::ofstream& file)
+{
+    if (!imageLayer)
+    {
+        return;
+    }
+
+    WRITE_PROPERTY_BLOCK_START("node " + imageLayer->getName());
+
+    // Sprite
+    {
+        WRITE_PROPERTY_BLOCK_START("sprite");
+
+        WRITE_PROPERTY_BLOCK_VALUE("path", imageLayer->getImagePath());
+        WRITE_PROPERTY_NEWLINE();
+
+        WRITE_PROPERTY_BLOCK_END();
+    }
+
+    writeNodeProperties(imageLayer->getVisible(), imageLayer->getPosition(), imageLayer->getProperties(), file);
+    WRITE_PROPERTY_BLOCK_END();
+}
+
+void TMXSceneEncoder::writeNodeProperties(bool enabled, const Vector2& pos, const TMXProperties& properties, std::ofstream& file, bool seperatorLineWritten)
+{
+    char buffer[BUFFER_SIZE];
+    if (!enabled)
+    {
+        // Default is true, so only write it false
+        WRITE_PROPERTY_NEWLINE();
+        seperatorLineWritten = true;
+        WRITE_PROPERTY_DIRECT("enabled = false");
+    }
+    if (!(pos == Vector2::zero()))
+    {
+        if (!seperatorLineWritten)
+        {
+            WRITE_PROPERTY_NEWLINE();
+            seperatorLineWritten = true;
+        }
+        snprintf(buffer, BUFFER_SIZE, "translate = %d, %d, 0", static_cast<int>(pos.x), static_cast<int>(pos.y));
+        WRITE_PROPERTY_DIRECT(buffer);
+    }
+
+    if (properties.size() > 0)
+    {
+        WRITE_PROPERTY_NEWLINE();
+        WRITE_PROPERTY_BLOCK_START("tags");
+        for (auto it = properties.begin(); it != properties.end(); it++)
+        {
+            WRITE_PROPERTY_BLOCK_VALUE(it->first, it->second);
+        }
+        WRITE_PROPERTY_BLOCK_END();
+    }
+}
+
+#undef WRITE_PROPERTY_NEWLINE
+#undef WRITE_PROPERTY_DIRECT
+#undef WRITE_PROPERTY_BLOCK_VALUE
+#undef WRITE_PROPERTY_BLOCK_END
+#undef WRITE_PROPERTY_BLOCK_START
+
+void TMXSceneEncoder::writeLine(std::ofstream& file, const string& line) const
+{
+    file << TAB_STRING(_tabCount) << line << std::endl;
+}
+
+bool isBase64(char c)
+{
+    return (c >= 'a' && c <= 'z') ||
+        (c >= 'A' && c <= 'Z') ||
+        (c >= '0' && c <= '9') ||
+        (c == '=');
+}
+
+std::vector<unsigned int> TMXSceneEncoder::loadDataElement(const XMLElement* data)
+{
+    if (!data)
+    {
+        return std::vector<unsigned int>();
+    }
+
+    const char* encoding = data->Attribute("encoding");
+    const char* compression = data->Attribute("compression");
+
+    const char* attValue = "0";
+    unsigned int tileGid;
+    std::vector<unsigned int> tileData;
+
+    if (!compression && !encoding)
+    {
+        const XMLElement* xmlTile = data->FirstChildElement("tile");
+        while (xmlTile)
+        {
+            attValue = xmlTile->Attribute("gid");
+            sscanf(attValue, "%u", &tileGid);
+            tileData.push_back(tileGid);
+
+            xmlTile = xmlTile->NextSiblingElement("tile");
+        }
+    }
+    else
+    {
+        if (strcmp(encoding, "csv") == 0)
+        {
+            if (compression)
+            {
+                LOG(1, "Compression is not supported with CSV encoding.\n");
+                exit(-1);
+            }
+
+            const char* rawCsvData = data->GetText();
+            int start = 0;
+            char* endptr;
+            // Skip everything before values
+            while (rawCsvData[start] == ' ' || rawCsvData[start] == '\n' || rawCsvData[start] == '\r' || rawCsvData[start] == '\t')
+            {
+                start++;
+            }
+            // Iterate through values. Skipping to next value when done
+            while (rawCsvData[start])
+            {
+                tileData.push_back(strtoul(rawCsvData + start, &endptr, 10));
+                start = endptr - rawCsvData;
+                while (rawCsvData[start] == ' ' || rawCsvData[start] == ',' || rawCsvData[start] == '\n' || rawCsvData[start] == '\r' || rawCsvData[start] == '\t')
+                {
+                    start++;
+                }
+            }
+        }
+        else if (strcmp(encoding, "base64") == 0)
+        {
+            string base64Data = data->GetText();
+            if (base64Data.size() > 0 && (!isBase64(base64Data[0]) || !isBase64(base64Data[base64Data.size() - 1])))
+            {
+                int start = 0;
+                size_t end = base64Data.size() - 1;
+                while (!isBase64(base64Data[start]))
+                {
+                    start++;
+                }
+                while (!isBase64(base64Data[end]))
+                {
+                    end--;
+                }
+                base64Data = base64Data.substr(start, end - start + 1);
+            }
+            string byteData = base64_decode(base64Data);
+
+            if (compression)
+            {
+                int err;
+                const unsigned int buffer_size = 4096;
+                char buffer[buffer_size];
+                string decomData;
+
+                if (strcmp(compression, "zlib") == 0 || strcmp(compression, "gzip") == 0)
+                {
+                    z_stream dString;
+                    dString.zalloc = Z_NULL;
+                    dString.zfree = Z_NULL;
+                    dString.opaque = Z_NULL;
+                    dString.next_in = (Bytef*)byteData.c_str();
+                    dString.avail_in = byteData.size();
+
+                    if (strcmp(compression, "zlib") == 0)
+                    {
+                        // zlib
+                        if ((err = inflateInit(&dString)) != Z_OK)
+                        {
+                            LOG(1, "ZLIB inflateInit failed. Error: %d.\n", err);
+                            exit(-1);
+                        }
+                    }
+                    else
+                    {
+                        // gzip
+                        if ((err = inflateInit2(&dString, 16+MAX_WBITS)) != Z_OK)
+                        {
+                            LOG(1, "ZLIB inflateInit2 failed. Error: %d.\n", err);
+                            exit(-1);
+                        }
+                    }
+
+                    do
+                    {
+                        dString.next_out = (Bytef*)buffer;
+                        dString.avail_out = buffer_size;
+                        err = inflate(&dString, Z_NO_FLUSH);
+                        switch (err)
+                        {
+                            case Z_NEED_DICT:
+                            case Z_DATA_ERROR:
+                            case Z_MEM_ERROR:
+                                inflateEnd(&dString);
+                                LOG(1, "ZLIB inflate failed. Error: %d.\n", err);
+                                exit(-1);
+                                break;
+                        }
+
+                        string decomBlock(buffer, buffer_size - dString.avail_out);
+                        decomData += decomBlock;
+                    } while (err != Z_STREAM_END);
+
+                    inflateEnd(&dString);
+                }
+                else
+                {
+                    LOG(1, "Unknown compression: %s.\n", compression);
+                    exit(-1);
+                }
+
+                byteData = decomData;
+            }
+
+            size_t byteDataSize = byteData.size();
+            for (unsigned int i = 0; i < byteDataSize; i += 4)
+            {
+                unsigned int gid = static_cast<unsigned char>(byteData[i + 0]) |
+                    (static_cast<unsigned char>(byteData[i + 1]) << 8u) | 
+                    (static_cast<unsigned char>(byteData[i + 2]) << 16u) |
+                    (static_cast<unsigned char>(byteData[i + 3]) << 24u);
+                tileData.push_back(gid);
+            }
+        }
+        else
+        {
+            LOG(1, "Unknown encoding: %s.\n", encoding);
+            exit(-1);
+        }
+    }
+
+    return tileData;
+}
+
+std::string TMXSceneEncoder::buildFilePath(const std::string& directory, const std::string& file)
+{
+    return EncoderArguments::getRealPath(directory + "/" + file);
+}
+
+void TMXSceneEncoder::copyImage(unsigned char* dst, const unsigned char* src,
+    unsigned int srcWidth, unsigned int dstWidth, unsigned int bpp,
+    unsigned int srcx, unsigned int srcy, unsigned int dstx, unsigned int dsty, unsigned int width, unsigned int height)
+{
+    unsigned int sizePerRow = width * bpp;
+    for (unsigned int y = 0; y < height; y++)
+    {
+        const unsigned char* srcPtr = &src[((y + srcy) * srcWidth + srcx) * bpp];
+        unsigned char* dstPtr = &dst[((y + dsty) * dstWidth + dstx) * bpp];
+        if (src == dst)
+        {
+            memmove(dstPtr, srcPtr, sizePerRow);
+        }
+        else
+        {
+            memcpy(dstPtr, srcPtr, sizePerRow);
+        }
+    }
+}

+ 82 - 0
tools/encoder/src/TMXSceneEncoder.h

@@ -0,0 +1,82 @@
+#ifndef TMXSCENEEENCODER_H_
+#define TMXSCENEEENCODER_H_
+
+#include <fstream>
+#include <unordered_map>
+#include <tinyxml2.h>
+
+#include "Base.h"
+#include "Vector2.h"
+#include "TMXTypes.h"
+#include "EncoderArguments.h"
+#include "Image.h"
+
+/**
+ * Class for encoding an TMX file.
+ */
+class TMXSceneEncoder
+{
+public:
+    /**
+     * Constructor.
+     */
+    TMXSceneEncoder();
+
+    /**
+     * Destructor.
+     */
+    ~TMXSceneEncoder();
+
+    /**
+     * Writes out encoded TMX file.
+     */
+    void write(const gameplay::EncoderArguments& arguments);
+
+private:
+    static std::vector<unsigned int> loadDataElement(const tinyxml2::XMLElement* data);
+    static inline std::string buildFilePath(const std::string& directory, const std::string& file);
+    static void copyImage(unsigned char* dst, const unsigned char* src,
+        unsigned int srcWidth, unsigned int dstWidth, unsigned int bpp,
+        unsigned int srcx, unsigned int srcy, unsigned int dstx, unsigned int dsty, unsigned int width, unsigned int height);
+
+    // Parsing
+    bool parseTmx(const tinyxml2::XMLDocument& xmlDoc, gameplay::TMXMap& map, const std::string& inputDirectory) const;
+    void parseBaseLayerProperties(const tinyxml2::XMLElement* xmlBaseLayer, gameplay::TMXBaseLayer* layer) const;
+
+    // Gutter
+    void buildTileGutter(gameplay::TMXMap& map, const std::string& inputDirectory, const std::string& outputDirectory);
+    bool buildTileGutterTileset(const gameplay::TMXTileSet& tileset, const std::string& inputFile, const std::string& outputFile);
+
+    // Writing
+    void writeScene(const gameplay::TMXMap& map, const std::string& outputFilepath, const std::string& sceneName);
+
+    void writeTileset(const gameplay::TMXMap& map, const gameplay::TMXLayer* layer, std::ofstream& file);
+    void writeSoloTileset(const gameplay::TMXMap& map, const gameplay::TMXTileSet& tmxTileset, const gameplay::TMXLayer& tileset, std::ofstream& file, unsigned int resultOnlyForTileset = TMX_INVALID_ID);
+
+    void writeSprite(const gameplay::TMXImageLayer* imageLayer, std::ofstream& file);
+
+    void writeNodeProperties(bool enabled, std::ofstream& file, bool seperatorLineWritten = false);
+    void writeNodeProperties(bool enabled, const gameplay::TMXProperties& properties, std::ofstream& file, bool seperatorLineWritten = false);
+    void writeNodeProperties(bool enabled, const gameplay::Vector2& pos, std::ofstream& file, bool seperatorLineWritten = false);
+    void writeNodeProperties(bool enabled, const gameplay::Vector2& pos, const gameplay::TMXProperties& properties, std::ofstream& file, bool seperatorLineWritten = false);
+
+    void writeLine(std::ofstream& file, const std::string& line) const;
+
+    unsigned int _tabCount;
+};
+
+inline void TMXSceneEncoder::writeNodeProperties(bool enabled, std::ofstream& file, bool seperatorLineWritten)
+{
+    writeNodeProperties(enabled, gameplay::Vector2::zero(), file, seperatorLineWritten);
+}
+inline void TMXSceneEncoder::writeNodeProperties(bool enabled, const gameplay::TMXProperties& properties, std::ofstream& file, bool seperatorLineWritten)
+{
+    writeNodeProperties(enabled, gameplay::Vector2::zero(), properties, file, seperatorLineWritten);
+}
+inline void TMXSceneEncoder::writeNodeProperties(bool enabled, const gameplay::Vector2& pos, std::ofstream& file, bool seperatorLineWritten)
+{
+    gameplay::TMXProperties noOp;
+    writeNodeProperties(enabled, pos, noOp, file, seperatorLineWritten);
+}
+
+#endif

+ 513 - 0
tools/encoder/src/TMXTypes.cpp

@@ -0,0 +1,513 @@
+#include "TMXTypes.h"
+
+using namespace gameplay;
+
+// TMXMap
+TMXMap::TMXMap()
+    : _width(0), _height(0),
+    _tileWidth(0.0f), _tileHeight(0.0f),
+    _tileSets(), _layers()
+{
+}
+
+TMXMap::~TMXMap()
+{
+    for (auto it = _layers.begin(); it != _layers.end(); it++)
+    {
+        delete *it;
+    }
+}
+
+void TMXMap::setWidth(unsigned int value)
+{
+    _width = value;
+}
+
+void TMXMap::setHeight(unsigned int value)
+{
+    _height = value;
+}
+
+unsigned int TMXMap::getWidth() const
+{
+    return _width;
+}
+
+unsigned int TMXMap::getHeight() const
+{
+    return _height;
+}
+
+void TMXMap::setTileWidth(float value)
+{
+    _tileWidth = value;
+}
+
+void TMXMap::setTileHeight(float value)
+{
+    _tileHeight = value;
+}
+
+float TMXMap::getTileWidth() const
+{
+    return _tileWidth;
+}
+
+float TMXMap::getTileHeight() const
+{
+    return _tileHeight;
+}
+
+bool TMXMap::isValidTileId(unsigned int tileId) const
+{
+    if (tileId != 0)
+    {
+        unsigned int tileSet = findTileSet(tileId);
+        return tileSet < _tileSets.size();
+    }
+
+    return false;
+}
+
+unsigned int TMXMap::findTileSet(unsigned int tileId) const
+{
+    if (tileId == 0)
+    {
+        return (unsigned int)_tileSets.size();
+    }
+
+    for (int i = (int)_tileSets.size() - 1; i >= 0; i--)
+    {
+        if (_tileSets[i].getFirstGid() > tileId)
+        {
+            continue;
+        }
+        return i;
+    }
+
+    return (unsigned int)_tileSets.size();
+}
+
+void TMXMap::addTileSet(const TMXTileSet& tileset)
+{
+    _tileSets.push_back(tileset);
+}
+
+void TMXMap::addLayer(const TMXBaseLayer* layer)
+{
+   _layers.push_back(layer);
+}
+
+unsigned int TMXMap::getTileSetCount() const
+{
+    return (unsigned int)_tileSets.size();
+}
+
+unsigned int TMXMap::getLayerCount() const
+{
+    return (unsigned int)_layers.size();
+}
+
+TMXTileSet& TMXMap::getTileSet(unsigned int index)
+{
+    return _tileSets[index];
+}
+
+const TMXTileSet& TMXMap::getTileSet(unsigned int index) const
+{
+    return _tileSets[index];
+}
+
+const TMXBaseLayer* TMXMap::getLayer(unsigned int index) const
+{
+    return _layers[index];
+}
+
+// TMXTileSet
+TMXTileSet::TMXTileSet()
+    : _path(""),
+    _imgWidth(0), _imgHeight(0), _horzTileCount(0), _vertTileCount(0),
+    _firstGid(0), _maxTileWidth(0), _maxTileHeight(0),
+    _offset(), _spacing(0), _margin(0)
+{
+}
+
+void TMXTileSet::setImagePath(const std::string& path)
+{
+    _path = path;
+}
+
+const std::string& TMXTileSet::getImagePath() const
+{
+    return _path;
+}
+
+void TMXTileSet::setFirstGid(unsigned int gid)
+{
+    _firstGid = gid;
+}
+
+unsigned int TMXTileSet::getFirstGid() const
+{
+    return _firstGid;
+}
+
+void TMXTileSet::setMaxTileWidth(unsigned int value, bool adjustTiles)
+{
+    _maxTileWidth = value;
+    if (adjustTiles && _maxTileWidth != 0)
+    {
+        setImageWidth(_imgWidth);
+    }
+}
+
+unsigned int TMXTileSet::getMaxTileWidth() const
+{
+    return _maxTileWidth;
+}
+
+void TMXTileSet::setMaxTileHeight(unsigned int value, bool adjustTiles)
+{
+    _maxTileHeight = value;
+    if (adjustTiles && _maxTileHeight != 0)
+    {
+        setImageWidth(_imgHeight);
+    }
+}
+
+unsigned int TMXTileSet::getMaxTileHeight() const
+{
+    return _maxTileHeight;
+}
+
+void TMXTileSet::setSpacing(unsigned int value, bool adjustTiles)
+{
+    _spacing = value;
+    if (adjustTiles)
+    {
+        if (_maxTileWidth != 0)
+        {
+            setImageWidth(_imgWidth);
+        }
+        if (_maxTileHeight != 0)
+        {
+            setImageWidth(_imgHeight);
+        }
+    }
+}
+
+unsigned int TMXTileSet::getSpacing() const
+{
+    return _spacing;
+}
+
+void TMXTileSet::setMargin(unsigned int value, bool adjustTiles)
+{
+    _margin = value;
+    if (adjustTiles)
+    {
+        if (_maxTileWidth != 0)
+        {
+            setImageWidth(_imgWidth);
+        }
+        if (_maxTileHeight != 0)
+        {
+            setImageWidth(_imgHeight);
+        }
+    }
+}
+
+unsigned int TMXTileSet::getMargin() const
+{
+    return _margin;
+}
+
+void TMXTileSet::setImageWidth(unsigned int value)
+{
+    _imgWidth = value;
+    _horzTileCount = (value - (_margin * 2) + (_spacing ? _spacing : 0)) / (_maxTileWidth + _spacing);
+}
+
+void TMXTileSet::setImageHeight(unsigned int value)
+{
+    _imgHeight = value;
+    _vertTileCount = (value - (_margin * 2) + (_spacing ? _spacing : 0)) / (_maxTileHeight + _spacing);
+}
+
+unsigned int TMXTileSet::getHorizontalTileCount() const
+{
+    return _horzTileCount;
+}
+
+unsigned int TMXTileSet::getVerticalTileCount() const
+{
+    return _vertTileCount;
+}
+
+void TMXTileSet::setOffset(const Vector2& off)
+{
+    _offset = off;
+}
+
+const Vector2& TMXTileSet::getOffset() const
+{
+    return _offset;
+}
+
+unsigned int TMXTileSet::calculateImageDimension(unsigned int tileCount, unsigned int tileSize, unsigned int spacing, unsigned int margin)
+{
+    return tileCount * (tileSize + spacing) + (margin * 2) - spacing;
+}
+
+Vector2 TMXTileSet::calculateTileOrigin(const Vector2& pos, const Vector2& tileSize, unsigned int spacing, unsigned int margin)
+{
+    float xpos = (tileSize.x + spacing) * pos.x;
+    float ypos = (tileSize.y + spacing) * pos.y;
+
+    return Vector2(xpos + margin, ypos + margin);
+}
+
+// TMXBaseLayer
+TMXBaseLayer::TMXBaseLayer(TMXLayerType type)
+    : _name(""), _type(type), _opacity(1.0f), _visible(true), _properties()
+{
+}
+
+TMXBaseLayer::~TMXBaseLayer()
+{
+}
+
+TMXLayerType TMXBaseLayer::getType() const
+{
+    return _type;
+}
+
+void TMXBaseLayer::setName(const std::string& value)
+{
+    _name = value;
+}
+
+const std::string& TMXBaseLayer::getName() const
+{
+    return _name;
+}
+
+void TMXBaseLayer::setOpacity(float value)
+{
+    _opacity = value;
+}
+
+float TMXBaseLayer::getOpacity() const
+{
+    return _opacity;
+}
+
+void TMXBaseLayer::setVisible(bool value)
+{
+    _visible = value;
+}
+
+bool TMXBaseLayer::getVisible() const
+{
+    return _visible;
+}
+
+TMXProperties& TMXBaseLayer::getProperties()
+{
+    return _properties;
+}
+
+const TMXProperties& TMXBaseLayer::getProperties() const
+{
+    return _properties;
+}
+
+// TMXLayer
+#define FLIPPED_HORIZONTALLY_FLAG 0x80000000
+#define FLIPPED_VERTICALLY_FLAG 0x40000000
+#define FLIPPED_DIAGONALLY_FLAG 0x20000000
+
+TMXLayer::TMXLayer()
+    : TMXBaseLayer(TMXLayerType::NormalLayer),
+    _width(0), _height(0),
+    _tiles()
+{
+}
+
+TMXLayer::~TMXLayer()
+{
+}
+
+void TMXLayer::setWidth(unsigned int value)
+{
+    _width = value;
+}
+
+unsigned int TMXLayer::getWidth() const
+{
+    return _width;
+}
+
+void TMXLayer::setHeight(unsigned int value)
+{
+    _height = value;
+}
+
+unsigned int TMXLayer::getHeight() const
+{
+    return _height;
+}
+
+void TMXLayer::setupTiles()
+{
+    if (_tiles.size() != (_width * _height))
+    {
+        _tiles.resize(_width * _height);
+    }
+}
+
+void TMXLayer::setTile(unsigned int x, unsigned int y, unsigned int gid)
+{
+    bool flipHorz = (gid & FLIPPED_HORIZONTALLY_FLAG) != 0;
+    bool flipVert = (gid & FLIPPED_VERTICALLY_FLAG) != 0;
+    bool flipDiag = (gid & FLIPPED_DIAGONALLY_FLAG) != 0;
+    unsigned int flaglessGid = gid & ~(FLIPPED_HORIZONTALLY_FLAG | FLIPPED_VERTICALLY_FLAG | FLIPPED_DIAGONALLY_FLAG);
+    _tiles[x + y * _width] = { flaglessGid, flipHorz, flipVert, flipDiag };
+}
+
+const TMXLayer::layer_tile& TMXLayer::getTileStruct(unsigned int x, unsigned int y) const
+{
+    return _tiles[x + y * _width];
+}
+
+unsigned int TMXLayer::getTile(unsigned int x, unsigned int y) const
+{
+    return getTileStruct(x, y).gid;
+}
+
+bool TMXLayer::isEmptyTile(unsigned int x, unsigned int y) const
+{
+    return getTileStruct(x, y).gid == 0;
+}
+
+Vector2 TMXLayer::getTileStart(unsigned int x, unsigned int y, const TMXMap& map, unsigned int resultOnlyForTileset) const
+{
+    // Get the tile
+    unsigned int gid = getTileStruct(x, y).gid;
+    if (gid == 0)
+    {
+        return Vector2(-1, -1);
+    }
+
+    // Get the tileset for the tile
+    unsigned int tileset_id = map.findTileSet(gid);
+    if (tileset_id == map.getTileSetCount() || 
+        (resultOnlyForTileset != TMX_INVALID_ID && tileset_id != resultOnlyForTileset))
+    {
+        return Vector2(-1, -1);
+    }
+    const TMXTileSet& tileset = map.getTileSet(tileset_id);
+    if (tileset.getHorizontalTileCount() == 0)
+    {
+        return Vector2(-1, -1);
+    }
+
+    // Calculate the tile position
+    unsigned int horzTileCount = tileset.getHorizontalTileCount();
+    unsigned int adjusted_gid = gid - tileset.getFirstGid();
+
+    return TMXTileSet::calculateTileOrigin(Vector2(static_cast<float>(adjusted_gid % horzTileCount), static_cast<float>(adjusted_gid / horzTileCount)),
+        Vector2(static_cast<float>(tileset.getMaxTileWidth()), static_cast<float>(tileset.getMaxTileHeight())),
+        tileset.getSpacing(), 
+        tileset.getMargin());
+}
+
+bool TMXLayer::hasTiles() const
+{
+    size_t tile_size = _tiles.size();
+    for (unsigned int i = 0; i < tile_size; i++)
+    {
+        if (_tiles[i].gid != 0)
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+std::set<unsigned int> TMXLayer::getTilesetsUsed(const TMXMap& map) const
+{
+    std::set<unsigned int> tilesets;
+
+    unsigned int tileset_size = map.getTileSetCount();
+    size_t tile_size = _tiles.size();
+    for (unsigned int i = 0; i < tile_size; i++)
+    {
+        unsigned int gid = _tiles[i].gid;
+        if (gid == 0)
+        {
+            // Empty tile
+            continue;
+        }
+        unsigned int tileset = map.findTileSet(gid);
+        if (tileset == tileset_size)
+        {
+            // Could not find tileset
+            continue;
+        }
+        tilesets.insert(tileset);
+        if (tilesets.size() == tileset_size)
+        {
+            // Don't need to continue checking, we have every possible tileset
+            break;
+        }
+    }
+
+    return tilesets;
+}
+
+// TMXImageLayer
+TMXImageLayer::TMXImageLayer()
+    : TMXBaseLayer(TMXLayerType::ImageLayer),
+    _pos(), _path("")
+{
+}
+
+TMXImageLayer::~TMXImageLayer()
+{
+}
+
+void TMXImageLayer::setX(int value)
+{
+    _pos.x = static_cast<float>(value);
+}
+
+int TMXImageLayer::getX() const
+{
+    return static_cast<int>(_pos.x);
+}
+
+void TMXImageLayer::setY(int value)
+{
+    _pos.y = static_cast<float>(value);
+}
+
+int TMXImageLayer::getY() const
+{
+    return static_cast<int>(_pos.y);
+}
+
+const Vector2& TMXImageLayer::getPosition() const
+{
+    return _pos;
+}
+
+void TMXImageLayer::setImagePath(const std::string& path)
+{
+    _path = path;
+}
+
+const std::string& TMXImageLayer::getImagePath() const
+{
+    return _path;
+}

+ 252 - 0
tools/encoder/src/TMXTypes.h

@@ -0,0 +1,252 @@
+#ifndef TMXTYPES_H_
+#define TMXTYPES_H_
+
+#include <vector>
+#include <set>
+#include <map>
+#include <string>
+
+#include "Vector2.h"
+
+namespace gameplay
+{
+
+typedef std::map<std::string, std::string> TMXProperties;
+
+/**
+ * Represents the tiles that make up a map.
+ */
+class TMXTileSet
+{
+public:
+    /**
+     * Constructor.
+     */
+    TMXTileSet();
+
+    void setImagePath(const std::string& path);
+    const std::string& getImagePath() const;
+
+    void setFirstGid(unsigned int gid);
+    unsigned int getFirstGid() const;
+
+    void setMaxTileWidth(unsigned int value, bool adjustTiles = true);
+    unsigned int getMaxTileWidth() const;
+    void setMaxTileHeight(unsigned int value, bool adjustTiles = true);
+    unsigned int getMaxTileHeight() const;
+
+    void setSpacing(unsigned int value, bool adjustTiles = true);
+    unsigned int getSpacing() const;
+    void setMargin(unsigned int value, bool adjustTiles = true);
+    unsigned int getMargin() const;
+
+    // This will calculate the number of tiles based on max tile size, spacing, and margin.
+    // If any of those change, this needs to be recalled.
+    void setImageWidth(unsigned int value);
+    void setImageHeight(unsigned int value);
+    unsigned int getHorizontalTileCount() const;
+    unsigned int getVerticalTileCount() const;
+
+    void setOffset(const Vector2& offset);
+    const Vector2& getOffset() const;
+
+    static unsigned int calculateImageDimension(unsigned int tileCount, unsigned int tileSize, unsigned int spacing = 0, unsigned int margin = 0);
+    static Vector2 calculateTileOrigin(const Vector2& pos, const Vector2& tileSize, unsigned int spacing = 0, unsigned int margin = 0);
+
+private:
+    std::string _path;
+
+    unsigned int _imgWidth;
+    unsigned int _imgHeight;
+    unsigned int _horzTileCount;
+    unsigned int _vertTileCount;
+
+    unsigned int _firstGid;
+    unsigned int _maxTileWidth;
+    unsigned int _maxTileHeight;
+    Vector2 _offset;
+    unsigned int _spacing;
+    unsigned int _margin;
+};
+
+/**
+ * Types of layers
+ */
+enum TMXLayerType
+{
+    NormalLayer,
+    ImageLayer,
+};
+
+/**
+ * Represents the base type of layer on a map.
+ */
+class TMXBaseLayer
+{
+protected:
+    /**
+     * Constructor.
+     */
+    TMXBaseLayer(TMXLayerType type);
+
+public:
+    /**
+     * Destructor.
+     */
+    virtual ~TMXBaseLayer();
+
+    TMXLayerType getType() const;
+
+    void setName(const std::string& value);
+    const std::string& getName() const;
+
+    void setOpacity(float value);
+    float getOpacity() const;
+
+    void setVisible(bool value);
+    bool getVisible() const;
+
+    TMXProperties& getProperties();
+    const TMXProperties& getProperties() const;
+
+private:
+    std::string _name;
+    TMXLayerType _type;
+    float _opacity;
+    bool _visible;
+    TMXProperties _properties;
+};
+
+#define TMX_INVALID_ID 0xFFFFFFFFu
+
+class TMXMap;
+/**
+ * Represents a single layer on a map.
+ */
+class TMXLayer : public TMXBaseLayer
+{
+public:
+    /**
+     * Constructor.
+     */
+    TMXLayer();
+
+    /**
+     * Destructor.
+     */
+    virtual ~TMXLayer();
+
+    void setWidth(unsigned int value);
+    unsigned int getWidth() const;
+    void setHeight(unsigned int value);
+    unsigned int getHeight() const;
+
+    void setupTiles();
+    void setTile(unsigned int x, unsigned int y, unsigned int gid);
+    unsigned int getTile(unsigned int x, unsigned int y) const;
+    bool isEmptyTile(unsigned int x, unsigned int y) const;
+    Vector2 getTileStart(unsigned int x, unsigned int y, const TMXMap& map, unsigned int resultOnlyForTileset = TMX_INVALID_ID) const;
+
+    bool hasTiles() const;
+    std::set<unsigned int> getTilesetsUsed(const TMXMap& map) const;
+
+private:
+    struct layer_tile
+    {
+        unsigned int gid;
+        bool horz_flip;
+        bool vert_flip;
+        bool diag_flip;
+    };
+
+    const layer_tile& getTileStruct(unsigned int x, unsigned int y) const;
+
+    unsigned int _width;
+    unsigned int _height;
+
+    std::vector<layer_tile> _tiles;
+};
+
+/**
+ * Represents a image layer on a map.
+ */
+class TMXImageLayer : public TMXBaseLayer
+{
+public:
+    /**
+     * Constructor.
+     */
+    TMXImageLayer();
+
+    /**
+     * Destructor.
+     */
+    virtual ~TMXImageLayer();
+
+    void setX(int value);
+    int getX() const;
+    void setY(int value);
+    int getY() const;
+
+    const Vector2& getPosition() const;
+
+    void setImagePath(const std::string& path);
+    const std::string& getImagePath() const;
+
+private:
+    Vector2 _pos;
+    std::string _path;
+};
+
+/**
+ * Represents an entire map of tiles.
+ */
+class TMXMap
+{
+public:
+    /**
+     * Constructor.
+     */
+    TMXMap();
+
+    /**
+     * Destructor.
+     */
+    ~TMXMap();
+
+    void setWidth(unsigned int value);
+    void setHeight(unsigned int value);
+    unsigned int getWidth() const;
+    unsigned int getHeight() const;
+
+    void setTileWidth(float value);
+    void setTileHeight(float value);
+    float getTileWidth() const;
+    float getTileHeight() const;
+
+    bool isValidTileId(unsigned int tileId) const;
+    unsigned int findTileSet(unsigned int tileId) const;
+
+    void addTileSet(const TMXTileSet& tileset);
+    void addLayer(const TMXBaseLayer* layer);
+
+    unsigned int getTileSetCount() const;
+    unsigned int getLayerCount() const;
+
+    TMXTileSet& getTileSet(unsigned int index);
+    const TMXTileSet& getTileSet(unsigned int index) const;
+    const TMXBaseLayer* getLayer(unsigned int index) const;
+
+private:
+    unsigned int _width;
+    unsigned int _height;
+    float _tileWidth;
+    float _tileHeight;
+
+    std::vector<TMXTileSet> _tileSets;
+    std::vector<const TMXBaseLayer*> _layers;
+};
+
+}
+
+#endif

+ 7 - 5
tools/encoder/src/main.cpp

@@ -1,5 +1,6 @@
 #include "Base.h"
 #include "FBXSceneEncoder.h"
+#include "TMXSceneEncoder.h"
 #include "TTFFontEncoder.h"
 #include "GPBDecoder.h"
 #include "EncoderArguments.h"
@@ -99,11 +100,6 @@ int main(int argc, const char** argv)
 
     switch (arguments.getFileFormat())
     {
-    case EncoderArguments::FILEFORMAT_DAE:
-        {
-            LOG(1, "Error: Collada support has been removed. Convert your DAE file to FBX.\n");
-            return -1;
-        }
     case EncoderArguments::FILEFORMAT_FBX:
         {
             std::string realpath(arguments.getFilePath());
@@ -111,6 +107,12 @@ int main(int argc, const char** argv)
             fbxEncoder.write(realpath, arguments);
             break;
         }
+    case EncoderArguments::FILEFORMAT_TMX:
+        {
+            TMXSceneEncoder tmxEncoder;
+            tmxEncoder.write(arguments);
+            break;
+        }
     case EncoderArguments::FILEFORMAT_TTF:
         {
             std::vector<unsigned int> fontSizes = arguments.getFontSizes();