evershade/Packages/super-tiled2unity.v2.2.0/Editor/Loaders/TilesetLoader.cs

507 lines
22 KiB
C#

using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using UnityEditor;
using UnityEngine;
namespace SuperTiled2Unity.Editor
{
// Tilesets can be stand-alone TSX files (preferred) or embedded in Tiled Map Editor files (TMX)
// This helper loader class gives us the flexibility we need to load tileset data
public class TilesetLoader
{
private readonly SuperTileset m_SuperTileset;
private readonly TiledAssetImporter m_Importer;
public const string SpriteNameRoot = "st2u_";
// Make this an ugly name that shouldn't collide with sprites named by the user
internal static string RectToSpriteName(Rect rect)
{
return $"{SpriteNameRoot}x{rect.x}y{rect.y}-w{rect.width}h{rect.height}";
}
public TilesetLoader(SuperTileset tileset, TiledAssetImporter importer)
{
m_SuperTileset = tileset;
m_Importer = importer;
}
public bool LoadFromXml(XElement xTileset)
{
// There are attributes we *must* have that older versions of Tiled did not serialize
if (xTileset.Attribute("tilecount") == null || xTileset.Attribute("columns") == null)
{
m_Importer.ReportGenericError("Old Tiled file format detected. You must save this file with a newer verion of Tiled (At least 0.15).");
return false;
}
ProcessAttributes(xTileset);
BuildTileset(xTileset);
ProcessTileElements(xTileset);
return true;
}
private void ProcessAttributes(XElement xTileset)
{
m_SuperTileset.name = xTileset.GetAttributeAs<string>("name");
m_SuperTileset.m_TileWidth = xTileset.GetAttributeAs<int>("tilewidth");
m_SuperTileset.m_TileHeight = xTileset.GetAttributeAs<int>("tileheight");
m_SuperTileset.m_Spacing = xTileset.GetAttributeAs<int>("spacing");
m_SuperTileset.m_Margin = xTileset.GetAttributeAs<int>("margin");
m_SuperTileset.m_TileCount = xTileset.GetAttributeAs<int>("tilecount");
m_SuperTileset.m_TileColumns = xTileset.GetAttributeAs<int>("columns");
m_SuperTileset.m_ObjectAlignment = xTileset.GetAttributeAs<ObjectAlignment>("objectalignment", ObjectAlignment.Unspecified);
m_SuperTileset.m_TileRenderSize = xTileset.GetAttributeAs<TileRenderSize>("tilerendersize", TileRenderSize.Tile);
m_SuperTileset.m_FillMode = xTileset.GetAttributeAs<FillMode>("fillmode", FillMode.Stretch);
var xTileOffset = xTileset.Element("tileoffset");
if (xTileOffset != null)
{
var x = xTileOffset.GetAttributeAs<float>("x", 0.0f);
var y = xTileOffset.GetAttributeAs<float>("y", 0.0f);
m_SuperTileset.m_TileOffset = new Vector2(x, y);
}
var xGrid = xTileset.Element("grid");
if (xGrid != null)
{
m_SuperTileset.m_GridOrientation = xGrid.GetAttributeAs<GridOrientation>("orientation");
var w = xGrid.GetAttributeAs<float>("width", 0.0f);
var h = xGrid.GetAttributeAs<float>("height", 0.0f);
m_SuperTileset.m_GridSize = new Vector2(w, h);
}
m_SuperTileset.m_CustomProperties = CustomPropertyLoader.LoadCustomPropertyList(xTileset.Element("properties"));
}
private void BuildTileset(XElement xTileset)
{
// Build the initial database of tiles and the image components that make them
// There are two ways that our collection of tiles can be created from images
// 1) From one image broken down into parts (many tiles in one image)
// 2) From a collection of images (one tile per image)
if (xTileset.Element("image") != null)
{
BuildTilesetFromImage(xTileset);
}
else
{
BuildTilesetFromCollection(xTileset);
}
}
private void BuildTilesetFromImage(XElement xTileset)
{
m_SuperTileset.m_IsImageCollection = false;
XElement xImage = xTileset.Element("image");
string textureLocalPath = xImage.GetAttributeAs<string>("source");
int textureWidth = xImage.GetAttributeAs<int>("width");
int textureHeight = xImage.GetAttributeAs<int>("height");
bool forceErrorTiles = false;
// Load the texture. We will make sprites and tiles out of this image.
var tex2d = m_Importer.RequestDependencyAssetAtPath<Texture2D>(textureLocalPath);
var textureAssetPath = string.Empty;
var sprites = new Dictionary<(Rect, Vector2), Sprite>();
if (tex2d == null)
{
m_Importer.ReportMissingDependency(textureLocalPath);
forceErrorTiles = true;
}
else
{
textureAssetPath = AssetDatabase.GetAssetPath(tex2d);
// The pixels per unit of the sprites must match the pixels per unit of the tileset
sprites = AssetDatabase.LoadAllAssetsAtPath(textureAssetPath).OfType<Sprite>().SafeToDictionary(s => (s.rect, s.pivot), s => s);
if (sprites.Any())
{
var firstSprite = sprites.First().Value;
if (firstSprite.pixelsPerUnit != m_SuperTileset.m_PixelsPerUnit)
{
m_Importer.ReportWrongPixelsPerUnit(textureAssetPath, firstSprite.pixelsPerUnit, m_SuperTileset.m_PixelsPerUnit);
forceErrorTiles = true;
}
}
}
for (int i = 0; i < m_SuperTileset.m_TileCount; i++)
{
// Get grid x,y coords
int x = i % m_SuperTileset.m_TileColumns;
int y = i / m_SuperTileset.m_TileColumns;
int tileWidth = m_SuperTileset.m_TileWidth;
int tileHeight = m_SuperTileset.m_TileHeight;
// Get x source on texture
int srcx = x * tileWidth;
srcx += x * m_SuperTileset.m_Spacing;
srcx += m_SuperTileset.m_Margin;
// Get y source on texture
int srcy = y * tileHeight;
srcy += y * m_SuperTileset.m_Spacing;
srcy += m_SuperTileset.m_Margin;
// In Tiled, texture origin is the top-left. However, in Unity the origin is bottom-left.
srcy = (textureHeight - srcy) - tileHeight;
if (forceErrorTiles || !TryAddTile(i, srcx, srcy, tileWidth, tileHeight, sprites))
{
if (!string.IsNullOrEmpty(textureAssetPath))
{
m_Importer.ReportMissingSprite(textureAssetPath, i, srcx, srcy, tileWidth, tileHeight);
}
AddErrorTile(i, NamedColors.HotPink, tileWidth, tileHeight);
}
}
}
private void BuildTilesetFromCollection(XElement xTileset)
{
m_SuperTileset.m_IsImageCollection = true;
foreach (var xTile in xTileset.Elements("tile"))
{
int tileIndex = xTile.GetAttributeAs<int>("id");
var xImage = xTile.Element("image");
if (xImage != null)
{
string textureLocalPath = xImage.GetAttributeAs<string>("source");
int texture_w = xImage.GetAttributeAs<int>("width");
int texture_h = xImage.GetAttributeAs<int>("height");
bool forceErrorTiles = false;
var tex2d = m_Importer.RequestDependencyAssetAtPath<Texture2D>(textureLocalPath);
var textureAssetPath = string.Empty;
var sprites = new Dictionary<(Rect, Vector2), Sprite>();
// Load the texture. We will make sprites and tiles out of this image.
if (tex2d == null)
{
m_Importer.ReportMissingDependency(textureLocalPath);
forceErrorTiles = true;
}
else
{
textureAssetPath = AssetDatabase.GetAssetPath(tex2d);
// The pixels per unit of the sprites must match the pixels per unit of the tileset
sprites = AssetDatabase.LoadAllAssetsAtPath(textureAssetPath).OfType<Sprite>().SafeToDictionary(s => (s.rect, s.pivot), s => s);
if (sprites.Any())
{
var firstSprite = sprites.First().Value;
if (firstSprite.pixelsPerUnit != m_SuperTileset.m_PixelsPerUnit)
{
m_Importer.ReportWrongPixelsPerUnit(textureAssetPath, firstSprite.pixelsPerUnit, m_SuperTileset.m_PixelsPerUnit);
forceErrorTiles = true;
}
}
}
int tile_x = xTile.GetAttributeAs<int>("x", 0);
int tile_y = xTile.GetAttributeAs<int>("y", 0);
int tile_w = xTile.GetAttributeAs<int>("width", texture_w);
int tile_h = xTile.GetAttributeAs<int>("height", texture_h);
if (forceErrorTiles || !TryAddTile(tileIndex, tile_x, tile_y, tile_w, tile_h, sprites))
{
if (!string.IsNullOrEmpty(textureAssetPath))
{
m_Importer.ReportMissingSprite(textureAssetPath, tileIndex, tile_x, tile_y, tile_w, tile_h);
}
AddErrorTile(tileIndex, NamedColors.DeepPink, tile_w, tile_h);
}
}
}
}
private bool TryAddTile(int tileId, int x, int y, int width, int height, Dictionary<(Rect, Vector2), Sprite> sprites)
{
var rect = new Rect(x, y, width, height);
if (sprites.TryGetValue((rect, Vector2.zero), out Sprite tileSprite))
{
// Create the tile that uses the sprite
var tile = SuperTile.CreateSuperTile();
tile.m_TileId = tileId;
tile.name = tileSprite.name;
tile.hideFlags = HideFlags.HideInHierarchy;
tile.m_Sprite = tileSprite;
tile.m_Width = rect.width;
tile.m_Height = rect.height;
tile.m_TileOffsetX = m_SuperTileset.m_TileOffset.x;
tile.m_TileOffsetY = m_SuperTileset.m_TileOffset.y;
tile.m_ObjectAlignment = m_SuperTileset.m_ObjectAlignment;
tile.m_TileRenderSize = m_SuperTileset.m_TileRenderSize;
tile.m_FillMode = m_SuperTileset.m_FillMode;
if (m_Importer is TsxAssetImporter tsxAssetImporter)
{
tile.m_ColliderType = tsxAssetImporter.m_ColliderType;
}
m_SuperTileset.m_Tiles.Add(tile);
m_Importer.SuperImportContext.AddObjectToAsset($"Tile{tileId}_spriteName", tile);
return true;
}
return false;
}
private void AddErrorTile(int tileId, Color tint, int width, int height)
{
BadTileSpriteProvider.instance.CreateSpriteAndTile(tileId, tint, width, height, m_SuperTileset, out Sprite sprite, out SuperBadTile tile);
sprite.hideFlags = HideFlags.HideInHierarchy;
tile.hideFlags = HideFlags.HideInHierarchy;
m_SuperTileset.m_Tiles.Add(tile);
m_Importer.SuperImportContext.AddObjectToAsset(sprite.name, sprite);
m_Importer.SuperImportContext.AddObjectToAsset(tile.name, tile);
}
private void ProcessTileElements(XElement xTileset)
{
// Additional processing on tiles
foreach (var xTile in xTileset.Elements("tile"))
{
ProcessTileElement(xTile);
}
}
private void ProcessTileElement(XElement xTile)
{
int index = xTile.GetAttributeAs<int>("id", -1);
if (m_SuperTileset.TryGetTile(index, out SuperTile tile))
{
// A tile may have a class associated with it
tile.m_Type = xTile.GetAttributeAs("class", "");
// As of Tiled 1.9 types have been merged with classes
// As a simple way to support both version we can fall back like this to the old < 1.9 way
if (string.IsNullOrWhiteSpace(tile.m_Type))
{
tile.m_Type = xTile.GetAttributeAs("type", "");
}
// Tiles can have custom properties (and properties inherited from their Type)
tile.m_CustomProperties = CustomPropertyLoader.LoadCustomPropertyList(xTile.Element("properties"));
tile.m_CustomProperties.AddPropertiesFromType(tile.m_Type, m_Importer.SuperImportContext);
// Does the tile have any animation data?
var xAnimation = xTile.Element("animation");
if (xAnimation != null)
{
ProcessAnimationElement(tile, xAnimation);
}
// Does the tile have an object group?
var xObjectGroup = xTile.Element("objectgroup");
if (xObjectGroup != null && xObjectGroup.HasElements)
{
ProcessObjectGroupElement(tile, xObjectGroup);
}
}
}
private void ProcessAnimationElement(SuperTile tile, XElement xAnimation)
{
var fps = ST2USettings.instance.m_AnimationFramerate;
var animations = new AnimationBuilder(fps);
foreach (var xFrame in xAnimation.Elements("frame"))
{
var frameId = xFrame.GetAttributeAs<int>("tileid");
var frameDuration = xFrame.GetAttributeAs<int>("duration") / 1000.0f;
if (m_SuperTileset.TryGetTile(frameId, out SuperTile frame))
{
animations.AddFrames(frame.m_Sprite, frameDuration);
}
}
tile.m_AnimationSprites = animations.Sprites.ToArray();
}
private void ProcessObjectGroupElement(SuperTile tile, XElement xObjectGroup)
{
// Object groups on tiles come from the Tiled Collision Editor
if (xObjectGroup.Elements().Any())
{
// We'll be adding collision objects to our tile
tile.m_CollisionObjects = new List<CollisionObject>();
foreach (var xObject in xObjectGroup.Elements("object"))
{
var collision = new CollisionObject();
// Template may fill in a bunch of properties
m_Importer.ApplyTemplateToObject(xObject);
collision.m_ObjectId = xObject.GetAttributeAs("id", 0);
collision.m_ObjectName = xObject.GetAttributeAs("name", string.Format("Object_{0}", collision.m_ObjectId));
// As of Tiled 1.9 types have been merged with classes
// As a simple way to support both version we can fall back like this to the old < 1.9 way
collision.m_ObjectType = xObject.GetAttributeAs("class", "");
if (string.IsNullOrWhiteSpace(collision.m_ObjectType))
{
collision.m_ObjectType = xObject.GetAttributeAs("type", "");
}
collision.m_Position.x = xObject.GetAttributeAs("x", 0.0f);
collision.m_Position.y = xObject.GetAttributeAs("y", 0.0f);
collision.m_Size.x = xObject.GetAttributeAs("width", 0.0f);
collision.m_Size.y = xObject.GetAttributeAs("height", 0.0f);
collision.m_Rotation = xObject.GetAttributeAs("rotation", 0.0f);
// Are there any properties on the collision object?
collision.m_CustomProperties = CustomPropertyLoader.LoadCustomPropertyList(xObject.Element("properties"));
collision.m_CustomProperties.AddPropertiesFromType(collision.m_ObjectType, m_Importer.SuperImportContext);
var xPolygon = xObject.Element("polygon");
var xPolyline = xObject.Element("polyline");
if (xPolygon != null)
{
// Get points and make sure they are CCW
var points = xPolygon.GetAttributeAsVector2Array("points");
if (PolygonUtils.SumOverEdges(points) < 0)
{
points = points.Reverse().ToArray();
}
collision.MakePointsFromPolygon(points);
}
else if (xPolyline != null)
{
// Get points and make sure they are CCW
var points = xPolyline.GetAttributeAsVector2Array("points");
if (PolygonUtils.SumOverEdges(points) < 0)
{
points = points.Reverse().ToArray();
}
collision.MakePointsFromPolyline(points);
}
else if (xObject.Element("ellipse") != null)
{
if (collision.m_Size.x == 0)
{
m_Importer.ReportGenericError($"Invalid ellipse (Tile ID ='{tile.m_TileId}') in tileset '{m_SuperTileset.name}' has zero width");
}
else if (collision.m_Size.y == 0)
{
m_Importer.ReportGenericError($"Invalid ellipse (Tile ID ='{tile.m_TileId}') in tileset '{m_SuperTileset.name}' has zero height");
}
else
{
collision.MakePointsFromEllipse(m_Importer.EdgesPerEllipse);
}
}
else if (xObject.Element("point") != null)
{
collision.MakePoint();
}
else
{
// By default, objects are rectangles
if (collision.m_Size.x == 0)
{
m_Importer.ReportGenericError($"Invalid rectangle (Tile ID ='{tile.m_TileId}') in tileset '{m_SuperTileset.name}' has zero width");
}
else if (collision.m_Size.y == 0)
{
m_Importer.ReportGenericError($"Invalid rectangle (Tile ID ='{tile.m_TileId}') in tileset '{m_SuperTileset.name}' has zero height");
}
else
{
collision.MakePointsFromRectangle();
}
}
// Do not add if there are no points
if (!collision.Points.IsEmpty())
{
AssignCollisionObjectProperties(collision, tile);
collision.RenderPoints(tile, m_SuperTileset.m_GridOrientation, m_SuperTileset.m_GridSize);
tile.m_CollisionObjects.Add(collision);
}
}
}
}
private void AssignCollisionObjectProperties(CollisionObject collision, SuperTile tile)
{
// Check properties for layer name
var layerProperty = GetCollisionOrTileOrTilesetProperty(collision, tile, StringConstants.Unity_Layer);
if (layerProperty != null)
{
// Explicit request to assign a layer to a collision. Report errors if the layer is missing.
m_Importer.CheckLayerName(layerProperty.m_Value);
collision.m_PhysicsLayer = layerProperty.m_Value;
}
else if (!string.IsNullOrEmpty(collision.m_ObjectName))
{
// Try to go off of object name but layer must exist (and we don't check for it)
if (LayerMask.NameToLayer(collision.m_ObjectName) != -1)
{
collision.m_PhysicsLayer = collision.m_ObjectName;
}
}
if (string.IsNullOrEmpty(collision.m_PhysicsLayer))
{
collision.m_PhysicsLayer = "Default";
}
// Check properties for trigger setting
var triggerProperty = GetCollisionOrTileOrTilesetProperty(collision, tile, StringConstants.Unity_IsTrigger);
if (triggerProperty != null)
{
collision.m_IsTrigger = triggerProperty.GetValueAsBool();
}
}
private CustomProperty GetCollisionOrTileOrTilesetProperty(CollisionObject collision, SuperTile tile, string propertyName)
{
CustomProperty property;
// Get property off the collision object
if (collision.m_CustomProperties.TryGetProperty(propertyName, out property))
{
return property;
}
// Get property off the tile
if (tile.m_CustomProperties.TryGetProperty(propertyName, out property))
{
return property;
}
// Inherit property from the tileset
if (m_SuperTileset.m_CustomProperties.TryGetProperty(propertyName, out property))
{
return property;
}
return null;
}
}
}