using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Tilemaps; namespace SuperTiled2Unity.Editor { public partial class TmxAssetImporter { private CollisionBuilder m_CurrentCollisionBuilder; private SuperTileLayer m_CurrentTileLayer; private struct Chunk { public DataEncoding Encoding { get; set; } public DataCompression Compression { get; set; } public XElement XmlChunk { get; set; } public int X { get; set; } public int Y { get; set; } public int Width { get; set; } public int Height { get; set; } } private SuperLayer ProcessTileLayer(GameObject goParent, XElement xLayer) { Assert.IsNotNull(xLayer); Assert.IsNotNull(goParent); // Create the game object that contains the layer and add it to the grid parent var layerComponent = goParent.AddSuperLayerGameObject(new SuperTileLayerLoader(xLayer, this), SuperImportContext); AddSuperCustomProperties(layerComponent.gameObject, xLayer.Element("properties")); RendererSorter.BeginTileLayer(layerComponent); using (SuperImportContext.BeginIsTriggerOverride(layerComponent.gameObject)) { // Process the data for the layer var xData = xLayer.Element("data"); if (xData != null) { ProcessLayerData(layerComponent.gameObject, xData); } } RendererSorter.EndTileLayer(layerComponent); return layerComponent; } private void ProcessLayerData(GameObject goLayer, XElement xData) { Assert.IsNotNull(goLayer); Assert.IsNotNull(xData); Assert.IsNull(m_CurrentTileLayer); m_CurrentTileLayer = goLayer.GetComponent(); Assert.IsNotNull(m_CurrentTileLayer); // Create the tilemap for the layer if needed if (!m_TilesAsObjects && SuperImportContext.LayerIgnoreMode != LayerIgnoreMode.Visual) { GetOrAddTilemapComponent(goLayer); } else if (m_TilesAsObjects) { goLayer.AddComponent(); } var chunk = new Chunk(); chunk.Encoding = xData.GetAttributeAs("encoding"); chunk.Compression = xData.GetAttributeAs("compression"); // Are we reading in data in smaller chunks (for infinite maps) or one big chunk (the full map) var xChunks = xData.Elements("chunk"); if (xChunks.Any()) { foreach (var xChunk in xChunks) { chunk.XmlChunk = xChunk; chunk.X = xChunk.GetAttributeAs("x"); chunk.Y = xChunk.GetAttributeAs("y"); chunk.Width = xChunk.GetAttributeAs("width"); chunk.Height = xChunk.GetAttributeAs("height"); // Each chunk is a separate game object on the super layer GameObject goChunk = new GameObject(string.Format("Chunk ({0},{1})", chunk.X, chunk.Y)); goLayer.AddChildWithUniqueName(goChunk); ProcessLayerDataChunk(goChunk, chunk); } } else { // For regular maps the 'chunk' is the same as the layer data chunk.XmlChunk = xData; chunk.X = 0; chunk.Y = 0; chunk.Width = m_MapComponent.m_Width; chunk.Height = m_MapComponent.m_Height; ProcessLayerDataChunk(goLayer, chunk); } m_CurrentTileLayer = null; } private Tilemap GetOrAddTilemapComponent(GameObject go) { if (RendererSorter.IsUsingGroups()) { // If we have a group layer parent then use it instead as we are grouping tiles on the same tilemap (using the z-component of the tile location) var grouping = go.GetComponentInParent(); if (grouping != null) { // The Tilemap will go onto the group layer go = grouping.gameObject; } } // If we already have a Tilemap component then use it var tilemap = go.GetComponent(); if (tilemap != null) { return tilemap; } tilemap = go.AddComponent(); tilemap.animationFrameRate = ST2USettings.instance.m_AnimationFramerate; tilemap.tileAnchor = new Vector3(0, 0, 0); AddTilemapRendererComponent(go); // Figure out our opacity var layer = go.GetComponent(); tilemap.color = layer.CalculateColor(); return tilemap; } private TilemapRenderer AddTilemapRendererComponent(GameObject go) { var renderer = go.AddComponent(); renderer.sortOrder = MapRenderConverter.Tiled2Unity(m_MapComponent.m_RenderOrder); AssignMaterial(renderer, m_CurrentTileLayer.m_TiledName); AssignTilemapSorting(renderer); if (m_SortingMode == SortingMode.CustomSortAxis) { renderer.mode = TilemapRenderer.Mode.Individual; } else { renderer.mode = TilemapRenderer.Mode.Chunk; } return renderer; } private void ProcessLayerDataChunk(GameObject goTilemap, Chunk chunk) { Assert.IsNotNull(m_MapComponent); // Instantiate object to build all colliders for this chunk m_CurrentCollisionBuilder = new CollisionBuilder(goTilemap, m_TilePolygonDatabase, SuperImportContext, m_MapComponent); var tileIds = ReadTileIdsFromChunk(chunk); PlaceTiles(goTilemap, chunk, tileIds); m_CurrentCollisionBuilder.Build(this); m_CurrentCollisionBuilder = null; } private List ReadTileIdsFromChunk(Chunk chunk) { List tileIds = new List(chunk.Width * chunk.Height); if (chunk.Encoding == DataEncoding.Xml) { ReadTileIds_Xml(chunk.XmlChunk, ref tileIds); } else if (chunk.Encoding == DataEncoding.Csv) { ReadTileIds_Csv(chunk.XmlChunk, ref tileIds); } else if (chunk.Encoding == DataEncoding.Base64) { ReadTileIds_Base64(chunk.XmlChunk, chunk.Compression, ref tileIds); } else { ReportGenericError($"Unhandled encoding type ({chunk.Encoding}) used for map layer data."); } return tileIds; } private void PlaceTiles(GameObject goTilemap, Chunk chunk, List tileIds) { // Only report each missing tile Id once HashSet badTiles = new HashSet(); var tilemap = goTilemap.GetComponentInParent(); var tilemapRenderer = goTilemap.GetComponentInParent(); for (int i = 0; i < tileIds.Count; i++) { uint utId = tileIds[i]; if (utId != 0) { var tileId = new TileIdMath(utId); Vector3Int int3 = m_MapComponent.TiledIndexToGridCell(i, chunk.X, chunk.Y, chunk.Width); SuperTile tile; if (m_GlobalTileDatabase.TryGetTile(tileId.JustTileId, out tile)) { var cx = i % chunk.Width; var cy = i / chunk.Width; cx += chunk.X; cy += chunk.Y; PlaceTile(goTilemap, tilemap, tilemapRenderer, cx, cy, tile, int3, tileId); } else if (!badTiles.Contains(tileId.JustTileId)) { ReportGenericError($"Could not find tile '{tileId.JustTileId}'. Make sure the tilesets were successfully imported."); badTiles.Add(tileId.JustTileId); } } } } private void PlaceTile(GameObject goTilemap, Tilemap tilemap, TilemapRenderer tilemapRenderer, int cx, int cy, SuperTile tile, Vector3Int pos3, TileIdMath tileId) { if (m_TilesAsObjects) { PlaceTileAsObject(goTilemap, tile, cx, cy, tileId, pos3); } else { PlaceTileAsTile(goTilemap, tilemap, tilemapRenderer, tile, tileId, pos3); } } private void PlaceTileAsObject(GameObject goTilemap, SuperTile tile, int cx, int cy, TileIdMath tileId, Vector3Int pos3) { Assert.IsNotNull(goTilemap.GetComponentInParent()); Assert.IsNotNull(goTilemap.GetComponentInParent()); var superMap = goTilemap.GetComponentInParent(); var superLayer = goTilemap.GetComponentInParent(); var color = superLayer.CalculateColor(); string tileName = string.Format("tile ({0}, {1})", cx, cy); var goTRS = new GameObject(string.Format("{0} (TRS)", tileName)); goTilemap.AddChildWithUniqueName(goTRS); // Create a faux SuperObject component to add to our placed tile // We need this in case we have custom scripts that are looking for tile object via component { var tileObject = goTRS.AddComponent(); tileObject.m_Id = m_NextTileAsObjectId++; tileObject.m_TiledName = string.Format("AsObject_{0}", tileObject.m_Id); tileObject.m_Type = tile.m_Type; tileObject.m_X = pos3.x * superMap.m_TileWidth; tileObject.m_Y = -pos3.y * superMap.m_TileHeight; tileObject.m_Width = tile.m_Width; tileObject.m_Height = tile.m_Height; tileObject.m_TileId = (uint)tile.m_TileId; tileObject.m_Visible = true; // Does the tile have any properties? if (!tile.m_CustomProperties.IsEmpty()) { var component = tileObject.gameObject.AddComponent(); component.m_Properties = new List(); component.m_Properties.CombineFromSource(tile.m_CustomProperties); } } tile.GetTRS(tileId.FlipFlags, m_MapComponent.m_Orientation, m_MapComponent, out Vector3 translate, out Vector3 rotate, out Vector3 scale); var cellPos = superMap.CellPositionToLocalPosition(pos3.x, pos3.y, SuperImportContext); translate.x += cellPos.x; translate.y += cellPos.y; // Add the game object for the tile goTRS.transform.localPosition = translate; goTRS.transform.localRotation = Quaternion.Euler(rotate); goTRS.transform.localScale = scale; // Add the sprite renderer component var renderer = goTRS.AddComponent(); renderer.sprite = tile.m_Sprite; renderer.color = color; AssignMaterial(renderer, m_CurrentTileLayer.m_TiledName); AssignSpriteSorting(renderer); if (!tile.m_AnimationSprites.IsEmpty()) { var tileAnimator = goTRS.AddComponent(); tileAnimator.m_AnimationFramerate = ST2USettings.instance.m_AnimationFramerate; tileAnimator.m_AnimationSprites = tile.m_AnimationSprites; } // Add any colliders that may be on the tile object tile.AddCollidersForTileObject(goTRS, SuperImportContext); } private void PlaceTileAsTile(GameObject goTilemap, Tilemap tilemap, TilemapRenderer tilemapRenderer, SuperTile tile, TileIdMath tileId, Vector3Int pos3) { // Burn our layer index into the z component of the tile position // This allows us to support Tilemaps being shared by groups pos3.z = RendererSorter.CurrentTileZ; if (SuperImportContext.LayerIgnoreMode != LayerIgnoreMode.Visual) { // Set the tile (sprite, transform matrix, flags) tilemap.SetTile(pos3, tile); tilemap.SetTransformMatrix(pos3, tile.GetTransformMatrix(tileId.FlipFlags, m_MapComponent)); if (tilemapRenderer.mode == TilemapRenderer.Mode.Individual) { // Each tile color must be set individually tilemap.SetColor(pos3, tilemap.color); } tilemap.SetTileFlags(pos3, TileFlags.LockAll); } if (SuperImportContext.LayerIgnoreMode != LayerIgnoreMode.Collision) { // Do we have any colliders on the tile to be gathered? m_CurrentCollisionBuilder.PlaceTileColliders(m_MapComponent, tile, tileId, pos3); } } private void ReadTileIds_Xml(XElement xElement, ref List tileIds) { foreach (var xTile in xElement.Elements("tile")) { tileIds.Add(xTile.GetAttributeAs("gid", 0)); } } private void ReadTileIds_Csv(XElement xElement, ref List tileIds) { // Splitting line-by-line reducues out-of-memory exceptions for really large maps // (Really large maps should be avoided, however) string data = xElement.Value; StringReader reader = new StringReader(data); string line = string.Empty; do { line = reader.ReadLine(); if (!String.IsNullOrEmpty(line)) { var datum = from val in line.Split(',') where !val.IsNullOrWhiteSpace() select Convert.ToUInt32(val); tileIds.AddRange(datum); } } while (line != null); } private void ReadTileIds_Base64(XElement xElement, DataCompression compression, ref List tileIds) { string data = xElement.Value; if (compression == DataCompression.None) { tileIds = data.Base64ToBytes().ToUInts().ToList(); } else if (compression == DataCompression.Gzip) { try { tileIds = data.Base64ToBytes().GzipDecompress().ToUInts().ToList(); } catch { tileIds.Clear(); ReportGenericError("Gzip compression is not supported on your development platform. Change Tile Layer Format to another type in Tiled."); } } else if (compression == DataCompression.Zlib) { try { tileIds = data.Base64ToBytes().ZlibDeflate().ToUInts().ToList(); } catch { tileIds.Clear(); ReportGenericError("zlib compression is not supported on your development platform. Change Tile Layer Format to another type in Tiled."); } } else { ReportGenericError($"Unhandled compression type ({compression}) used for map layer data"); } } } }