- Simulated "fake" physics and collisions - Object pooling for tiles, obstacles and monster spawns - Base monster scoring with proximity triggers and depth multiplier Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #5
912 lines
35 KiB
C#
912 lines
35 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Editor.Utilities
|
|
{
|
|
public class SpriteColliderGenerator : EditorWindow
|
|
{
|
|
private Vector2 scrollPosition;
|
|
|
|
[Tooltip("List of GameObjects with SpriteRenderers to generate colliders for")]
|
|
private List<GameObject> selectedObjects = new List<GameObject>();
|
|
|
|
[Tooltip("Controls how much to simplify the collider shape (lower values create more complex colliders)")]
|
|
private float simplificationTolerance = 0.05f;
|
|
|
|
[Tooltip("When enabled, removes any existing PolygonCollider2D components before adding new ones")]
|
|
private bool replaceExistingColliders = true;
|
|
|
|
[Tooltip("When enabled, applies colliders to all child objects with SpriteRenderers")]
|
|
private bool applyToChildren = false;
|
|
|
|
[Tooltip("When enabled, allows scaling the collider outward or inward from the sprite center")]
|
|
private bool offsetFromCenter = false;
|
|
|
|
[Tooltip("Distance to offset the collider from the sprite outline (positive values expand, negative values contract)")]
|
|
private float offsetDistance = 0f;
|
|
|
|
[Tooltip("When enabled, creates trigger colliders instead of solid colliders")]
|
|
private bool generateTriggerColliders = false;
|
|
|
|
[Tooltip("Threshold for transparency detection (pixels with alpha below this value are considered transparent)")]
|
|
private int alphaCutoff = 128; // Used when generating colliders (0-255)
|
|
|
|
[Tooltip("Controls the level of detail for the generated collider (affects vertex count)")]
|
|
private int detailLevel = 2; // 1 = low, 2 = medium, 3 = high
|
|
|
|
[Tooltip("When enabled, shows a preview of the colliders in the scene view before generating them")]
|
|
private bool previewColliders = true;
|
|
|
|
[Tooltip("Color used for previewing colliders in the scene view")]
|
|
private Color previewColor = new Color(0.2f, 1f, 0.3f, 0.5f);
|
|
|
|
[Tooltip("Layer to assign to GameObjects when colliders are generated")]
|
|
private int targetLayer = 0;
|
|
|
|
private List<Mesh> previewMeshes = new List<Mesh>();
|
|
|
|
[MenuItem("Tools/Sprite Collider Generator")]
|
|
public static void ShowWindow()
|
|
{
|
|
GetWindow<SpriteColliderGenerator>("Sprite Collider Generator");
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
// Subscribe to scene change events to clear invalid object references
|
|
UnityEditor.SceneManagement.EditorSceneManager.sceneOpened += OnSceneOpened;
|
|
UnityEditor.SceneManagement.EditorSceneManager.sceneClosed += OnSceneClosed;
|
|
|
|
// Also subscribe to playmode changes as they can invalidate references
|
|
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
|
|
|
// Subscribe to prefab stage changes (Unity 2018.3+)
|
|
UnityEditor.SceneManagement.PrefabStage.prefabStageOpened += OnPrefabStageOpened;
|
|
UnityEditor.SceneManagement.PrefabStage.prefabStageClosing += OnPrefabStageClosing;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
// Unsubscribe from events
|
|
UnityEditor.SceneManagement.EditorSceneManager.sceneOpened -= OnSceneOpened;
|
|
UnityEditor.SceneManagement.EditorSceneManager.sceneClosed -= OnSceneClosed;
|
|
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
|
UnityEditor.SceneManagement.PrefabStage.prefabStageOpened -= OnPrefabStageOpened;
|
|
UnityEditor.SceneManagement.PrefabStage.prefabStageClosing -= OnPrefabStageClosing;
|
|
|
|
// Clean up any preview meshes when window is closed
|
|
foreach (var mesh in previewMeshes)
|
|
{
|
|
if (mesh != null)
|
|
{
|
|
DestroyImmediate(mesh);
|
|
}
|
|
}
|
|
previewMeshes.Clear();
|
|
}
|
|
|
|
private void OnSceneOpened(UnityEngine.SceneManagement.Scene scene, UnityEditor.SceneManagement.OpenSceneMode mode)
|
|
{
|
|
// Clear selected objects when a scene is opened
|
|
ClearInvalidReferences();
|
|
}
|
|
|
|
private void OnSceneClosed(UnityEngine.SceneManagement.Scene scene)
|
|
{
|
|
// Clear selected objects when a scene is closed
|
|
ClearInvalidReferences();
|
|
}
|
|
|
|
private void OnPlayModeStateChanged(PlayModeStateChange state)
|
|
{
|
|
// Clear references when entering/exiting play mode as they become invalid
|
|
if (state == PlayModeStateChange.ExitingEditMode || state == PlayModeStateChange.ExitingPlayMode)
|
|
{
|
|
ClearInvalidReferences();
|
|
}
|
|
}
|
|
|
|
private void OnPrefabStageOpened(UnityEditor.SceneManagement.PrefabStage stage)
|
|
{
|
|
// Clear selected objects when entering a prefab stage
|
|
ClearInvalidReferences();
|
|
}
|
|
|
|
private void OnPrefabStageClosing(UnityEditor.SceneManagement.PrefabStage stage)
|
|
{
|
|
// Clear selected objects when exiting a prefab stage
|
|
ClearInvalidReferences();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears invalid GameObject references from the selected objects list
|
|
/// </summary>
|
|
private void ClearInvalidReferences()
|
|
{
|
|
if (selectedObjects.Count > 0)
|
|
{
|
|
selectedObjects.Clear();
|
|
ClearPreviews(); // Also clear any preview meshes
|
|
Repaint(); // Refresh the window UI
|
|
}
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
EditorGUILayout.BeginVertical();
|
|
|
|
EditorGUILayout.LabelField("Sprite Collider Generator", EditorStyles.boldLabel);
|
|
EditorGUILayout.HelpBox("Select GameObjects with SpriteRenderers and generate accurate PolygonCollider2D components based on the sprite outlines.", MessageType.Info);
|
|
|
|
EditorGUILayout.Space();
|
|
|
|
// Object selection section
|
|
EditorGUILayout.LabelField("Selected Objects", EditorStyles.boldLabel);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
if (GUILayout.Button(new GUIContent("Add Selected GameObjects", "Add GameObjects currently selected in the scene or project to the list for processing.")))
|
|
{
|
|
AddSelectedGameObjects();
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(150));
|
|
|
|
for (int i = 0; i < selectedObjects.Count; i++)
|
|
{
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
selectedObjects[i] = (GameObject)EditorGUILayout.ObjectField(selectedObjects[i], typeof(GameObject), true);
|
|
|
|
if (GUILayout.Button("X", GUILayout.Width(20)))
|
|
{
|
|
selectedObjects.RemoveAt(i);
|
|
i--;
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
}
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
|
|
if (GUILayout.Button(new GUIContent("Clear All", "Remove all objects from the selection list.")))
|
|
{
|
|
selectedObjects.Clear();
|
|
}
|
|
|
|
EditorGUILayout.Space();
|
|
|
|
// Collider generation options
|
|
EditorGUILayout.LabelField("Generation Options", EditorStyles.boldLabel);
|
|
|
|
// Detail level for collider generation (affects vertex count)
|
|
string[] detailOptions = new string[] { "Low", "Medium", "High" };
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField(new GUIContent("Detail Level:", "Controls the level of detail for the generated collider (affects vertex count)."), GUILayout.Width(180));
|
|
detailLevel = EditorGUILayout.Popup(detailLevel - 1, detailOptions) + 1;
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
// Simplification tolerance (how much to simplify the collider)
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField(new GUIContent("Simplification Tolerance:", "Controls how much to simplify the collider shape (lower values create more complex colliders)."), GUILayout.Width(180));
|
|
simplificationTolerance = EditorGUILayout.Slider(simplificationTolerance, 0.01f, 0.2f);
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
// Alpha cutoff for transparency
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField(new GUIContent("Alpha Cutoff (0-255):", "Threshold for transparency detection (pixels with alpha below this value are considered transparent)."), GUILayout.Width(180));
|
|
alphaCutoff = EditorGUILayout.IntSlider(alphaCutoff, 0, 255);
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
EditorGUILayout.Space();
|
|
|
|
// Additional options
|
|
replaceExistingColliders = EditorGUILayout.Toggle(
|
|
new GUIContent("Replace Existing Colliders", "When enabled, removes any existing PolygonCollider2D components before adding new ones."),
|
|
replaceExistingColliders);
|
|
|
|
applyToChildren = EditorGUILayout.Toggle(
|
|
new GUIContent("Apply To Children", "When enabled, applies colliders to all child objects with SpriteRenderers."),
|
|
applyToChildren);
|
|
|
|
generateTriggerColliders = EditorGUILayout.Toggle(
|
|
new GUIContent("Generate Trigger Colliders", "When enabled, creates trigger colliders instead of solid colliders."),
|
|
generateTriggerColliders);
|
|
|
|
// Layer selection
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField(new GUIContent("Target Layer:", "Layer to assign to GameObjects when colliders are generated. Leave as 'Nothing' to keep current layer."), GUILayout.Width(180));
|
|
targetLayer = EditorGUILayout.LayerField(targetLayer);
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
// Offset option
|
|
offsetFromCenter = EditorGUILayout.Toggle(
|
|
new GUIContent("Offset From Center", "When enabled, allows scaling the collider outward or inward from the sprite center."),
|
|
offsetFromCenter);
|
|
|
|
if (offsetFromCenter)
|
|
{
|
|
EditorGUI.indentLevel++;
|
|
offsetDistance = EditorGUILayout.FloatField(
|
|
new GUIContent("Offset Distance", "Distance to offset the collider from the sprite outline (positive values expand, negative values contract)."),
|
|
offsetDistance);
|
|
EditorGUI.indentLevel--;
|
|
}
|
|
|
|
// Preview option
|
|
previewColliders = EditorGUILayout.Toggle(
|
|
new GUIContent("Preview Colliders", "When enabled, shows a preview of the colliders in the scene view before generating them."),
|
|
previewColliders);
|
|
|
|
if (previewColliders)
|
|
{
|
|
EditorGUI.indentLevel++;
|
|
previewColor = EditorGUILayout.ColorField(
|
|
new GUIContent("Preview Color", "Color used for previewing colliders in the scene view."),
|
|
previewColor);
|
|
|
|
// Create a horizontal layout for the preview buttons
|
|
EditorGUILayout.BeginHorizontal();
|
|
if (GUILayout.Button(new GUIContent("Update Preview", "Refresh the preview display in the scene view.")))
|
|
{
|
|
GenerateColliderPreviews();
|
|
}
|
|
if (GUILayout.Button(new GUIContent("Clear Preview", "Remove all preview colliders from the scene view.")))
|
|
{
|
|
ClearPreviews();
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
EditorGUI.indentLevel--;
|
|
}
|
|
|
|
EditorGUILayout.Space();
|
|
|
|
// Generate colliders button
|
|
GUI.enabled = selectedObjects.Count > 0;
|
|
|
|
if (GUILayout.Button(new GUIContent("Generate Colliders", "Create polygon colliders for all selected sprites based on current settings.")))
|
|
{
|
|
GenerateColliders();
|
|
}
|
|
|
|
GUI.enabled = true;
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
// Force the scene view to repaint if we're showing previews
|
|
if (previewColliders && Event.current.type == EventType.Repaint)
|
|
{
|
|
SceneView.RepaintAll();
|
|
}
|
|
}
|
|
|
|
private void AddSelectedGameObjects()
|
|
{
|
|
foreach (GameObject obj in Selection.gameObjects)
|
|
{
|
|
if (!selectedObjects.Contains(obj))
|
|
{
|
|
// Only add if it has a SpriteRenderer or any of its children do
|
|
if (obj.GetComponent<SpriteRenderer>() != null ||
|
|
(applyToChildren && obj.GetComponentInChildren<SpriteRenderer>() != null))
|
|
{
|
|
selectedObjects.Add(obj);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GenerateColliderPreviews()
|
|
{
|
|
// Clean up existing preview meshes
|
|
foreach (var mesh in previewMeshes)
|
|
{
|
|
if (mesh != null)
|
|
{
|
|
DestroyImmediate(mesh);
|
|
}
|
|
}
|
|
previewMeshes.Clear();
|
|
|
|
if (!previewColliders || selectedObjects.Count == 0)
|
|
return;
|
|
|
|
foreach (var obj in selectedObjects)
|
|
{
|
|
if (obj == null) continue;
|
|
|
|
var spriteRenderers = applyToChildren ?
|
|
obj.GetComponentsInChildren<SpriteRenderer>() :
|
|
new SpriteRenderer[] { obj.GetComponent<SpriteRenderer>() };
|
|
|
|
foreach (var renderer in spriteRenderers)
|
|
{
|
|
if (renderer == null || renderer.sprite == null)
|
|
continue;
|
|
|
|
Sprite sprite = renderer.sprite;
|
|
List<Vector2[]> paths = GetSpritePaths(sprite, simplificationTolerance);
|
|
if (paths.Count == 0)
|
|
continue;
|
|
|
|
foreach (var path in paths)
|
|
{
|
|
// Create a preview mesh from the path
|
|
Mesh previewMesh = CreateMeshFromPath(path, renderer.transform);
|
|
if (previewMesh != null)
|
|
previewMeshes.Add(previewMesh);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all preview meshes from the scene view
|
|
/// </summary>
|
|
private void ClearPreviews()
|
|
{
|
|
foreach (var mesh in previewMeshes)
|
|
{
|
|
if (mesh != null)
|
|
{
|
|
DestroyImmediate(mesh);
|
|
}
|
|
}
|
|
previewMeshes.Clear();
|
|
|
|
// Force a repaint of the scene view
|
|
SceneView.RepaintAll();
|
|
}
|
|
|
|
private Mesh CreateMeshFromPath(Vector2[] path, Transform transform)
|
|
{
|
|
if (path.Length < 3)
|
|
return null;
|
|
|
|
Mesh mesh = new Mesh();
|
|
|
|
// Convert the path to 3D vertices and apply the sprite's transform
|
|
Vector3[] vertices = new Vector3[path.Length];
|
|
for (int i = 0; i < path.Length; i++)
|
|
{
|
|
// Convert the local position to world space using the transform
|
|
vertices[i] = transform.TransformPoint(new Vector3(path[i].x, path[i].y, 0));
|
|
}
|
|
|
|
// Triangulate the polygon
|
|
Triangulator triangulator = new Triangulator(path);
|
|
int[] triangles = triangulator.Triangulate();
|
|
|
|
mesh.vertices = vertices;
|
|
mesh.triangles = triangles;
|
|
mesh.RecalculateNormals();
|
|
|
|
return mesh;
|
|
}
|
|
|
|
private void GenerateColliders()
|
|
{
|
|
int successCount = 0;
|
|
List<string> errors = new List<string>();
|
|
|
|
Undo.RecordObjects(selectedObjects.ToArray(), "Generate Sprite Colliders");
|
|
|
|
foreach (var obj in selectedObjects)
|
|
{
|
|
if (obj == null) continue;
|
|
|
|
try
|
|
{
|
|
var spriteRenderers = applyToChildren ?
|
|
obj.GetComponentsInChildren<SpriteRenderer>() :
|
|
new SpriteRenderer[] { obj.GetComponent<SpriteRenderer>() };
|
|
|
|
foreach (var renderer in spriteRenderers)
|
|
{
|
|
if (renderer == null || renderer.sprite == null)
|
|
continue;
|
|
|
|
// Check if we're working with a prefab
|
|
bool isPrefab = PrefabUtility.IsPartOfPrefabAsset(renderer.gameObject);
|
|
GameObject targetObject = renderer.gameObject;
|
|
|
|
if (isPrefab)
|
|
{
|
|
// If it's a prefab, we need special handling
|
|
string prefabPath = AssetDatabase.GetAssetPath(targetObject);
|
|
targetObject = PrefabUtility.LoadPrefabContents(prefabPath);
|
|
SpriteRenderer prefabRenderer = targetObject.GetComponent<SpriteRenderer>();
|
|
|
|
if (prefabRenderer == null || prefabRenderer.sprite == null)
|
|
{
|
|
PrefabUtility.UnloadPrefabContents(targetObject);
|
|
continue;
|
|
}
|
|
|
|
if (GenerateColliderForRenderer(prefabRenderer))
|
|
{
|
|
// Save the changes to the prefab
|
|
PrefabUtility.SaveAsPrefabAsset(targetObject, prefabPath);
|
|
successCount++;
|
|
}
|
|
|
|
PrefabUtility.UnloadPrefabContents(targetObject);
|
|
}
|
|
else
|
|
{
|
|
// For scene objects, just generate the collider directly
|
|
if (GenerateColliderForRenderer(renderer))
|
|
{
|
|
successCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
errors.Add($"{obj.name}: {e.Message}");
|
|
}
|
|
}
|
|
|
|
// Clean up any preview meshes as we've now generated real colliders
|
|
ClearPreviews();
|
|
|
|
if (successCount > 0)
|
|
{
|
|
Debug.Log($"Successfully generated colliders for {successCount} sprite(s).");
|
|
}
|
|
|
|
if (errors.Count > 0)
|
|
{
|
|
Debug.LogError($"Errors occurred while generating colliders:\n{string.Join("\n", errors)}");
|
|
}
|
|
}
|
|
|
|
private bool GenerateColliderForRenderer(SpriteRenderer renderer)
|
|
{
|
|
if (renderer == null || renderer.sprite == null)
|
|
return false;
|
|
|
|
Sprite sprite = renderer.sprite;
|
|
GameObject targetObject = renderer.gameObject;
|
|
|
|
// Remove existing colliders if specified
|
|
if (replaceExistingColliders)
|
|
{
|
|
PolygonCollider2D[] existingColliders = targetObject.GetComponents<PolygonCollider2D>();
|
|
foreach (var collider in existingColliders)
|
|
{
|
|
Undo.DestroyObjectImmediate(collider);
|
|
}
|
|
}
|
|
|
|
// Create a new polygon collider
|
|
PolygonCollider2D polygonCollider = Undo.AddComponent<PolygonCollider2D>(targetObject);
|
|
if (polygonCollider == null)
|
|
return false;
|
|
|
|
// Set as trigger if specified
|
|
polygonCollider.isTrigger = generateTriggerColliders;
|
|
|
|
// Get paths from the sprite
|
|
List<Vector2[]> paths = GetSpritePaths(sprite, simplificationTolerance);
|
|
if (paths.Count == 0)
|
|
return false;
|
|
|
|
// Apply offset if needed
|
|
if (offsetFromCenter && offsetDistance != 0)
|
|
{
|
|
for (int i = 0; i < paths.Count; i++)
|
|
{
|
|
Vector2[] offsetPath = new Vector2[paths[i].Length];
|
|
for (int j = 0; j < paths[i].Length; j++)
|
|
{
|
|
// Calculate direction from center (0,0) to the point
|
|
Vector2 dir = paths[i][j].normalized;
|
|
// Apply offset in that direction
|
|
offsetPath[j] = paths[i][j] + dir * offsetDistance;
|
|
}
|
|
paths[i] = offsetPath;
|
|
}
|
|
}
|
|
|
|
// Set the paths on the collider
|
|
polygonCollider.pathCount = paths.Count;
|
|
for (int i = 0; i < paths.Count; i++)
|
|
{
|
|
polygonCollider.SetPath(i, paths[i]);
|
|
}
|
|
|
|
// Set the layer on the GameObject if a specific layer is selected
|
|
SetTargetLayer(targetObject);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the target layer on the GameObject if a layer is selected
|
|
/// </summary>
|
|
/// <param name="targetObject">The GameObject to set the layer on</param>
|
|
private void SetTargetLayer(GameObject targetObject)
|
|
{
|
|
if (targetLayer != 0)
|
|
{
|
|
Undo.RecordObject(targetObject, "Set GameObject Layer");
|
|
targetObject.layer = targetLayer;
|
|
}
|
|
}
|
|
|
|
private List<Vector2[]> GetSpritePaths(Sprite sprite, float tolerance)
|
|
{
|
|
List<Vector2[]> result = new List<Vector2[]>();
|
|
|
|
if (sprite == null)
|
|
return result;
|
|
|
|
// Get the raw physics shape data from the sprite
|
|
int physicsShapeCount = sprite.GetPhysicsShapeCount();
|
|
if (physicsShapeCount == 0)
|
|
{
|
|
// Use the sprite's bounds if no physics shape is defined
|
|
Vector2[] boundingBoxPath = new Vector2[4];
|
|
Bounds bounds = sprite.bounds;
|
|
boundingBoxPath[0] = new Vector2(bounds.min.x, bounds.min.y);
|
|
boundingBoxPath[1] = new Vector2(bounds.min.x, bounds.max.y);
|
|
boundingBoxPath[2] = new Vector2(bounds.max.x, bounds.max.y);
|
|
boundingBoxPath[3] = new Vector2(bounds.max.x, bounds.min.y);
|
|
result.Add(boundingBoxPath);
|
|
return result;
|
|
}
|
|
|
|
// Adjust the detail level based on the setting
|
|
float actualTolerance = tolerance;
|
|
switch (detailLevel)
|
|
{
|
|
case 1: // Low
|
|
actualTolerance = tolerance * 2.0f;
|
|
break;
|
|
case 2: // Medium - default
|
|
actualTolerance = tolerance;
|
|
break;
|
|
case 3: // High
|
|
actualTolerance = tolerance * 0.5f;
|
|
break;
|
|
}
|
|
|
|
// Get all physics shapes from the sprite
|
|
for (int i = 0; i < physicsShapeCount; i++)
|
|
{
|
|
List<Vector2> path = new List<Vector2>();
|
|
sprite.GetPhysicsShape(i, path);
|
|
|
|
// Apply simplification if needed
|
|
if (actualTolerance > 0.01f)
|
|
{
|
|
path = SimplifyPath(path, actualTolerance);
|
|
}
|
|
|
|
if (path.Count >= 3) // Need at least 3 points for a valid polygon
|
|
{
|
|
result.Add(path.ToArray());
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private List<Vector2> SimplifyPath(List<Vector2> points, float tolerance)
|
|
{
|
|
if (points.Count <= 3)
|
|
return points;
|
|
|
|
// Implementation of Ramer-Douglas-Peucker algorithm for simplifying a polygon
|
|
List<Vector2> result = new List<Vector2>();
|
|
List<int> markers = new List<int>(new int[points.Count]);
|
|
markers[0] = 1;
|
|
markers[points.Count - 1] = 1;
|
|
|
|
SimplifyDouglasPeucker(points, tolerance, markers, 0, points.Count - 1);
|
|
|
|
for (int i = 0; i < points.Count; i++)
|
|
{
|
|
if (markers[i] == 1)
|
|
{
|
|
result.Add(points[i]);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void SimplifyDouglasPeucker(List<Vector2> points, float tolerance, List<int> markers, int start, int end)
|
|
{
|
|
if (end <= start + 1)
|
|
return;
|
|
|
|
float maxDistance = 0;
|
|
int maxIndex = start;
|
|
|
|
Vector2 startPoint = points[start];
|
|
Vector2 endPoint = points[end];
|
|
|
|
// Find the point furthest from the line segment
|
|
for (int i = start + 1; i < end; i++)
|
|
{
|
|
float distance = PerpendicularDistance(points[i], startPoint, endPoint);
|
|
if (distance > maxDistance)
|
|
{
|
|
maxDistance = distance;
|
|
maxIndex = i;
|
|
}
|
|
}
|
|
|
|
// If the furthest point is beyond tolerance, mark it for keeping and recurse
|
|
if (maxDistance > tolerance)
|
|
{
|
|
markers[maxIndex] = 1;
|
|
SimplifyDouglasPeucker(points, tolerance, markers, start, maxIndex);
|
|
SimplifyDouglasPeucker(points, tolerance, markers, maxIndex, end);
|
|
}
|
|
}
|
|
|
|
private float PerpendicularDistance(Vector2 point, Vector2 lineStart, Vector2 lineEnd)
|
|
{
|
|
if (lineStart == lineEnd)
|
|
return Vector2.Distance(point, lineStart);
|
|
|
|
float dx = lineEnd.x - lineStart.x;
|
|
float dy = lineEnd.y - lineStart.y;
|
|
|
|
// Normalize
|
|
float norm = Mathf.Sqrt(dx * dx + dy * dy);
|
|
if (norm < float.Epsilon)
|
|
return Vector2.Distance(point, lineStart);
|
|
|
|
dx /= norm;
|
|
dy /= norm;
|
|
|
|
// Calculate perpendicular distance
|
|
float px = point.x - lineStart.x;
|
|
float py = point.y - lineStart.y;
|
|
|
|
float projectionLength = px * dx + py * dy;
|
|
|
|
Vector2 projection = new Vector2(
|
|
lineStart.x + projectionLength * dx,
|
|
lineStart.y + projectionLength * dy);
|
|
|
|
return Vector2.Distance(point, projection);
|
|
}
|
|
|
|
// Scene view event handling for previewing colliders
|
|
[InitializeOnLoadMethod]
|
|
static void Initialize()
|
|
{
|
|
SceneView.duringSceneGui += OnSceneGUI;
|
|
}
|
|
|
|
static void OnSceneGUI(SceneView sceneView)
|
|
{
|
|
// Find all open collider generator windows
|
|
var windows = Resources.FindObjectsOfTypeAll<SpriteColliderGenerator>();
|
|
foreach (var window in windows)
|
|
{
|
|
window.DrawColliderPreviews(sceneView);
|
|
}
|
|
}
|
|
|
|
void DrawColliderPreviews(SceneView sceneView)
|
|
{
|
|
if (!previewColliders || previewMeshes.Count == 0)
|
|
return;
|
|
|
|
// Draw all preview meshes with the selected color
|
|
Material previewMaterial = new Material(Shader.Find("Hidden/Internal-Colored"));
|
|
previewMaterial.SetPass(0);
|
|
previewMaterial.SetColor("_Color", previewColor);
|
|
|
|
if (Event.current.type == EventType.Repaint)
|
|
{
|
|
GL.PushMatrix();
|
|
GL.MultMatrix(Matrix4x4.identity);
|
|
|
|
// Enable blending for transparency
|
|
GL.Begin(GL.TRIANGLES);
|
|
GL.Color(previewColor);
|
|
|
|
foreach (var mesh in previewMeshes)
|
|
{
|
|
if (mesh != null)
|
|
{
|
|
// Draw each triangle in the mesh
|
|
for (int i = 0; i < mesh.triangles.Length; i += 3)
|
|
{
|
|
Vector3 v0 = mesh.vertices[mesh.triangles[i]];
|
|
Vector3 v1 = mesh.vertices[mesh.triangles[i + 1]];
|
|
Vector3 v2 = mesh.vertices[mesh.triangles[i + 2]];
|
|
|
|
GL.Vertex(v0);
|
|
GL.Vertex(v1);
|
|
GL.Vertex(v2);
|
|
}
|
|
}
|
|
}
|
|
|
|
GL.End();
|
|
GL.PopMatrix();
|
|
|
|
// Also draw the outline
|
|
GL.PushMatrix();
|
|
GL.MultMatrix(Matrix4x4.identity);
|
|
GL.Begin(GL.LINES);
|
|
|
|
// Set a more visible outline color
|
|
Color outlineColor = new Color(previewColor.r, previewColor.g, previewColor.b, 1f);
|
|
GL.Color(outlineColor);
|
|
|
|
foreach (var mesh in previewMeshes)
|
|
{
|
|
if (mesh != null)
|
|
{
|
|
// Create a dictionary to track which edges we've drawn
|
|
HashSet<string> drawnEdges = new HashSet<string>();
|
|
|
|
// Draw edges of each triangle
|
|
for (int i = 0; i < mesh.triangles.Length; i += 3)
|
|
{
|
|
DrawEdgeIfNotDrawn(mesh.vertices[mesh.triangles[i]], mesh.vertices[mesh.triangles[i + 1]], drawnEdges);
|
|
DrawEdgeIfNotDrawn(mesh.vertices[mesh.triangles[i + 1]], mesh.vertices[mesh.triangles[i + 2]], drawnEdges);
|
|
DrawEdgeIfNotDrawn(mesh.vertices[mesh.triangles[i + 2]], mesh.vertices[mesh.triangles[i]], drawnEdges);
|
|
}
|
|
}
|
|
}
|
|
|
|
GL.End();
|
|
GL.PopMatrix();
|
|
}
|
|
}
|
|
|
|
private void DrawEdgeIfNotDrawn(Vector3 v1, Vector3 v2, HashSet<string> drawnEdges)
|
|
{
|
|
// Create a unique key for this edge (order vertices to ensure uniqueness)
|
|
string edgeKey;
|
|
if (v1.x < v2.x || (v1.x == v2.x && v1.y < v2.y))
|
|
edgeKey = $"{v1.x},{v1.y},{v1.z}_{v2.x},{v2.y},{v2.z}";
|
|
else
|
|
edgeKey = $"{v2.x},{v2.y},{v2.z}_{v1.x},{v1.y},{v1.z}";
|
|
|
|
// Only draw if we haven't drawn this edge yet
|
|
if (!drawnEdges.Contains(edgeKey))
|
|
{
|
|
GL.Vertex(v1);
|
|
GL.Vertex(v2);
|
|
drawnEdges.Add(edgeKey);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper class for triangulating polygons
|
|
public class Triangulator
|
|
{
|
|
private List<Vector2> m_points;
|
|
|
|
public Triangulator(Vector2[] points)
|
|
{
|
|
m_points = new List<Vector2>(points);
|
|
}
|
|
|
|
public int[] Triangulate()
|
|
{
|
|
List<int> indices = new List<int>();
|
|
|
|
int n = m_points.Count;
|
|
if (n < 3)
|
|
return indices.ToArray();
|
|
|
|
int[] V = new int[n];
|
|
if (Area() > 0)
|
|
{
|
|
for (int v = 0; v < n; v++)
|
|
V[v] = v;
|
|
}
|
|
else
|
|
{
|
|
for (int v = 0; v < n; v++)
|
|
V[v] = (n - 1) - v;
|
|
}
|
|
|
|
int nv = n;
|
|
int count = 2 * nv;
|
|
for (int v = nv - 1; nv > 2; )
|
|
{
|
|
if ((count--) <= 0)
|
|
return indices.ToArray();
|
|
|
|
int u = v;
|
|
if (nv <= u)
|
|
u = 0;
|
|
v = u + 1;
|
|
if (nv <= v)
|
|
v = 0;
|
|
int w = v + 1;
|
|
if (nv <= w)
|
|
w = 0;
|
|
|
|
if (Snip(u, v, w, nv, V))
|
|
{
|
|
int a, b, c, s, t;
|
|
a = V[u];
|
|
b = V[v];
|
|
c = V[w];
|
|
indices.Add(a);
|
|
indices.Add(b);
|
|
indices.Add(c);
|
|
|
|
for (s = v, t = v + 1; t < nv; s++, t++)
|
|
V[s] = V[t];
|
|
nv--;
|
|
count = 2 * nv;
|
|
}
|
|
}
|
|
|
|
indices.Reverse();
|
|
return indices.ToArray();
|
|
}
|
|
|
|
private float Area()
|
|
{
|
|
int n = m_points.Count;
|
|
float A = 0.0f;
|
|
for (int p = n - 1, q = 0; q < n; p = q++)
|
|
{
|
|
Vector2 pval = m_points[p];
|
|
Vector2 qval = m_points[q];
|
|
A += pval.x * qval.y - qval.x * pval.y;
|
|
}
|
|
return (A * 0.5f);
|
|
}
|
|
|
|
private bool Snip(int u, int v, int w, int n, int[] V)
|
|
{
|
|
int p;
|
|
Vector2 A = m_points[V[u]];
|
|
Vector2 B = m_points[V[v]];
|
|
Vector2 C = m_points[V[w]];
|
|
if (Mathf.Epsilon > (((B.x - A.x) * (C.y - A.y)) - ((B.y - A.y) * (C.x - A.x))))
|
|
return false;
|
|
for (p = 0; p < n; p++)
|
|
{
|
|
if ((p == u) || (p == v) || (p == w))
|
|
continue;
|
|
Vector2 P = m_points[V[p]];
|
|
if (InsideTriangle(A, B, C, P))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool InsideTriangle(Vector2 A, Vector2 B, Vector2 C, Vector2 P)
|
|
{
|
|
float ax, ay, bx, by, cx, cy, apx, apy, bpx, bpy, cpx, cpy;
|
|
float cCROSSap, bCROSScp, aCROSSbp;
|
|
|
|
ax = C.x - B.x; ay = C.y - B.y;
|
|
bx = A.x - C.x; by = A.y - C.y;
|
|
cx = B.x - A.x; cy = B.y - A.y;
|
|
apx = P.x - A.x; apy = P.y - A.y;
|
|
bpx = P.x - B.x; bpy = P.y - B.y;
|
|
cpx = P.x - C.x; cpy = P.y - C.y;
|
|
|
|
aCROSSbp = ax * bpy - ay * bpx;
|
|
cCROSSap = cx * apy - cy * apx;
|
|
bCROSScp = bx * cpy - by * cpx;
|
|
|
|
return ((aCROSSbp >= 0.0f) && (bCROSScp >= 0.0f) && (cCROSSap >= 0.0f));
|
|
}
|
|
}
|
|
}
|