/* Advanced Polygon Collider (c) 2015 Digital Ruby, LLC http://www.digitalruby.com Source code may not be redistributed. Use in apps and games is fine. */ using System; using System.Collections; using System.Collections.Generic; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif namespace DigitalRuby.AdvancedPolygonCollider { public struct PolygonParameters { /// /// Texture - must be readable and writeable. /// public Texture2D Texture; /// /// Source rect from the texture containing the sprite. /// public Rect Rect; /// /// Offset (pivot) for the sprite /// public Vector2 Offset; /// /// X multiplier if pixels per unit are used, otherwise 1 (see UpdatePolygonCollider method). /// public float XMultiplier; /// /// Y multiplier if pixels per unit are used, otherwise 1 (see UpdatePolygonCollider method). /// public float YMultiplier; /// /// Alpha tolerance. Pixels with greater than this are considered solid. /// public byte AlphaTolerance; /// /// Distance threshold to collapse vertices in pixels. /// public int DistanceThreshold; /// /// True to decompose into convex polygons, false otherwise. /// public bool Decompose; /// /// Whether to use the cache. Values will be cached accordingly. /// public bool UseCache; public override int GetHashCode() { int h = Texture.GetHashCode(); if (h == 0) { h = 1; } return h * (int)(Rect.GetHashCode() * XMultiplier * YMultiplier * AlphaTolerance * Mathf.Max(DistanceThreshold, 1) * (Decompose ? 2 : 1)); } public override bool Equals(object obj) { if (obj is PolygonParameters) { PolygonParameters p = (PolygonParameters)obj; return Texture == p.Texture && Rect == p.Rect && XMultiplier == p.XMultiplier && YMultiplier == p.YMultiplier && AlphaTolerance == p.AlphaTolerance && DistanceThreshold == p.DistanceThreshold && Decompose == p.Decompose; } return false; } } [RequireComponent(typeof(PolygonCollider2D))] [RequireComponent(typeof(SpriteRenderer))] [ExecuteInEditMode] public class AdvancedPolygonCollider : MonoBehaviour { [Serializable] public struct ArrayWrapper { [SerializeField] public Vector2[] Array; } [Serializable] public struct ListWrapper { [SerializeField] public List List; } [Serializable] public struct CacheEntry { [SerializeField] public CacheKey Key; [SerializeField] public ListWrapper Value; } [Serializable] public struct CacheKey { [SerializeField] public Texture2D Texture; [SerializeField] public Rect Rect; public override int GetHashCode() { return Texture.GetHashCode() * Rect.GetHashCode(); } public override bool Equals(object obj) { if (obj is CacheKey) { CacheKey k = (CacheKey)obj; return (Texture == k.Texture && Rect == k.Rect); } return false; } } [Tooltip("Pixels with alpha greater than this count as solid.")] [Range(0, 254)] public byte AlphaTolerance = 20; [Tooltip("Points further away than this number of pixels will be consolidated.")] [Range(0, 64)] public int DistanceThreshold = 8; [Tooltip("Scale of the polygon.")] [Range(0.5f, 2.0f)] public float Scale = 1.0f; [Tooltip("Whether to decompse vertices into convex only polygons.")] public bool Decompose = false; [Tooltip("Whether to live update everything when in play mode. Typically for performance this can be false, " + "but if you plan on making changes to the sprite or parameters at runtime, you will want to set this to true.")] public bool RunInPlayMode = false; [Tooltip("True to use the cache, false otherwise. The cache is populated in editor and play mode and uses the most recent geometry " + "for a texture and rect regardless of other parameters. When ignoring the cache, values will not be added to the cache either. Cache is " + "only useful if you will be changing your sprite at run-time (i.e. animation)")] public bool UseCache; private SpriteRenderer spriteRenderer; private PolygonCollider2D polygonCollider; private bool dirty; [SerializeField] [HideInInspector] private byte lastAlphaTolerance; [SerializeField] [HideInInspector] private float lastScale; [SerializeField] [HideInInspector] private int lastDistanceThreshold; [SerializeField] [HideInInspector] private bool lastDecompose; [SerializeField] [HideInInspector] private Sprite lastSprite; [SerializeField] [HideInInspector] private Rect lastRect = new Rect(); [SerializeField] [HideInInspector] private Vector2 lastOffset = new Vector2(-99999.0f, -99999.0f); [SerializeField] [HideInInspector] private float lastPixelsPerUnit; [SerializeField] [HideInInspector] private bool lastFlipX; [SerializeField] [HideInInspector] private bool lastFlipY; private static readonly Dictionary> cache = new Dictionary>(); [Tooltip("All the cached objects from the editor. Do not modify this data.")] [SerializeField] private List editorCache = new List(); // private readonly AdvancedPolygonColliderAutoGeometry geometryDetector = new AdvancedPolygonColliderAutoGeometry(); private readonly TextureConverter geometryDetector = new TextureConverter(); #if UNITY_EDITOR private Texture2D blackBackground; #endif private void Awake() { if (Application.isPlaying) { // move editor cache to regular cache foreach (var v in editorCache) { List list = new List(); cache[v.Key] = list; foreach (ArrayWrapper w in v.Value.List) { list.Add(w.Array); } } } } private void Start() { #if UNITY_EDITOR blackBackground = new Texture2D(1, 1); blackBackground.SetPixel(0, 0, new Color(0.0f, 0.0f, 0.0f, 0.8f)); blackBackground.Apply(); #endif polygonCollider = GetComponent(); spriteRenderer = GetComponent(); } private void UpdateDirtyState() { if (spriteRenderer.sprite != lastSprite) { lastSprite = spriteRenderer.sprite; dirty = true; } if (spriteRenderer.sprite != null) { if (lastOffset != spriteRenderer.sprite.pivot) { lastOffset = spriteRenderer.sprite.pivot; dirty = true; } if (lastRect != spriteRenderer.sprite.rect) { lastRect = spriteRenderer.sprite.rect; dirty = true; } if (lastPixelsPerUnit != spriteRenderer.sprite.pixelsPerUnit) { lastPixelsPerUnit = spriteRenderer.sprite.pixelsPerUnit; dirty = true; } if (lastFlipX != spriteRenderer.flipX) { lastFlipX = spriteRenderer.flipX; dirty = true; } if (lastFlipY != spriteRenderer.flipY) { lastFlipY = spriteRenderer.flipY; dirty = true; } } if (AlphaTolerance != lastAlphaTolerance) { lastAlphaTolerance = AlphaTolerance; dirty = true; } if (Scale != lastScale) { lastScale = Scale; dirty = true; } if (DistanceThreshold != lastDistanceThreshold) { lastDistanceThreshold = DistanceThreshold; dirty = true; } if (Decompose != lastDecompose) { lastDecompose = Decompose; dirty = true; } } private void Update() { if (Application.isPlaying) { if (!RunInPlayMode) { return; } } else if (!UseCache) { editorCache.Clear(); } UpdateDirtyState(); if (dirty) { RecalculatePolygon(); } } #if UNITY_EDITOR private void OnDrawGizmos() { UnityEditor.Handles.BeginGUI(); GUI.color = Color.white; string text = " Vertices: " + VerticesCount + " "; var view = UnityEditor.SceneView.currentDrawingSceneView; Vector3 screenPos = view.camera.WorldToScreenPoint(gameObject.transform.position); Vector2 size = GUI.skin.label.CalcSize(new GUIContent(text)); GUI.skin.box.normal.background = blackBackground; Rect rect = new Rect(screenPos.x - (size.x / 2), -screenPos.y + view.position.height + 4, size.x, size.y); GUI.Box(rect, GUIContent.none); GUI.Label(rect, text); UnityEditor.Handles.EndGUI(); } private void AddEditorCache(ref PolygonParameters p, List list) { CacheKey key = new CacheKey(); key.Texture = p.Texture; key.Rect = p.Rect; CacheEntry e = new CacheEntry(); e.Key = key; e.Value = new ListWrapper(); e.Value.List = new List(); foreach (Vector2[] v in list) { ArrayWrapper w = new ArrayWrapper(); w.Array = v; e.Value.List.Add(w); } for (int i = 0; i < editorCache.Count; i++) { if (editorCache[i].Key.Equals(key)) { editorCache.RemoveAt(i); editorCache.Insert(i, e); return; } } editorCache.Add(e); } #endif public void RecalculatePolygon() { if (spriteRenderer.sprite != null) { PolygonParameters p = new PolygonParameters(); p.AlphaTolerance = AlphaTolerance; p.Decompose = Decompose; p.DistanceThreshold = DistanceThreshold; p.Rect = spriteRenderer.sprite.rect; p.Offset = spriteRenderer.sprite.pivot; p.Texture = spriteRenderer.sprite.texture; p.XMultiplier = (spriteRenderer.sprite.rect.width * 0.5f) / spriteRenderer.sprite.pixelsPerUnit; p.YMultiplier = (spriteRenderer.sprite.rect.height * 0.5f) / spriteRenderer.sprite.pixelsPerUnit; p.UseCache = UseCache; UpdatePolygonCollider(ref p); } } public void UpdatePolygonCollider(ref PolygonParameters p) { if (spriteRenderer.sprite == null || spriteRenderer.sprite.texture == null) { return; } dirty = false; List cached; if (Application.isPlaying && p.UseCache) { CacheKey key = new CacheKey(); key.Texture = p.Texture; key.Rect = p.Rect; if (cache.TryGetValue(key, out cached)) { polygonCollider.pathCount = cached.Count; for (int i = 0; i < cached.Count; i++) { polygonCollider.SetPath(i, cached[i]); } return; } } PopulateCollider(polygonCollider, ref p); } public int VerticesCount { get { return (polygonCollider == null ? 0 : polygonCollider.GetTotalPointCount()); } } /// /// Populate the vertices of a collider /// /// Collider to setup vertices in. /// Polygon creation parameters public void PopulateCollider(PolygonCollider2D collider, ref PolygonParameters p) { try { if (p.Texture.format != TextureFormat.ARGB32 && p.Texture.format != TextureFormat.BGRA32 && p.Texture.format != TextureFormat.RGBA32 && p.Texture.format != TextureFormat.RGB24 && p.Texture.format != TextureFormat.Alpha8 && p.Texture.format != TextureFormat.RGBAFloat && p.Texture.format != TextureFormat.RGBAHalf && p.Texture.format != TextureFormat.RGB565) { Debug.LogWarning("Advanced Polygon Collider works best with a non-compressed texture in ARGB32, BGRA32, RGB24, RGBA4444, RGB565, RGBAFloat or RGBAHalf format"); } int width = (int)p.Rect.width; int height = (int)p.Rect.height; int x = (int)p.Rect.x; int y = (int)p.Rect.y; UnityEngine.Color[] pixels = p.Texture.GetPixels(x, y, width, height, 0); List verts = geometryDetector.DetectVertices(pixels, width, p.AlphaTolerance); int pathIndex = 0; List list = new List(); for (int i = 0; i < verts.Count; i++) { ProcessVertices(collider, verts[i], list, ref p, ref pathIndex); } #if UNITY_EDITOR if (Application.isPlaying) { #endif if (p.UseCache) { CacheKey key = new CacheKey(); key.Texture = p.Texture; key.Rect = p.Rect; cache[key] = list; } #if UNITY_EDITOR } else if (p.UseCache) { AddEditorCache(ref p, list); } #endif Debug.Log("Updated polygon."); } catch (Exception ex) { Debug.LogError("Error creating collider: " + ex); } } private List ProcessVertices(PolygonCollider2D collider, Vertices v, List list, ref PolygonParameters p, ref int pathIndex) { Vector2 offset = p.Offset; float flipXMultiplier = (spriteRenderer.flipX ? -1.0f : 1.0f); float flipYMultiplier = (spriteRenderer.flipY ? -1.0f : 1.0f); if (p.DistanceThreshold > 1) { v = SimplifyTools.DouglasPeuckerSimplify (v, p.DistanceThreshold); } if (p.Decompose) { List> points = BayazitDecomposer.ConvexPartition(v); for (int j = 0; j < points.Count; j++) { List v2 = points[j]; for (int i = 0; i < v2.Count; i++) { float xValue = (2.0f * (((v2[i].x - offset.x) + 0.5f) / p.Rect.width)); float yValue = (2.0f * (((v2[i].y - offset.y) + 0.5f) / p.Rect.height)); v2[i] = new Vector2(xValue * p.XMultiplier * Scale * flipXMultiplier, yValue * p.YMultiplier * Scale * flipYMultiplier); } Vector2[] arr = v2.ToArray(); collider.pathCount = pathIndex + 1; collider.SetPath(pathIndex++, arr); list.Add(arr); } } else { collider.pathCount = pathIndex + 1; for (int i = 0; i < v.Count; i++) { float xValue = (2.0f * (((v[i].x - offset.x) + 0.5f) / p.Rect.width)); float yValue = (2.0f * (((v[i].y - offset.y) + 0.5f) / p.Rect.height)); v[i] = new Vector2(xValue * p.XMultiplier * Scale * flipXMultiplier, yValue * p.YMultiplier * Scale * flipYMultiplier); } Vector2[] arr = v.ToArray(); collider.SetPath(pathIndex++, arr); list.Add(arr); } return list; } } }