evershade/Packages/super-tiled2unity.v2.2.0/Editor/Importers/TmxAssetImporter.cs

537 lines
21 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using UnityEditor;
using UnityEditor.AssetImporters;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Tilemaps;
namespace SuperTiled2Unity.Editor
{
[ScriptedImporter(ImporterConstants.MapVersion, ImporterConstants.MapExtension, ImporterConstants.MapImportOrder)]
public partial class TmxAssetImporter : TiledAssetImporter
{
public const string TilesAsObjectsSerializedName = nameof(m_TilesAsObjects);
public const string SortingModeSerializedName = nameof(m_SortingMode);
public const string CustomImporterClassNameSerializedName = nameof(m_CustomImporterClassName);
private SuperMap m_MapComponent;
private Grid m_GridComponent;
private GlobalTileDatabase m_GlobalTileDatabase;
private Dictionary<uint, TilePolygonCollection> m_TilePolygonDatabase;
private int m_NextTileAsObjectId;
private Dictionary<int, GameObject> m_ObjectsById;
[SerializeField]
private bool m_TilesAsObjects = false;
public bool TilesAsObjects => m_TilesAsObjects;
[SerializeField]
private SortingMode m_SortingMode = SortingMode.Stacked;
public SortingMode SortingMode => m_SortingMode;
[SerializeField]
private bool m_IsIsometric = false;
public bool IsIsometric => m_IsIsometric;
[SerializeField]
private string m_CustomImporterClassName = string.Empty;
[SerializeField]
private List<SuperTileset> m_InternalTilesets;
/// Returns the game object corresponding to a Tiled object ID.
public GameObject GetObject(int targetId)
{
if (m_ObjectsById.TryGetValue(targetId, out var go))
{
return go;
}
return null;
}
protected override void InternalOnImportAsset()
{
base.InternalOnImportAsset();
ImporterVersion = ImporterConstants.MapVersion;
AddSuperAsset<SuperAssetMap>();
XDocument doc = XDocument.Load(assetPath);
if (doc != null)
{
// Early out if Zstd compression is used. This simply isn't supported by Unity.
if (doc.Descendants("data").Where(x => ((string)x.Attribute("compression")) == "zstd").Count() > 0)
{
ReportGenericError("Unity does not support Zstandard compression.\nSelect a different 'Tile Layer Format' in your map settings in Tiled and resave.");
return;
}
var xMap = doc.Element("map");
ProcessMap(xMap);
}
DoPrefabReplacements();
DoCustomImporting();
// Were any import errors captured along the way?
m_MapComponent.m_ImportErrors = ImportErrors;
}
private void ProcessMap(XElement xMap)
{
Assert.IsNotNull(xMap);
Assert.IsNull(m_MapComponent);
Assert.IsNull(m_GlobalTileDatabase);
m_TilePolygonDatabase = new Dictionary<uint, TilePolygonCollection>();
RendererSorter.SortingMode = m_SortingMode;
// Create our map and fill it out
bool success = true;
success = success && PrepareMainObject();
success = success && ProcessMapAttributes(xMap);
success = success && ProcessGridObject(xMap);
success = success && ProcessTilesetElements(xMap);
if (success)
{
// Custom properties need to be in place before we process the map layers
AddSuperCustomProperties(m_MapComponent.gameObject, xMap.Element("properties"));
using (SuperImportContext.BeginIsTriggerOverride(m_MapComponent.gameObject))
{
// Add layers to our grid object
ProcessMapLayers(m_GridComponent.gameObject, xMap);
PostProccessMapLayers(m_GridComponent.gameObject);
}
}
}
// The map object is our Main Asset - the prefab that is created in our scene when dragged into the hierarchy
private bool PrepareMainObject()
{
var icon = SuperIcons.instance.m_TmxIcon;
var goGrid = new GameObject("_MapMainObject");
SuperImportContext.AddObjectToAsset("_MapPrfab", goGrid, icon);
SuperImportContext.SetMainObject(goGrid);
m_MapComponent = goGrid.AddComponent<SuperMap>();
return true;
}
private bool ProcessMapAttributes(XElement xMap)
{
m_MapComponent.name = Path.GetFileNameWithoutExtension(this.assetPath);
m_MapComponent.m_Version = xMap.GetAttributeAs<string>("version");
m_MapComponent.m_TiledVersion = xMap.GetAttributeAs<string>("tiledversion");
m_MapComponent.m_Orientation = xMap.GetAttributeAs<MapOrientation>("orientation");
m_MapComponent.m_RenderOrder = xMap.GetAttributeAs<MapRenderOrder>("renderorder");
m_MapComponent.m_Width = xMap.GetAttributeAs<int>("width");
m_MapComponent.m_Height = xMap.GetAttributeAs<int>("height");
m_MapComponent.m_TileWidth = xMap.GetAttributeAs<int>("tilewidth");
m_MapComponent.m_TileHeight = xMap.GetAttributeAs<int>("tileheight");
m_MapComponent.m_HexSideLength = xMap.GetAttributeAs<int>("hexsidelength");
m_MapComponent.m_StaggerAxis = xMap.GetAttributeAs<StaggerAxis>("staggeraxis");
m_MapComponent.m_StaggerIndex = xMap.GetAttributeAs<StaggerIndex>("staggerindex");
m_MapComponent.m_Infinite = xMap.GetAttributeAs<bool>("infinite");
m_MapComponent.m_BackgroundColor = xMap.GetAttributeAsColor("backgroundcolor", NamedColors.Gray);
m_MapComponent.m_NextObjectId = xMap.GetAttributeAs<int>("nextobjectid");
m_IsIsometric = m_MapComponent.m_Orientation == MapOrientation.Isometric;
m_NextTileAsObjectId = m_MapComponent.m_NextObjectId;
return true;
}
private bool ProcessGridObject(XElement xMap)
{
// Add the grid to the map
var goGrid = new GameObject("Grid");
goGrid.transform.SetParent(m_MapComponent.gameObject.transform);
m_GridComponent = goGrid.AddComponent<Grid>();
// Grid cell size always has a z-value of 1 so that we can use custom axis sorting
float sx = SuperImportContext.MakeScalar(m_MapComponent.m_TileWidth);
float sy = SuperImportContext.MakeScalar(m_MapComponent.m_TileHeight);
m_GridComponent.cellSize = new Vector3(sx, sy, 1);
Vector3 tilemapOffset = new Vector3(0, 0, 0);
switch (m_MapComponent.m_Orientation)
{
case MapOrientation.Isometric:
m_GridComponent.cellLayout = GridLayout.CellLayout.Isometric;
tilemapOffset = new Vector3(-sx * 0.5f, -sy, 0);
break;
case MapOrientation.Staggered:
m_GridComponent.cellLayout = GridLayout.CellLayout.Isometric;
if (m_MapComponent.m_StaggerAxis == StaggerAxis.Y)
{
if (m_MapComponent.m_StaggerIndex == StaggerIndex.Odd)
{
// Y - Odd
tilemapOffset = new Vector3(0, -sy, 0);
}
else
{
// Y-Even
tilemapOffset = new Vector3(sx * 0.5f, -sy, 0);
}
}
else if (m_MapComponent.m_StaggerAxis == StaggerAxis.X)
{
// X-Ood
if (m_MapComponent.m_StaggerIndex == StaggerIndex.Odd)
{
tilemapOffset = new Vector3(0, -sy, 0);
}
else
{
// X-Even
tilemapOffset = new Vector3(0, -sy * 1.5f, 0);
}
}
break;
case MapOrientation.Hexagonal:
if (m_MapComponent.m_StaggerAxis == StaggerAxis.Y)
{
// Pointy-top hex maps
m_GridComponent.cellLayout = GridLayout.CellLayout.Hexagon;
m_GridComponent.cellSwizzle = GridLayout.CellSwizzle.XYZ;
if (m_MapComponent.m_StaggerIndex == StaggerIndex.Odd)
{
// Y-Odd
tilemapOffset = new Vector3(0, -sy, 0);
}
else
{
// Y-Even
tilemapOffset = new Vector3(0, -sy * 0.25f, 0);
}
}
else if (m_MapComponent.m_StaggerAxis == StaggerAxis.X)
{
// Flat-top hex maps. Reverse x and y on size.
m_GridComponent.cellLayout = GridLayout.CellLayout.Hexagon;
m_GridComponent.cellSwizzle = GridLayout.CellSwizzle.YXZ;
m_GridComponent.cellSize = new Vector3(sy, sx, 1);
if (m_MapComponent.m_StaggerIndex == StaggerIndex.Odd)
{
// X-Odd
tilemapOffset = new Vector3(-sx * 0.75f, -sy * 1.5f, 0);
}
else
{
// X-Even
tilemapOffset = new Vector3(0, -sy * 1.5f, 0);
}
}
break;
default:
m_GridComponent.cellLayout = GridLayout.CellLayout.Rectangle;
tilemapOffset = new Vector3(0, -sy, 0);
break;
}
SuperImportContext.TilemapOffset = tilemapOffset;
return true;
}
private bool ProcessTilesetElements(XElement xMap)
{
Assert.IsNull(m_GlobalTileDatabase);
bool success = true;
// Our tile database will be fed with tiles from each referenced tileset
m_GlobalTileDatabase = new GlobalTileDatabase();
m_InternalTilesets = new List<SuperTileset>();
foreach (var xTileset in xMap.Elements("tileset"))
{
if (xTileset.Attribute("source") != null)
{
success = success && ProcessTilesetElementExternal(xTileset);
}
else
{
success = success && ProcessTilesetElementInternal(xTileset);
}
}
return success;
}
private bool ProcessTilesetElementExternal(XElement xTileset)
{
Assert.IsNotNull(xTileset);
Assert.IsNotNull(m_GlobalTileDatabase);
var firstId = xTileset.GetAttributeAs<int>("firstgid");
var source = xTileset.GetAttributeAs<string>("source");
// JSON customized assets are not supported as Unity has the *.json extension reserved
if (string.Equals(Path.GetExtension(source), ".json", StringComparison.OrdinalIgnoreCase))
{
ReportGenericError($"JSON tilesets are not supported by Unity. Use TSX files instead. Tileset: {source}");
return false;
}
// Load the tileset and process the tiles inside
var tileset = RequestDependencyAssetAtPath<SuperTileset>(source);
if (tileset == null)
{
// Tileset is missing or was not imported properly
return false;
}
else
{
// Register all the tiles with the tile database for this map
m_GlobalTileDatabase.RegisterTileset(firstId, tileset);
}
return true;
}
private bool ProcessTilesetElementInternal(XElement xTileset)
{
var firstId = xTileset.GetAttributeAs<int>("firstgid");
var name = xTileset.GetAttributeAs<string>("name");
var tileset = ScriptableObject.CreateInstance<SuperTileset>();
tileset.m_IsInternal = true;
tileset.name = name;
tileset.m_PixelsPerUnit = PixelsPerUnit;
m_InternalTilesets.Add(tileset);
string assetName = string.Format("_TilesetScriptObjectInternal_{0}", m_InternalTilesets.Count);
SuperImportContext.AddObjectToAsset(assetName, tileset);
var loader = new TilesetLoader(tileset, this);
if (loader.LoadFromXml(xTileset))
{
m_GlobalTileDatabase.RegisterTileset(firstId, tileset);
return true;
}
return false;
}
private void ProcessMapLayers(GameObject goParent, XElement xMap)
{
// Note that this method is re-entrant due to group layers
foreach (XElement xNode in xMap.Elements())
{
if (!xNode.GetAttributeAs<bool>("visible", true))
{
continue;
}
LayerIgnoreMode ignoreMode = xNode.GetPropertyAttributeAs(StringConstants.Unity_Ignore, SuperImportContext.LayerIgnoreMode);
if (ignoreMode == LayerIgnoreMode.True)
{
continue;
}
using (SuperImportContext.BeginLayerIgnoreMode(ignoreMode))
{
if (xNode.Name == "layer")
{
ProcessTileLayer(goParent, xNode);
}
else if (xNode.Name == "group")
{
ProcessGroupLayer(goParent, xNode);
}
else if (xNode.Name == "objectgroup")
{
ProcessObjectLayer(goParent, xNode);
}
else if (xNode.Name == "imagelayer")
{
ProcessImageLayer(goParent, xNode);
}
}
}
}
private void PostProccessMapLayers(GameObject goParent)
{
foreach (var layer in goParent.GetComponentsInChildren<SuperLayer>())
{
layer.SetWorldPosition(m_MapComponent, SuperImportContext);
}
// Refresh all our tilemaps so that needless prefab instance changes don't appear
foreach (var tilemap in goParent.GetComponentsInChildren<Tilemap>())
{
tilemap.RefreshAllTiles();
CalculateChunkCullingBounds(tilemap);
}
}
private static void CalculateChunkCullingBounds(Tilemap tilemap)
{
// Each tilemap gameobject should have a tilemap renderer
// Set the chunk culling bounds of the renderer using the size of the largest sprite in the tilemap
var tilemapRenderer = tilemap.gameObject.GetComponent<TilemapRenderer>();
if (tilemapRenderer != null)
{
tilemapRenderer.detectChunkCullingBounds = TilemapRenderer.DetectChunkCullingBounds.Manual;
float maxWidth = 0;
float maxHeight = 0;
var allTiles = tilemap.GetTilesBlock(tilemap.cellBounds);
foreach (var tile in allTiles.OfType<SuperTile>())
{
maxWidth = Mathf.Max(tile.m_Sprite.bounds.size.x, maxWidth);
maxHeight = Mathf.Max(tile.m_Sprite.bounds.size.y, maxHeight);
// Look out for animated tiles
if (!tile.m_AnimationSprites.IsEmpty())
{
foreach (var sprite in tile.m_AnimationSprites)
{
maxWidth = Mathf.Max(sprite.bounds.size.x, maxWidth);
maxHeight = Mathf.Max(sprite.bounds.size.y, maxHeight);
}
}
}
tilemapRenderer.chunkCullingBounds = new Vector3(maxWidth, maxHeight, 0);
}
}
private void DoPrefabReplacements()
{
// Should any of our objects (from Tiled) be replaced by instantiated prefabs?
var supers = m_MapComponent.GetComponentsInChildren<SuperObject>();
m_ObjectsById = supers.ToDictionary(so => so.m_Id, so => so.gameObject);
var goToDestroy = new List<GameObject>();
foreach (var so in supers)
{
var prefab = ST2USettings.instance.GetPrefabReplacement(so.m_Type);
if (prefab != null)
{
// Replace the super object with the instantiated prefab
var instance = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
instance.transform.SetParent(so.transform.parent);
instance.transform.position = so.transform.position + prefab.transform.localPosition;
instance.transform.rotation = so.transform.rotation;
// Keep the name from Tiled.
instance.name = so.gameObject.name;
// Update bookkeeping for later custom property replacement.
goToDestroy.Add(so.gameObject);
m_ObjectsById[so.m_Id] = instance;
}
}
// Now that all the replacements have been instantiated, apply custom properties
// where object references can now also point to the new replacement instances.
foreach (var so in supers)
{
// Apply custom properties as messages to the instanced prefab
var props = so.GetComponent<SuperCustomProperties>();
if (props != null)
{
foreach (var p in props.m_Properties)
{
m_ObjectsById[so.m_Id].BroadcastProperty(p, m_ObjectsById);
}
}
}
// Finally, destroy replaced game objects.
foreach (var go in goToDestroy)
{
DestroyImmediate(go);
}
}
private void DoCustomImporting()
{
ApplyUserImporter();
ApplyAutoImporters();
}
private void ApplyUserImporter()
{
if (!string.IsNullOrEmpty(m_CustomImporterClassName))
{
var type = AppDomain.CurrentDomain.GetTypeFromName(m_CustomImporterClassName);
if (type == null)
{
ReportGenericError($"Custom Importer error. Class type '{m_CustomImporterClassName}' is missing.");
return;
}
RunCustomImporterType(type);
}
}
private void ApplyAutoImporters()
{
foreach (var type in AutoCustomTmxImporterAttribute.GetOrderedAutoImportersTypes())
{
RunCustomImporterType(type);
}
}
private void RunCustomImporterType(Type type)
{
// Instantiate a custom importer class for specialized projects to use
CustomTmxImporter customImporter;
try
{
customImporter = Activator.CreateInstance(type) as CustomTmxImporter;
}
catch (Exception e)
{
ReportGenericError($"Error creating custom importer class. Message = '{e.Message}'\n{e.StackTrace}");
return;
}
try
{
var args = new TmxAssetImportedArgs();
args.AssetImporter = this;
args.ImportedSuperMap = m_MapComponent;
customImporter.TmxAssetImported(args);
}
catch (CustomImporterException cie)
{
ReportGenericError($"Custom Importer error: \n Importer: {customImporter.GetType().Name}\n Message: {cie.Message}");
Debug.LogErrorFormat("Custom Importer ({0}) exception: {1}", customImporter.GetType().Name, cie.Message);
}
catch (Exception e)
{
ReportGenericError($"Custom importer '{customImporter.GetType().Name}' threw an exception. Message = '{e.Message}', Stack:\n{e.StackTrace}");
Debug.LogErrorFormat("Custom importer general exception: {0}", e.Message);
}
}
}
}