Compare commits

...

3 Commits

Author SHA1 Message Date
Michal Pikulski
a455e34ed0 Add a sprite collider generator tool window 2025-09-16 15:39:49 +02:00
Michal Pikulski
75be338065 Working generic object pooling, pool monitor editor tool and batch component adder editor tool 2025-09-16 15:02:57 +02:00
Michal Pikulski
bcc6f05058 Working single-purpose object pooling solution 2025-09-16 15:02:57 +02:00
34 changed files with 2884 additions and 141 deletions

View File

@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 6f3207429a65b3e4b83935ac19791077
m_currentHash:
serializedVersion: 2
Hash: c0cf00979528ae95d3583c572e4eb343
Hash: 00000000000000000000000000000000
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_CatalogRequestsTimeout: 0

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8b28cee1553b4a15aa1c3be950983fee
timeCreated: 1758016486

View File

@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace Editor.Utilities
{
public class BatchComponentAdder : EditorWindow
{
private Vector2 scrollPosition;
private List<GameObject> selectedPrefabs = new List<GameObject>();
private string searchText = "";
private List<Type> availableComponentTypes = new List<Type>();
private List<Type> filteredComponentTypes = new List<Type>();
private int selectedComponentIndex = -1;
private bool showScriptsOnly = true;
private bool showBuiltInComponents = false;
[MenuItem("Tools/Batch Component Adder")]
public static void ShowWindow()
{
GetWindow<BatchComponentAdder>("Batch Component Adder");
}
private void OnEnable()
{
// Get all component types when the window is opened
RefreshComponentTypes();
}
private void RefreshComponentTypes()
{
// Get all types that derive from Component
availableComponentTypes = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => type.IsSubclassOf(typeof(Component)) && !type.IsAbstract)
.OrderBy(type => type.Name)
.ToList();
// Apply initial filtering
FilterComponentTypes();
}
private void FilterComponentTypes()
{
filteredComponentTypes = availableComponentTypes
.Where(type => {
if (!showBuiltInComponents && type.Namespace != null && type.Namespace.StartsWith("UnityEngine"))
return false;
if (showScriptsOnly && type.Namespace != null && type.Namespace.StartsWith("UnityEngine"))
return false;
if (!string.IsNullOrEmpty(searchText))
return type.Name.ToLower().Contains(searchText.ToLower());
return true;
})
.ToList();
// Reset selection if it's no longer valid
if (selectedComponentIndex >= filteredComponentTypes.Count)
selectedComponentIndex = -1;
}
private void OnGUI()
{
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("Batch Component Adder", EditorStyles.boldLabel);
EditorGUILayout.HelpBox("Select multiple prefabs, choose a component type, and add it to the root of all selected prefabs.", MessageType.Info);
EditorGUILayout.Space();
// Prefab selection section
EditorGUILayout.LabelField("Selected Prefabs", EditorStyles.boldLabel);
if (GUILayout.Button("Add Selected Assets"))
{
AddSelectedAssets();
}
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.Height(150));
for (int i = 0; i < selectedPrefabs.Count; i++)
{
EditorGUILayout.BeginHorizontal();
selectedPrefabs[i] = (GameObject)EditorGUILayout.ObjectField(selectedPrefabs[i], typeof(GameObject), false);
if (GUILayout.Button("X", GUILayout.Width(20)))
{
selectedPrefabs.RemoveAt(i);
i--;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
if (GUILayout.Button("Clear All"))
{
selectedPrefabs.Clear();
}
EditorGUILayout.Space();
// Component selection section
EditorGUILayout.LabelField("Component to Add", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
showScriptsOnly = EditorGUILayout.Toggle("Scripts Only", showScriptsOnly);
showBuiltInComponents = EditorGUILayout.Toggle("Show Built-in Components", showBuiltInComponents);
EditorGUILayout.EndHorizontal();
string newSearchText = EditorGUILayout.TextField("Search", searchText);
if (newSearchText != searchText)
{
searchText = newSearchText;
FilterComponentTypes();
}
string[] componentNames = filteredComponentTypes.Select(t => t.Name).ToArray();
selectedComponentIndex = EditorGUILayout.Popup("Component Type", selectedComponentIndex, componentNames);
EditorGUILayout.Space();
// Validate and add the component
GUI.enabled = selectedPrefabs.Count > 0 && selectedComponentIndex >= 0 && selectedComponentIndex < filteredComponentTypes.Count;
if (GUILayout.Button("Add Component to Prefabs"))
{
AddComponentToPrefabs();
}
GUI.enabled = true;
EditorGUILayout.EndVertical();
}
private void AddSelectedAssets()
{
UnityEngine.Object[] selectedObjects = Selection.objects;
foreach (var obj in selectedObjects)
{
if (obj is GameObject go)
{
string path = AssetDatabase.GetAssetPath(go);
if (!string.IsNullOrEmpty(path) && path.EndsWith(".prefab"))
{
if (!selectedPrefabs.Contains(go))
{
selectedPrefabs.Add(go);
}
}
}
}
}
private void AddComponentToPrefabs()
{
if (selectedComponentIndex < 0 || selectedComponentIndex >= filteredComponentTypes.Count)
return;
Type componentType = filteredComponentTypes[selectedComponentIndex];
int successCount = 0;
List<string> failedPrefabs = new List<string>();
// For undo operations
Undo.RecordObjects(selectedPrefabs.ToArray(), "Add Component To Prefabs");
foreach (GameObject prefab in selectedPrefabs)
{
// Skip null entries
if (prefab == null) continue;
try
{
// Open the prefab for editing
string prefabPath = AssetDatabase.GetAssetPath(prefab);
GameObject prefabRoot = PrefabUtility.LoadPrefabContents(prefabPath);
// Check if the component already exists
if (prefabRoot.GetComponent(componentType) == null)
{
// Add the component
prefabRoot.AddComponent(componentType);
// Save the prefab
PrefabUtility.SaveAsPrefabAsset(prefabRoot, prefabPath);
successCount++;
}
else
{
failedPrefabs.Add($"{prefab.name} (already has component)");
}
// Unload the prefab
PrefabUtility.UnloadPrefabContents(prefabRoot);
}
catch (Exception e)
{
Debug.LogError($"Error adding component to {prefab.name}: {e.Message}");
failedPrefabs.Add($"{prefab.name} (error)");
}
}
// Show results
if (successCount > 0)
{
Debug.Log($"Successfully added {componentType.Name} to {successCount} prefabs.");
}
if (failedPrefabs.Count > 0)
{
Debug.LogWarning($"Failed to add component to {failedPrefabs.Count} prefabs: {string.Join(", ", failedPrefabs)}");
}
// Refresh the asset database to show changes
AssetDatabase.Refresh();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 34bcaf56206d4ec29cfa108c96622c37
timeCreated: 1758027437

View File

@@ -0,0 +1,432 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.Reflection;
using System.Collections;
using Pooling;
namespace Editor.Utilities
{
public class PoolMonitorWindow : EditorWindow
{
private Vector2 scrollPosition;
private bool autoRefresh = true;
private float refreshInterval = 1.0f;
private float lastRefreshTime;
private bool showSinglePrefabPools = true;
private bool showMultiPrefabPools = true;
[MenuItem("Tools/Pool Monitor")]
public static void ShowWindow()
{
GetWindow<PoolMonitorWindow>("Pool Monitor");
}
void OnGUI()
{
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField("Object Pool Monitor", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
autoRefresh = EditorGUILayout.Toggle("Auto Refresh", autoRefresh);
if (autoRefresh)
{
refreshInterval = EditorGUILayout.Slider("Refresh Interval", refreshInterval, 0.1f, 5f);
}
if (GUILayout.Button("Refresh Now"))
{
RefreshPoolInfo();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Display toggles for showing different pool types
EditorGUILayout.BeginHorizontal();
showSinglePrefabPools = EditorGUILayout.ToggleLeft("Show Single Prefab Pools", showSinglePrefabPools, GUILayout.Width(200));
showMultiPrefabPools = EditorGUILayout.ToggleLeft("Show Multi-Prefab Pools", showMultiPrefabPools, GUILayout.Width(200));
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
if (Application.isPlaying)
{
DisplayPoolInfo();
}
else
{
EditorGUILayout.HelpBox("Enter play mode to see pool statistics.", MessageType.Info);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
void Update()
{
if (autoRefresh && Application.isPlaying)
{
float currentTime = (float)EditorApplication.timeSinceStartup;
if (currentTime - lastRefreshTime > refreshInterval)
{
lastRefreshTime = currentTime;
RefreshPoolInfo();
Repaint();
}
}
}
void RefreshPoolInfo()
{
if (!Application.isPlaying) return;
// Find all pool types and call LogPoolStats
if (showSinglePrefabPools)
{
// Find all types that derive from BaseObjectPool<T>
foreach (var pool in FindObjectsOfBaseType(typeof(Component), typeof(BaseObjectPool<>)))
{
if (pool != null && pool.gameObject.activeInHierarchy)
{
var logMethod = pool.GetType().GetMethod("LogPoolStats");
if (logMethod != null)
{
logMethod.Invoke(pool, null);
}
}
}
}
if (showMultiPrefabPools)
{
// Find all types that derive from MultiPrefabPool<T>
foreach (var pool in FindObjectsOfBaseType(typeof(Component), typeof(MultiPrefabPool<>)))
{
if (pool != null && pool.gameObject.activeInHierarchy)
{
var logMethod = pool.GetType().GetMethod("LogPoolStats");
if (logMethod != null)
{
logMethod.Invoke(pool, null);
}
}
}
}
}
void DisplayPoolInfo()
{
EditorGUILayout.LabelField("Scene Statistics:", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"Total GameObjects: {UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None).Length}");
EditorGUILayout.Space();
if (showSinglePrefabPools)
{
DisplaySinglePrefabPoolInfo();
}
if (showMultiPrefabPools)
{
DisplayMultiPrefabPoolInfo();
}
}
void DisplaySinglePrefabPoolInfo()
{
// Find all types that derive from BaseObjectPool<T>
Component[] pools = FindObjectsOfBaseType(typeof(Component), typeof(BaseObjectPool<>));
if (pools.Length == 0)
{
EditorGUILayout.HelpBox("No single prefab pools found in the scene.", MessageType.Info);
return;
}
EditorGUILayout.LabelField("Single Prefab Pools", EditorStyles.boldLabel);
foreach (var poolComponent in pools)
{
EditorGUILayout.LabelField($"Pool: {poolComponent.name} ({poolComponent.GetType().Name})", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
// Get private field values using reflection
Type poolType = poolComponent.GetType();
FieldInfo pooledObjectsField = poolType.GetField("pooledObjects",
BindingFlags.NonPublic | BindingFlags.Instance);
FieldInfo totalCreatedField = poolType.GetField("totalCreated",
BindingFlags.NonPublic | BindingFlags.Instance);
FieldInfo totalReturnedField = poolType.GetField("totalReturned",
BindingFlags.NonPublic | BindingFlags.Instance);
PropertyInfo maxPoolSizeProp = poolType.GetProperty("maxPoolSize") ??
poolType.GetField("maxPoolSize")?.DeclaringType.GetProperty("maxPoolSize");
PropertyInfo initialPoolSizeProp = poolType.GetProperty("initialPoolSize") ??
poolType.GetField("initialPoolSize")?.DeclaringType.GetProperty("initialPoolSize");
if (pooledObjectsField != null)
{
object pooledObjects = pooledObjectsField.GetValue(poolComponent);
int count = 0;
// Handle Stack<T>
if (pooledObjects is System.Collections.ICollection collection)
{
count = collection.Count;
}
int maxSize = 0;
if (maxPoolSizeProp != null)
{
maxSize = (int)maxPoolSizeProp.GetValue(poolComponent);
}
else
{
FieldInfo maxPoolSizeField = poolType.GetField("maxPoolSize",
BindingFlags.Public | BindingFlags.Instance);
if (maxPoolSizeField != null)
{
maxSize = (int)maxPoolSizeField.GetValue(poolComponent);
}
}
EditorGUILayout.LabelField($"Pooled Objects: {count}/{maxSize}");
int initialSize = 0;
if (initialPoolSizeProp != null)
{
initialSize = (int)initialPoolSizeProp.GetValue(poolComponent);
}
else
{
FieldInfo initialPoolSizeField = poolType.GetField("initialPoolSize",
BindingFlags.Public | BindingFlags.Instance);
if (initialPoolSizeField != null)
{
initialSize = (int)initialPoolSizeField.GetValue(poolComponent);
}
}
EditorGUILayout.LabelField($"Initial Pool Size: {initialSize}");
if (totalCreatedField != null && totalReturnedField != null)
{
int created = (int)totalCreatedField.GetValue(poolComponent);
int returned = (int)totalReturnedField.GetValue(poolComponent);
EditorGUILayout.LabelField($"Created: {created}, Returned: {returned}");
}
}
// Try to find active objects of the pool's type
if (poolType.BaseType.IsGenericType)
{
Type elementType = poolType.BaseType.GetGenericArguments()[0];
// More accurately count only active objects in the current scene
int activeCount = 0;
// First, try to get a more accurate count from the current scene
foreach (var obj in UnityEngine.Object.FindObjectsByType(elementType, FindObjectsSortMode.None))
{
var comp = obj as Component;
if (comp != null && comp.gameObject.activeInHierarchy)
{
activeCount++;
}
}
EditorGUILayout.LabelField($"Active Objects (Current Scene): {activeCount}");
// Add a note about pooling status
if (activeCount > 0)
{
int pooledCount = 0;
if (pooledObjectsField != null)
{
object pooledObjects = pooledObjectsField.GetValue(poolComponent);
if (pooledObjects is ICollection collection)
{
pooledCount = collection.Count;
}
}
EditorGUILayout.LabelField($"Pooling Efficiency: {pooledCount} ready in pool, {activeCount} active");
}
}
EditorGUI.indentLevel--;
EditorGUILayout.Space();
}
}
void DisplayMultiPrefabPoolInfo()
{
// Find all types that derive from MultiPrefabPool<T>
Component[] pools = FindObjectsOfBaseType(typeof(Component), typeof(MultiPrefabPool<>));
if (pools.Length == 0)
{
EditorGUILayout.HelpBox("No multi-prefab pools found in the scene.", MessageType.Info);
return;
}
EditorGUILayout.LabelField("Multi-Prefab Pools", EditorStyles.boldLabel);
foreach (var poolComponent in pools)
{
EditorGUILayout.LabelField($"Pool: {poolComponent.name} ({poolComponent.GetType().Name})", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
// Get private field values using reflection
Type poolType = poolComponent.GetType();
FieldInfo totalPooledCountField = poolType.GetField("totalPooledCount",
BindingFlags.NonPublic | BindingFlags.Instance);
FieldInfo pooledObjectsField = poolType.GetField("pooledObjects",
BindingFlags.NonPublic | BindingFlags.Instance);
FieldInfo prefabUsageField = poolType.GetField("prefabUsageCount",
BindingFlags.NonPublic | BindingFlags.Instance);
PropertyInfo totalMaxPoolSizeProp = poolType.GetProperty("totalMaxPoolSize") ??
poolType.GetField("totalMaxPoolSize")?.DeclaringType.GetProperty("totalMaxPoolSize");
if (totalPooledCountField != null && totalMaxPoolSizeProp != null)
{
int totalCount = (int)totalPooledCountField.GetValue(poolComponent);
int maxSize = 0;
if (totalMaxPoolSizeProp != null)
{
maxSize = (int)totalMaxPoolSizeProp.GetValue(poolComponent);
}
else
{
FieldInfo totalMaxPoolSizeField = poolType.GetField("totalMaxPoolSize",
BindingFlags.Public | BindingFlags.Instance);
if (totalMaxPoolSizeField != null)
{
maxSize = (int)totalMaxPoolSizeField.GetValue(poolComponent);
}
}
EditorGUILayout.LabelField($"Total Pooled Objects: {totalCount}/{maxSize}");
}
if (pooledObjectsField != null && prefabUsageField != null)
{
// This is more complex because we don't know the exact generic types
// Just show basic information
object pooledTiles = pooledObjectsField.GetValue(poolComponent);
object usageCounts = prefabUsageField.GetValue(poolComponent);
if (pooledTiles != null && pooledTiles is IDictionary poolDict)
{
EditorGUILayout.LabelField("Prefab Details:", EditorStyles.boldLabel);
EditorGUI.indentLevel++;
foreach (DictionaryEntry entry in poolDict)
{
int prefabIndex = Convert.ToInt32(entry.Key);
object value = entry.Value;
int count = 0;
// Handle Stack<T>
if (value is ICollection collection)
{
count = collection.Count;
}
int usageCount = 0;
if (usageCounts is IDictionary usageDict)
{
if (usageDict.Contains(entry.Key))
{
usageCount = Convert.ToInt32(usageDict[entry.Key]);
}
}
EditorGUILayout.LabelField($"Prefab {prefabIndex}: {count} pooled, {usageCount} usages");
}
EditorGUI.indentLevel--;
}
}
// Try to find active objects of the pool's type
if (poolType.BaseType.IsGenericType)
{
Type elementType = poolType.BaseType.GetGenericArguments()[0];
int activeCount = 0;
// Count active objects of the specific pool's component type
foreach (var obj in UnityEngine.Object.FindObjectsByType(elementType, FindObjectsSortMode.None))
{
var comp = obj as Component;
if (comp != null && comp.gameObject.activeInHierarchy)
{
activeCount++;
}
}
EditorGUILayout.LabelField($"Active Objects (Current Scene): {activeCount}");
// Add a note about pooling status
if (activeCount > 0 && totalPooledCountField != null)
{
int pooledCount = (int)totalPooledCountField.GetValue(poolComponent);
EditorGUILayout.LabelField($"Pooling Efficiency: {pooledCount} ready in pool, {activeCount} active");
}
}
EditorGUI.indentLevel--;
EditorGUILayout.Space();
}
}
/// <summary>
/// Finds all objects that derive from a generic base type
/// </summary>
private Component[] FindObjectsOfBaseType(Type baseComponentType, Type genericBaseType)
{
List<Component> results = new List<Component>();
// Find all components in the scene
Component[] allComponents = UnityEngine.Object.FindObjectsByType<Component>(FindObjectsSortMode.None);
foreach (var component in allComponents)
{
Type componentType = component.GetType();
// Check if this type derives from the generic base type
while (componentType != null && componentType != typeof(object))
{
if (componentType.IsGenericType &&
componentType.GetGenericTypeDefinition() == genericBaseType)
{
results.Add(component);
break;
}
// Also check for non-generic derived types
if (componentType.BaseType != null &&
componentType.BaseType.IsGenericType &&
componentType.BaseType.GetGenericTypeDefinition() == genericBaseType)
{
results.Add(component);
break;
}
componentType = componentType.BaseType;
}
}
return results.ToArray();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 17d6e42e7ca549b8b209f0714c8d106b
timeCreated: 1758016486

View File

@@ -0,0 +1,819 @@
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);
private List<Mesh> previewMeshes = new List<Mesh>();
[MenuItem("Tools/Sprite Collider Generator")]
public static void ShowWindow()
{
GetWindow<SpriteColliderGenerator>("Sprite Collider Generator");
}
private void OnDisable()
{
// Clean up any preview meshes when window is closed
foreach (var mesh in previewMeshes)
{
if (mesh != null)
{
DestroyImmediate(mesh);
}
}
previewMeshes.Clear();
}
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);
// 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]);
}
return true;
}
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));
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 420526c0ea5943bb930e3e0913b9dee7
timeCreated: 1758028488

View File

@@ -73,8 +73,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: 622133659
m_SortingLayer: -1
m_SortingOrder: 0
m_Sprite: {fileID: 8944853044452083345, guid: 4ad95f797558b28478685ca60bd90ff4, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -134,4 +134,3 @@ MonoBehaviour:
m_EditorClassIdentifier:
speed: 1
wobbleSpeed: 1
wobbleAmount: 0.1

View File

@@ -10,6 +10,7 @@ GameObject:
m_Component:
- component: {fileID: 7111145574660306503}
- component: {fileID: 3889795708575321074}
- component: {fileID: 7249681423942450184}
m_Layer: 0
m_Name: Left_Tile2_0
m_TagString: Untagged
@@ -26,7 +27,7 @@ Transform:
m_GameObject: {fileID: 864595161669782950}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -2.77, y: 0, z: 0}
m_LocalPosition: {x: -3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -73,8 +74,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 7559449286846427561, guid: e3d18475ab86b1246912f497417465f8, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -87,6 +88,63 @@ SpriteRenderer:
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!60 &7249681423942450184
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 864595161669782950}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.37, y: 5}
newSize: {x: 2.37, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: 0.575, y: -1.5}
- {x: 0.185, y: -0.63}
- {x: 1.145, y: 0.85999995}
- {x: 1.185, y: 1.41}
- {x: 0.835, y: 1.8499999}
- {x: 0.635, y: 2.5}
- {x: -1.185, y: 2.5}
- {x: -1.185, y: -2.5}
- {x: 0.635, y: -2.5}
- {x: 0.625, y: -1.64}
m_UseDelaunayMesh: 0
--- !u!1 &2171518497100337372
GameObject:
m_ObjectHideFlags: 0
@@ -97,6 +155,7 @@ GameObject:
m_Component:
- component: {fileID: 1003080013996268193}
- component: {fileID: 4856205316150460481}
- component: {fileID: 2843103852598642252}
m_Layer: 0
m_Name: Right_Tile1_0
m_TagString: Untagged
@@ -113,7 +172,7 @@ Transform:
m_GameObject: {fileID: 2171518497100337372}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 2.95, y: 0, z: 0}
m_LocalPosition: {x: 3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -160,8 +219,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 3241551651087908563, guid: 8e7c95ebe5325df4395d97ea2ace65d7, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -174,6 +233,69 @@ SpriteRenderer:
m_WasSpriteAssigned: 1
m_MaskInteraction: 0
m_SpriteSortPoint: 0
--- !u!60 &2843103852598642252
PolygonCollider2D:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2171518497100337372}
m_Enabled: 1
serializedVersion: 3
m_Density: 1
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_ForceSendLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ForceReceiveLayers:
serializedVersion: 2
m_Bits: 4294967295
m_ContactCaptureLayers:
serializedVersion: 2
m_Bits: 4294967295
m_CallbackLayers:
serializedVersion: 2
m_Bits: 4294967295
m_IsTrigger: 0
m_UsedByEffector: 0
m_CompositeOperation: 0
m_CompositeOrder: 0
m_Offset: {x: 0, y: 0}
m_SpriteTilingProperty:
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
oldSize: {x: 2.65, y: 5}
newSize: {x: 2.65, y: 5}
adaptiveTilingThreshold: 0.5
drawMode: 0
adaptiveTiling: 0
m_AutoTiling: 0
m_Points:
m_Paths:
- - {x: -0.48499998, y: 2.5}
- {x: -0.49499997, y: 2.19}
- {x: 0.035, y: 1.66}
- {x: 0.035, y: 1.42}
- {x: -0.24499999, y: 1.15}
- {x: -0.285, y: 0.90999997}
- {x: -0.13499999, y: 0.45999998}
- {x: -1.115, y: -0.03}
- {x: -1.3249999, y: -0.35}
- {x: -1.2049999, y: -0.84999996}
- {x: -0.36499998, y: -1.0699999}
- {x: -0.585, y: -1.27}
- {x: -0.625, y: -1.65}
- {x: -0.48499998, y: -2.5}
- {x: 1.3249999, y: -2.5}
- {x: 1.3249999, y: 2.5}
m_UseDelaunayMesh: 0
--- !u!1 &2956826569642009690
GameObject:
m_ObjectHideFlags: 0
@@ -183,6 +305,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 4925660644986369589}
- component: {fileID: 2488201930835981397}
m_Layer: 0
m_Name: Tile1
m_TagString: Untagged
@@ -207,3 +330,16 @@ Transform:
- {fileID: 1003080013996268193}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2488201930835981397
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2956826569642009690}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 41def183b6714aca97663d74cc2d0678, type: 3}
m_Name:
m_EditorClassIdentifier:
tileIndex: 0

View File

@@ -26,7 +26,7 @@ Transform:
m_GameObject: {fileID: 864595161669782950}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -3.093, y: 0, z: 0}
m_LocalPosition: {x: -3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -73,8 +73,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 7559449286846427561, guid: e3d18475ab86b1246912f497417465f8, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -113,7 +113,7 @@ Transform:
m_GameObject: {fileID: 2171518497100337372}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 2.627, y: 0, z: 0}
m_LocalPosition: {x: 3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -160,8 +160,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 3241551651087908563, guid: 8e7c95ebe5325df4395d97ea2ace65d7, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -183,6 +183,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 4925660644986369589}
- component: {fileID: 7876353970701168068}
m_Layer: 0
m_Name: Tile1_flipped
m_TagString: Untagged
@@ -207,3 +208,16 @@ Transform:
- {fileID: 1003080013996268193}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 180}
--- !u!114 &7876353970701168068
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2956826569642009690}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 41def183b6714aca97663d74cc2d0678, type: 3}
m_Name:
m_EditorClassIdentifier:
tileIndex: 0

View File

@@ -26,7 +26,7 @@ Transform:
m_GameObject: {fileID: 864595161669782950}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -2.64, y: 0, z: 0}
m_LocalPosition: {x: -3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -73,8 +73,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 2249565037559538771, guid: 836c1ae2997af4045b714ceaff665a6e, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -113,7 +113,7 @@ Transform:
m_GameObject: {fileID: 2171518497100337372}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 3.08, y: 0, z: 0}
m_LocalPosition: {x: 3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -160,8 +160,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 2764166773722941650, guid: f7ec8080b46b20f459d02e73b12f1694, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -183,6 +183,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 4925660644986369589}
- component: {fileID: 2017387953723006367}
m_Layer: 0
m_Name: Tile2
m_TagString: Untagged
@@ -207,3 +208,16 @@ Transform:
- {fileID: 1003080013996268193}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2017387953723006367
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2956826569642009690}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 41def183b6714aca97663d74cc2d0678, type: 3}
m_Name:
m_EditorClassIdentifier:
tileIndex: 0

View File

@@ -26,7 +26,7 @@ Transform:
m_GameObject: {fileID: 864595161669782950}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -2.95, y: 0, z: 0}
m_LocalPosition: {x: -3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -73,8 +73,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 2249565037559538771, guid: 836c1ae2997af4045b714ceaff665a6e, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -113,7 +113,7 @@ Transform:
m_GameObject: {fileID: 2171518497100337372}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 2.77, y: 0, z: 0}
m_LocalPosition: {x: 3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -160,8 +160,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 2764166773722941650, guid: f7ec8080b46b20f459d02e73b12f1694, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -183,6 +183,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 4925660644986369589}
- component: {fileID: 451715946189956124}
m_Layer: 0
m_Name: Tile2_flipped
m_TagString: Untagged
@@ -207,3 +208,16 @@ Transform:
- {fileID: 1003080013996268193}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 180}
--- !u!114 &451715946189956124
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2956826569642009690}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 41def183b6714aca97663d74cc2d0678, type: 3}
m_Name:
m_EditorClassIdentifier:
tileIndex: 0

View File

@@ -26,7 +26,7 @@ Transform:
m_GameObject: {fileID: 864595161669782950}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -2.62, y: 0, z: 0}
m_LocalPosition: {x: -3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -73,8 +73,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 2249565037559538771, guid: 836c1ae2997af4045b714ceaff665a6e, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -113,7 +113,7 @@ Transform:
m_GameObject: {fileID: 2171518497100337372}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 2.95, y: 0, z: 0}
m_LocalPosition: {x: 3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -160,8 +160,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 3241551651087908563, guid: 8e7c95ebe5325df4395d97ea2ace65d7, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -183,6 +183,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 4925660644986369589}
- component: {fileID: 8822397971507360111}
m_Layer: 0
m_Name: Tile3
m_TagString: Untagged
@@ -207,3 +208,16 @@ Transform:
- {fileID: 1003080013996268193}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8822397971507360111
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2956826569642009690}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 41def183b6714aca97663d74cc2d0678, type: 3}
m_Name:
m_EditorClassIdentifier:
tileIndex: 0

View File

@@ -26,7 +26,7 @@ Transform:
m_GameObject: {fileID: 864595161669782950}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: -2.95, y: 0, z: 0}
m_LocalPosition: {x: -3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -73,8 +73,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 2249565037559538771, guid: 836c1ae2997af4045b714ceaff665a6e, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -113,7 +113,7 @@ Transform:
m_GameObject: {fileID: 2171518497100337372}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 2.62, y: 0, z: 0}
m_LocalPosition: {x: 3.1, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
@@ -160,8 +160,8 @@ SpriteRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingLayerID: -1132846201
m_SortingLayer: 1
m_SortingOrder: 0
m_Sprite: {fileID: 3241551651087908563, guid: 8e7c95ebe5325df4395d97ea2ace65d7, type: 3}
m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -183,6 +183,7 @@ GameObject:
serializedVersion: 6
m_Component:
- component: {fileID: 4925660644986369589}
- component: {fileID: 2006557459409230470}
m_Layer: 0
m_Name: Tile3_flipped
m_TagString: Untagged
@@ -207,3 +208,16 @@ Transform:
- {fileID: 1003080013996268193}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 180}
--- !u!114 &2006557459409230470
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2956826569642009690}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 41def183b6714aca97663d74cc2d0678, type: 3}
m_Name:
m_EditorClassIdentifier:
tileIndex: 0

View File

@@ -170,7 +170,7 @@ MonoBehaviour:
linePoints: 10
stiffness: 350
damping: 15
ropeLength: 4.19
ropeLength: 2
ropeWidth: 0.1
midPointWeight: 1
midPointPosition: 0.5
@@ -220,17 +220,17 @@ LineRenderer:
m_SortingLayer: 0
m_SortingOrder: 0
m_Positions:
- {x: 0, y: 2.07, z: 0}
- {x: -0.002, y: 1.5150144, z: 0}
- {x: -0.004, y: 1.0280256, z: 0}
- {x: -0.006, y: 0.6090335, z: 0}
- {x: -0.008, y: 0.25803846, z: 0}
- {x: -0.01, y: -0.024959907, z: 0}
- {x: -0.012, y: -0.23996155, z: 0}
- {x: -0.013999999, y: -0.3869663, z: 0}
- {x: -0.016, y: -0.46597433, z: 0}
- {x: -0.018, y: -0.47698557, z: 0}
- {x: -0.02, y: -0.42000002, z: 0}
- {x: 0, y: 4.1716814, z: 0}
- {x: -0.0011514801, y: 3.9187107, z: 0}
- {x: -0.00230296, y: 3.6922278, z: 0}
- {x: -0.0034544398, y: 3.4922323, z: 0}
- {x: -0.00460592, y: 3.3187256, z: 0}
- {x: -0.0057574, y: 3.1717062, z: 0}
- {x: -0.0069088796, y: 3.0511749, z: 0}
- {x: -0.008060359, y: 2.9571314, z: 0}
- {x: -0.00921184, y: 2.8895762, z: 0}
- {x: -0.010363319, y: 2.8485086, z: 0}
- {x: -0.0115148, y: 2.833929, z: 0}
m_Parameters:
serializedVersion: 3
widthMultiplier: 1
@@ -488,8 +488,8 @@ Transform:
m_GameObject: {fileID: 747976396}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_LocalPosition: {x: 0, y: 2.9799, z: 0}
m_LocalScale: {x: 0.57574, y: 0.57574, z: 0.57574}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 730962733}
@@ -573,6 +573,9 @@ MonoBehaviour:
spawnY: -6.78
wobbleMinScale: 0.5
wobbleMaxScale: 1.2
useObjectPooling: 1
initialPoolSize: 10
maxPoolSize: 30
--- !u!4 &1003335105
Transform:
m_ObjectHideFlags: 0
@@ -639,7 +642,7 @@ MonoBehaviour:
linePoints: 10
stiffness: 350
damping: 15
ropeLength: 4.19
ropeLength: 2
ropeWidth: 0.1
midPointWeight: 1
midPointPosition: 0.5
@@ -689,17 +692,17 @@ LineRenderer:
m_SortingLayer: 0
m_SortingOrder: 0
m_Positions:
- {x: 0, y: 2.07, z: 0}
- {x: 0.06300001, y: 1.5291233, z: 0}
- {x: 0.126, y: 1.053108, z: 0}
- {x: 0.18900001, y: 0.6419541, z: 0}
- {x: 0.25200003, y: 0.29566196, z: 0}
- {x: 0.315, y: 0.01423122, z: 0}
- {x: 0.37800002, y: -0.20233808, z: 0}
- {x: 0.44099998, y: -0.35404575, z: 0}
- {x: 0.504, y: -0.44089204, z: 0}
- {x: 0.567, y: -0.46287674, z: 0}
- {x: 0.63, y: -0.42000002, z: 0}
- {x: 0, y: 4.1716814, z: 0}
- {x: 0.036271624, y: 3.927396, z: 0}
- {x: 0.07254324, y: 3.7076683, z: 0}
- {x: 0.10881486, y: 3.5124984, z: 0}
- {x: 0.14508648, y: 3.3418865, z: 0}
- {x: 0.1813581, y: 3.195832, z: 0}
- {x: 0.21762972, y: 3.0743358, z: 0}
- {x: 0.25390133, y: 2.9773972, z: 0}
- {x: 0.29017296, y: 2.905017, z: 0}
- {x: 0.32644457, y: 2.857194, z: 0}
- {x: 0.3627162, y: 2.833929, z: 0}
m_Parameters:
serializedVersion: 3
widthMultiplier: 1
@@ -976,7 +979,7 @@ MonoBehaviour:
linePoints: 10
stiffness: 350
damping: 15
ropeLength: 4.19
ropeLength: 2
ropeWidth: 0.1
midPointWeight: 1
midPointPosition: 0.5
@@ -1026,17 +1029,17 @@ LineRenderer:
m_SortingLayer: 0
m_SortingOrder: 0
m_Positions:
- {x: 0, y: 2.07, z: 0}
- {x: -0.058000002, y: 1.5269984, z: 0}
- {x: -0.116, y: 1.0493305, z: 0}
- {x: -0.174, y: 0.63699615, z: 0}
- {x: -0.23200001, y: 0.28999573, z: 0}
- {x: -0.29, y: 0.00832893, z: 0}
- {x: -0.348, y: -0.20800425, z: 0}
- {x: -0.406, y: -0.35900372, z: 0}
- {x: -0.46400002, y: -0.44466949, z: 0}
- {x: -0.52199996, y: -0.46500158, z: 0}
- {x: -0.58, y: -0.42000002, z: 0}
- {x: 0, y: 4.1716814, z: 0}
- {x: -0.03339292, y: 3.9260902, z: 0}
- {x: -0.066785835, y: 3.705347, z: 0}
- {x: -0.10017875, y: 3.5094519, z: 0}
- {x: -0.13357168, y: 3.3384047, z: 0}
- {x: -0.16696459, y: 3.1922054, z: 0}
- {x: -0.20035751, y: 3.0708542, z: 0}
- {x: -0.2337504, y: 2.9743507, z: 0}
- {x: -0.26714337, y: 2.9026957, z: 0}
- {x: -0.30053627, y: 2.8558884, z: 0}
- {x: -0.33392918, y: 2.833929, z: 0}
m_Parameters:
serializedVersion: 3
widthMultiplier: 1
@@ -1204,8 +1207,12 @@ MonoBehaviour:
initialTileCount: 3
tileSpawnBuffer: 1
moveSpeed: 3
speedUpFactor: 0.2
speedUpInterval: 2
speedUpFactor: 0
speedUpInterval: 0
maxMoveSpeed: 12
useObjectPooling: 1
maxPerPrefabPoolSize: 2
totalMaxPoolSize: 25
onTileSpawned:
m_PersistentCalls:
m_Calls: []
@@ -1388,8 +1395,8 @@ Transform:
m_GameObject: {fileID: 2106431001}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: -1, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_LocalPosition: {x: 0, y: 2.5, z: 0}
m_LocalScale: {x: 0.57574, y: 0.57574, z: 0.57574}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1834056337}
@@ -1417,7 +1424,7 @@ MonoBehaviour:
bottleWobble: {fileID: 747976399}
followStiffness: 4
useWobbleOffset: 1
baseY: -1
baseY: 2.5
--- !u!114 &2106431004
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@@ -1,48 +1,109 @@
using UnityEngine;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Represents a single bubble, handling its movement, wobble effect, scaling, and sprite assignment.
/// </summary>
public class Bubble : MonoBehaviour
public class Bubble : MonoBehaviour, IPoolableWithReference<BubblePool>
{
public float speed = 1f;
public float wobbleSpeed = 1f;
private SpriteRenderer spriteRenderer;
private SpriteRenderer bottleSpriteRenderer;
private SpriteRenderer bubbleSpriteRenderer; // Renamed from bottleSpriteRenderer
private float timeOffset;
private float minScale = 0.2f;
private float maxScale = 1.2f;
private float baseScale = 1f; // Added to store the initial scale
private Camera mainCamera; // Cache camera reference
private BubblePool parentPool; // Reference to the pool this bubble came from
void Awake()
{
// Cache references and randomize time offset for wobble
spriteRenderer = GetComponent<SpriteRenderer>();
timeOffset = Random.value * 100f;
// Find the child named "BottleSprite" and get its SpriteRenderer
Transform bottleSpriteTransform = transform.Find("BubbleSprite");
if (bottleSpriteTransform != null)
// Find the child named "BubbleSprite" and get its SpriteRenderer
Transform bubbleSpriteTransform = transform.Find("BubbleSprite");
if (bubbleSpriteTransform != null)
{
bottleSpriteRenderer = bottleSpriteTransform.GetComponent<SpriteRenderer>();
bubbleSpriteRenderer = bubbleSpriteTransform.GetComponent<SpriteRenderer>();
}
// Cache camera reference
mainCamera = Camera.main;
}
void Update()
{
// Move bubble upward
transform.position += Vector3.up * speed * Time.deltaTime;
transform.position += Vector3.up * (speed * Time.deltaTime);
// Wobble effect (smooth oscillation between min and max scale)
float t = (Mathf.Sin((Time.time + timeOffset) * wobbleSpeed) + 1f) * 0.5f; // t in [0,1]
float newScale = Mathf.Lerp(minScale, maxScale, t);
transform.localScale = Vector3.one * newScale;
// Destroy when off screen
if (transform.position.y > Camera.main.orthographicSize + 2f)
float wobbleFactor = Mathf.Lerp(minScale, maxScale, t);
transform.localScale = Vector3.one * (baseScale * wobbleFactor);
// Destroy when off screen - using cached camera reference
if (mainCamera != null && transform.position.y > mainCamera.orthographicSize + 2f)
{
Destroy(gameObject);
OnBubbleDestroy();
}
}
/// <summary>
/// Called when bubble is about to be destroyed
/// </summary>
private void OnBubbleDestroy()
{
// Use the cached pool reference instead of finding it each time
if (parentPool != null)
{
parentPool.ReturnBubble(this);
}
else
{
// Fallback to find the pool if the reference is somehow lost
BubblePool pool = FindFirstObjectByType<BubblePool>();
if (pool != null)
{
Debug.LogWarning("Bubble is missing its parent pool reference, finding pool as fallback");
pool.ReturnBubble(this);
}
else
{
Destroy(gameObject);
}
}
}
/// <summary>
/// Sets the parent pool for this bubble
/// </summary>
/// <param name="pool">The bubble pool that created this bubble</param>
public void SetPool(BubblePool pool)
{
parentPool = pool;
}
/// <summary>
/// Called when the object is retrieved from the pool.
/// </summary>
public void OnSpawn()
{
ResetState();
}
/// <summary>
/// Called when the object is returned to the pool.
/// </summary>
public void OnDespawn()
{
// Nothing to do here for now, but we could clean up resources
}
/// <summary>
/// Sets the main sprite for the bubble.
/// </summary>
@@ -54,13 +115,22 @@ namespace Minigames.DivingForPictures
}
/// <summary>
/// Sets the sprite for the child "BottleSprite" renderer.
/// Sets the sprite for the child "BubbleSprite" renderer.
/// </summary>
/// <param name="sprite">Sprite to assign.</param>
public void SetBottleSprite(Sprite sprite)
public void SetBubbleSprite(Sprite sprite)
{
if (bottleSpriteRenderer != null)
bottleSpriteRenderer.sprite = sprite;
if (bubbleSpriteRenderer != null)
bubbleSpriteRenderer.sprite = sprite;
}
/// <summary>
/// Sets the base scale for the bubble
/// </summary>
/// <param name="scale">Base scale value</param>
public void SetBaseScale(float scale)
{
baseScale = scale;
}
/// <summary>
@@ -73,5 +143,13 @@ namespace Minigames.DivingForPictures
minScale = min;
maxScale = max;
}
/// <summary>
/// Resets the bubble state for reuse from object pool
/// </summary>
public void ResetState()
{
timeOffset = Random.value * 100f;
}
}
}

View File

@@ -0,0 +1,42 @@
using UnityEngine;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Manages a pool of bubble objects to reduce garbage collection overhead.
/// </summary>
public class BubblePool : BaseObjectPool<Bubble>
{
/// <summary>
/// Gets a bubble from the pool, or creates a new one if the pool is empty
/// </summary>
/// <returns>A bubble instance ready to use</returns>
public Bubble GetBubble()
{
Bubble bubble = Get();
// Set reference to this pool so the bubble can return itself
bubble.SetPool(this);
return bubble;
}
/// <summary>
/// Returns a bubble to the pool
/// </summary>
/// <param name="bubble">The bubble to return to the pool</param>
public void ReturnBubble(Bubble bubble)
{
Return(bubble);
}
/// <summary>
/// Logs pool statistics
/// </summary>
public override void LogPoolStats()
{
Debug.Log($"[BubblePool] Pooled bubbles: {pooledObjects.Count}/{maxPoolSize} (Created: {totalCreated}, Returned: {totalReturned})");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 45cdaed0c047423bbb0b7380cd3687f3
timeCreated: 1758015081

View File

@@ -1,4 +1,5 @@
using UnityEngine;
using Pooling;
namespace Minigames.DivingForPictures
{
@@ -19,9 +20,37 @@ namespace Minigames.DivingForPictures
public float spawnY = -5f;
public float wobbleMinScale = 0.2f;
public float wobbleMaxScale = 1.2f;
[Header("Object Pooling")]
public bool useObjectPooling = true;
public int initialPoolSize = 10;
public int maxPoolSize = 30;
private float _timer;
private float _nextSpawnInterval;
private BubblePool _bubblePool;
private Camera _mainCamera; // Cache camera reference
void Awake()
{
_mainCamera = Camera.main;
if (useObjectPooling)
{
// Create the bubble pool
GameObject poolGO = new GameObject("BubblePool");
poolGO.transform.SetParent(transform);
_bubblePool = poolGO.AddComponent<BubblePool>();
_bubblePool.initialPoolSize = initialPoolSize;
_bubblePool.maxPoolSize = maxPoolSize;
_bubblePool.Initialize(bubblePrefab);
// Periodically check for pool statistics in debug builds
#if DEVELOPMENT_BUILD || UNITY_EDITOR
InvokeRepeating(nameof(LogPoolStats), 5f, 30f);
#endif
}
}
void Start()
{
@@ -56,23 +85,49 @@ namespace Minigames.DivingForPictures
{
float x = Random.Range(spawnXMin, spawnXMax);
Vector3 spawnPos = new Vector3(x, spawnY, 0f);
Bubble bubble = Instantiate(bubblePrefab, spawnPos, Quaternion.identity, transform);
Bubble bubble;
if (useObjectPooling && _bubblePool != null)
{
bubble = _bubblePool.GetBubble();
bubble.transform.position = spawnPos;
}
else
{
bubble = Instantiate(bubblePrefab, spawnPos, Quaternion.identity, transform);
}
// Randomize bubble properties
bubble.speed = Random.Range(speedRange.x, speedRange.y);
bubble.wobbleSpeed = Random.Range(wobbleSpeedRange.x, wobbleSpeedRange.y);
float scale = Random.Range(scaleRange.x, scaleRange.y);
bubble.transform.localScale = Vector3.one * scale;
// Assign random sprite to BottleSprite
// Set base scale (initial size) for the bubble
float baseScale = Random.Range(scaleRange.x, scaleRange.y);
bubble.SetBaseScale(baseScale);
// Assign random sprite to BubbleSprite (fixed naming from BottleSprite)
if (bubbleSprites != null && bubbleSprites.Length > 0)
{
Sprite randomSprite = bubbleSprites[Random.Range(0, bubbleSprites.Length)];
bubble.SetBottleSprite(randomSprite);
bubble.SetBubbleSprite(randomSprite);
}
// Random rotation
bubble.transform.rotation = Quaternion.Euler(0f, 0f, Random.Range(0f, 360f));
// Pass min/max scale for wobble clamping
bubble.SetWobbleScaleLimits(wobbleMinScale, wobbleMaxScale);
}
/// <summary>
/// Logs the current pool statistics for debugging
/// </summary>
private void LogPoolStats()
{
if (_bubblePool != null)
{
_bubblePool.LogPoolStats();
}
}
}
}

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using UnityEngine;
using Pooling;
namespace Minigames.DivingForPictures
{
/// <summary>
/// Manages a pool of trench tile objects to reduce garbage collection overhead.
/// Optimized for handling a large number of different prefab types.
/// </summary>
public class TrenchTilePool : MultiPrefabPool<Tile>
{
/// <summary>
/// Returns a tile to the pool
/// </summary>
/// <param name="tile">The tile to return to the pool</param>
/// <param name="prefabIndex">The index of the prefab this tile was created from</param>
public void ReturnTile(GameObject tile, int prefabIndex)
{
if (tile != null)
{
Tile tileComponent = tile.GetComponent<Tile>();
if (tileComponent != null)
{
Return(tileComponent, prefabIndex);
}
else
{
Debug.LogWarning($"Attempted to return a GameObject without a Tile component: {tile.name}");
Destroy(tile);
}
}
}
/// <summary>
/// Gets a tile from the pool, or creates a new one if the pool is empty
/// </summary>
/// <returns>A tile instance ready to use</returns>
public GameObject GetTile(int prefabIndex)
{
Tile tileComponent = Get(prefabIndex);
return tileComponent.gameObject;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fc32ce01955e4e44b4c1d28871d64049
timeCreated: 1758015121

View File

@@ -2,6 +2,7 @@
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
using Pooling;
namespace Minigames.DivingForPictures
{
@@ -12,43 +13,85 @@ namespace Minigames.DivingForPictures
{
[Header("Tile Prefabs")]
[Tooltip("List of possible trench tile prefabs.")]
public List<GameObject> tilePrefabs;
[SerializeField] private List<GameObject> tilePrefabs;
[Header("Tile Settings")]
private const float TileHeight = 5f; // Set to match prefab height
public int initialTileCount = 3;
public float tileSpawnBuffer = 1f;
[SerializeField] private int initialTileCount = 3;
[SerializeField] private float tileSpawnBuffer = 1f;
[Header("Movement Settings")]
public float moveSpeed = 3f;
public float speedUpFactor = 0.2f;
public float speedUpInterval = 10f;
[SerializeField] private float moveSpeed = 3f;
[SerializeField] private float speedUpFactor = 0.2f;
[SerializeField] private float speedUpInterval = 10f;
[SerializeField] private float maxMoveSpeed = 12f;
[FormerlySerializedAs("OnTileSpawned")] [Header("Events")]
[Header("Object Pooling")]
[SerializeField] private bool useObjectPooling = true;
[SerializeField] private int maxPerPrefabPoolSize = 2;
[SerializeField] private int totalMaxPoolSize = 10;
[Header("Events")]
[FormerlySerializedAs("OnTileSpawned")]
public UnityEvent<GameObject> onTileSpawned;
[FormerlySerializedAs("OnTileDestroyed")]
public UnityEvent<GameObject> onTileDestroyed;
// Private fields
private readonly Dictionary<GameObject, float> _tileHeights = new Dictionary<GameObject, float>();
private readonly List<GameObject> _activeTiles = new List<GameObject>();
private readonly Dictionary<int, int> _tileLastUsed = new Dictionary<int, int>();
private int _spawnCounter;
private float _speedUpTimer;
private Camera _mainCamera;
private float _screenBottom;
private float _screenTop;
private TrenchTilePool _tilePool;
void Start()
private const float TileSpawnZ = -1f;
private const float DefaultTileHeight = 5f;
private void Awake()
{
_mainCamera = Camera.main;
CalculateScreenBounds();
for (int i = 0; i < initialTileCount; i++)
// Calculate tile heights for each prefab
CalculateTileHeights();
// Ensure all prefabs have Tile components
ValidateTilePrefabs();
if (useObjectPooling)
{
SpawnTileAtY(_screenBottom + i * TileHeight);
InitializeObjectPool();
}
}
void Update()
// Validate that all prefabs have Tile components
private void ValidateTilePrefabs()
{
for (int i = 0; i < tilePrefabs.Count; i++)
{
if (tilePrefabs[i] == null) continue;
// Check if the prefab has a Tile component
if (tilePrefabs[i].GetComponent<Tile>() == null)
{
Debug.LogWarning($"Prefab {tilePrefabs[i].name} does not have a Tile component. Adding one automatically.");
// Add the Tile component if it doesn't exist
tilePrefabs[i].AddComponent<Tile>();
}
}
}
private void Start()
{
CalculateScreenBounds();
SpawnInitialTiles();
}
private void Update()
{
MoveTiles();
HandleTileDestruction();
@@ -56,14 +99,115 @@ namespace Minigames.DivingForPictures
HandleSpeedRamping();
}
/// <summary>
/// Calculate height values for all tile prefabs
/// </summary>
private void CalculateTileHeights()
{
foreach (var prefab in tilePrefabs)
{
if (prefab == null)
{
Debug.LogError("Null prefab found in tilePrefabs list!");
continue;
}
Renderer renderer = prefab.GetComponentInChildren<Renderer>();
if (renderer != null)
{
_tileHeights[prefab] = renderer.bounds.size.y;
}
else
{
// Fallback in case no renderer is found
_tileHeights[prefab] = DefaultTileHeight;
Debug.LogWarning($"No renderer found in prefab {prefab.name}. Using default height of {DefaultTileHeight}.");
}
}
}
/// <summary>
/// Initialize the object pool system
/// </summary>
private void InitializeObjectPool()
{
// Create the tile pool
GameObject poolGO = new GameObject("TrenchTilePool");
poolGO.transform.SetParent(transform);
_tilePool = poolGO.AddComponent<TrenchTilePool>();
// Set up the pool configuration
_tilePool.maxPerPrefabPoolSize = maxPerPrefabPoolSize;
_tilePool.totalMaxPoolSize = totalMaxPoolSize;
// Convert the GameObject list to a Tile list
List<Tile> prefabTiles = new List<Tile>(tilePrefabs.Count);
foreach (var prefab in tilePrefabs)
{
if (prefab != null)
{
Tile tileComponent = prefab.GetComponent<Tile>();
if (tileComponent != null)
{
prefabTiles.Add(tileComponent);
}
else
{
Debug.LogError($"Prefab {prefab.name} is missing a Tile component!");
}
}
}
// Initialize the pool with the tile component list
_tilePool.Initialize(prefabTiles);
// Periodically trim the pool to optimize memory usage
InvokeRepeating(nameof(TrimExcessPooledTiles), 10f, 30f);
}
/// <summary>
/// Spawn the initial set of tiles
/// </summary>
private void SpawnInitialTiles()
{
for (int i = 0; i < initialTileCount; i++)
{
float y = _screenBottom;
// Calculate proper Y position based on previous tiles
if (i > 0 && _activeTiles.Count > 0)
{
GameObject prevTile = _activeTiles[_activeTiles.Count - 1];
float prevHeight = GetTileHeight(prevTile);
y = prevTile.transform.position.y - prevHeight;
}
SpawnTileAtY(y);
}
}
/// <summary>
/// Calculate the screen bounds in world space
/// </summary>
private void CalculateScreenBounds()
{
if (_mainCamera == null)
{
_mainCamera = Camera.main;
if (_mainCamera == null)
{
Debug.LogError("No main camera found!");
return;
}
}
Vector3 bottom = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, _mainCamera.nearClipPlane));
Vector3 top = _mainCamera.ViewportToWorldPoint(new Vector3(0.5f, 1f, _mainCamera.nearClipPlane));
_screenBottom = bottom.y;
_screenTop = top.y;
}
/// <summary>
/// Move all active tiles upward
/// </summary>
private void MoveTiles()
{
float moveDelta = moveSpeed * Time.deltaTime;
@@ -76,83 +220,264 @@ namespace Minigames.DivingForPictures
}
}
/// <summary>
/// Check for tiles that have moved off screen and should be destroyed or returned to pool
/// </summary>
private void HandleTileDestruction()
{
if (_activeTiles.Count == 0) return;
GameObject topTile = _activeTiles[0];
if (topTile.transform.position.y - TileHeight / 2 > _screenTop + tileSpawnBuffer)
if (topTile == null)
{
_activeTiles.RemoveAt(0);
return;
}
float tileHeight = GetTileHeight(topTile);
if (topTile.transform.position.y - tileHeight / 2 > _screenTop + tileSpawnBuffer)
{
_activeTiles.RemoveAt(0);
onTileDestroyed?.Invoke(topTile);
Destroy(topTile);
if (useObjectPooling && _tilePool != null)
{
// Find the prefab index for this tile
int prefabIndex = GetPrefabIndex(topTile);
if (prefabIndex >= 0)
{
_tilePool.ReturnTile(topTile, prefabIndex);
}
else
{
Destroy(topTile);
}
}
else
{
Destroy(topTile);
}
}
}
/// <summary>
/// Check if new tiles need to be spawned
/// </summary>
private void HandleTileSpawning()
{
if (_activeTiles.Count == 0) return;
GameObject bottomTile = _activeTiles[^1];
float bottomEdge = bottomTile.transform.position.y - TileHeight / 2;
if (bottomTile == null)
{
_activeTiles.RemoveAt(_activeTiles.Count - 1);
return;
}
float tileHeight = GetTileHeight(bottomTile);
float bottomEdge = bottomTile.transform.position.y - tileHeight / 2;
if (bottomEdge > _screenBottom - tileSpawnBuffer)
{
float newY = bottomTile.transform.position.y - TileHeight;
float newY = bottomTile.transform.position.y - tileHeight;
SpawnTileAtY(newY);
}
}
/// <summary>
/// Handle increasing the movement speed over time
/// </summary>
private void HandleSpeedRamping()
{
_speedUpTimer += Time.deltaTime;
if (_speedUpTimer >= speedUpInterval)
{
moveSpeed += speedUpFactor;
moveSpeed = Mathf.Min(moveSpeed + speedUpFactor, maxMoveSpeed);
_speedUpTimer = 0f;
}
}
/// <summary>
/// Spawn a new tile at the specified Y position
/// </summary>
/// <param name="y">The Y position to spawn at</param>
private void SpawnTileAtY(float y)
{
if (tilePrefabs == null || tilePrefabs.Count == 0)
{
Debug.LogError("No tile prefabs available for spawning!");
return;
}
int prefabIndex = GetWeightedRandomTileIndex();
GameObject prefab = tilePrefabs[prefabIndex];
// Use the prefab's original rotation
GameObject tile = Instantiate(prefab, new Vector3(0f, y, 0f), prefab.transform.rotation, transform);
if (prefab == null)
{
Debug.LogError($"Tile prefab at index {prefabIndex} is null!");
return;
}
GameObject tile;
if (useObjectPooling && _tilePool != null)
{
tile = _tilePool.GetTile(prefabIndex);
if (tile == null)
{
Debug.LogError("Failed to get tile from pool!");
return;
}
tile.transform.position = new Vector3(0f, y, TileSpawnZ);
tile.transform.rotation = prefab.transform.rotation;
tile.transform.SetParent(transform);
}
else
{
// Use the prefab's original rotation
tile = Instantiate(prefab, new Vector3(0f, y, TileSpawnZ), prefab.transform.rotation, transform);
}
_activeTiles.Add(tile);
_tileLastUsed[prefabIndex] = _spawnCounter++;
onTileSpawned?.Invoke(tile);
}
/// <summary>
/// Gets a weighted random tile index, favoring tiles that haven't been used recently
/// </summary>
/// <returns>The selected prefab index</returns>
private int GetWeightedRandomTileIndex()
{
// Weight tiles not used recently higher
int n = tilePrefabs.Count;
List<float> weights = new List<float>(n);
for (int i = 0; i < n; i++)
int prefabCount = tilePrefabs.Count;
List<float> weights = new List<float>(prefabCount);
for (int i = 0; i < prefabCount; i++)
{
int lastUsed = _tileLastUsed.TryGetValue(i, out var value) ? value : -n;
int lastUsed = _tileLastUsed.TryGetValue(i, out var value) ? value : -prefabCount;
int age = _spawnCounter - lastUsed;
float weight = Mathf.Clamp(age, 1, n * 2); // More unused = higher weight
float weight = Mathf.Clamp(age, 1, prefabCount * 2); // More unused = higher weight
weights.Add(weight);
}
float total = 0f;
foreach (var w in weights) total += w;
float r = Random.value * total;
for (int i = 0; i < n; i++)
float totalWeight = 0f;
foreach (var weight in weights)
{
if (r < weights[i]) return i;
r -= weights[i];
totalWeight += weight;
}
float randomValue = Random.value * totalWeight;
for (int i = 0; i < prefabCount; i++)
{
if (randomValue < weights[i])
{
return i;
}
randomValue -= weights[i];
}
return Random.Range(0, prefabCount); // fallback
}
/// <summary>
/// Gets the height of a tile based on its prefab or renderer bounds
/// </summary>
/// <param name="tile">The tile to measure</param>
/// <returns>The height of the tile</returns>
private float GetTileHeight(GameObject tile)
{
if (tile == null)
{
Debug.LogWarning("Attempted to get height of null tile!");
return DefaultTileHeight;
}
// Check if this is a known prefab
foreach (var prefab in tilePrefabs)
{
if (prefab == null) continue;
// Check if this tile was created from this prefab
if (tile.name.StartsWith(prefab.name))
{
if (_tileHeights.TryGetValue(prefab, out float height))
{
return height;
}
}
}
// If not found, calculate it from the renderer
Renderer renderer = tile.GetComponentInChildren<Renderer>();
if (renderer != null)
{
return renderer.bounds.size.y;
}
// Fallback
return DefaultTileHeight;
}
/// <summary>
/// Gets the index of the prefab that was used to create this tile
/// </summary>
/// <param name="tile">The tile to check</param>
/// <returns>The index of the prefab or -1 if not found</returns>
private int GetPrefabIndex(GameObject tile)
{
if (tile == null || tilePrefabs == null)
{
return -1;
}
for (int i = 0; i < tilePrefabs.Count; i++)
{
if (tilePrefabs[i] == null) continue;
if (tile.name.StartsWith(tilePrefabs[i].name))
{
return i;
}
}
return -1;
}
/// <summary>
/// Called periodically to trim excess pooled tiles
/// </summary>
private void TrimExcessPooledTiles()
{
if (_tilePool != null)
{
_tilePool.TrimExcess();
}
return Random.Range(0, n); // fallback
}
#if UNITY_EDITOR
void OnDrawGizmosSelected()
private void OnDrawGizmosSelected()
{
if (!Application.isPlaying)
{
// Only try to calculate this if _screenBottom hasn't been set by the game
Camera editorCam = Camera.main;
if (editorCam != null)
{
Vector3 bottom = editorCam.ViewportToWorldPoint(new Vector3(0.5f, 0f, editorCam.nearClipPlane));
_screenBottom = bottom.y;
}
}
// Draw tile bounds for debugging
Gizmos.color = Color.cyan;
for (int i = 0; i < initialTileCount; i++)
{
Vector3 center = new Vector3(0f, _screenBottom + i * TileHeight, 0f);
Gizmos.DrawWireCube(center, new Vector3(10f, TileHeight, 1f));
float height = DefaultTileHeight;
if (tilePrefabs != null && tilePrefabs.Count > 0 && tilePrefabs[0] != null &&
_tileHeights.TryGetValue(tilePrefabs[0], out float h))
{
height = h;
}
Vector3 center = new Vector3(0f, _screenBottom + i * height, 0f);
Gizmos.DrawWireCube(center, new Vector3(10f, height, 1f));
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a4883b41055949e99282264685fcd4f8
timeCreated: 1758019051

View File

@@ -0,0 +1,148 @@
using System.Collections.Generic;
using UnityEngine;
namespace Pooling
{
/// <summary>
/// Abstract base class for object pools.
/// Provides common functionality for creating, retrieving, and returning pooled objects.
/// </summary>
/// <typeparam name="T">The type of component to pool</typeparam>
public abstract class BaseObjectPool<T> : MonoBehaviour where T : Component
{
[Tooltip("Initial number of objects to pre-instantiate")]
public int initialPoolSize = 10;
[Tooltip("Maximum number of objects to keep in the pool")]
public int maxPoolSize = 30;
protected Stack<T> pooledObjects = new Stack<T>();
protected T prefab;
protected int totalCreated = 0;
protected int totalReturned = 0;
/// <summary>
/// Initialize the pool with the specified prefab
/// </summary>
/// <param name="prefabToPool">The prefab to use for this pool</param>
public virtual void Initialize(T prefabToPool)
{
prefab = prefabToPool;
// Pre-instantiate objects
for (int i = 0; i < initialPoolSize; i++)
{
CreateNew();
}
Debug.Log($"[{GetType().Name}] Initialized with {initialPoolSize} objects");
}
/// <summary>
/// Creates a new instance and adds it to the pool
/// </summary>
protected virtual T CreateNew()
{
if (prefab == null)
{
Debug.LogError($"[{GetType().Name}] Prefab is null! Call Initialize first.");
return null;
}
T obj = Instantiate(prefab, transform);
obj.gameObject.SetActive(false);
// Initialize IPoolable components if present
IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnDespawn();
}
pooledObjects.Push(obj);
totalCreated++;
return obj;
}
/// <summary>
/// Gets an object from the pool, or creates a new one if the pool is empty
/// </summary>
/// <returns>An object ready to use</returns>
public virtual T Get()
{
T obj;
if (pooledObjects.Count > 0)
{
obj = pooledObjects.Pop();
}
else
{
obj = CreateNew();
pooledObjects.Pop(); // Remove from pool since we're returning it
}
obj.gameObject.SetActive(true);
// Call OnSpawn for IPoolable components
IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnSpawn();
}
return obj;
}
/// <summary>
/// Returns an object to the pool
/// </summary>
/// <param name="obj">The object to return</param>
public virtual void Return(T obj)
{
if (obj == null) return;
// Call OnDespawn for IPoolable components
IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnDespawn();
}
// Only add to pool if we're under the maximum size
if (pooledObjects.Count < maxPoolSize)
{
obj.gameObject.SetActive(false);
obj.transform.SetParent(transform);
pooledObjects.Push(obj);
totalReturned++;
}
else
{
Destroy(obj.gameObject);
}
}
/// <summary>
/// Logs pool statistics
/// </summary>
public virtual void LogPoolStats()
{
Debug.Log($"[{GetType().Name}] Pooled objects: {pooledObjects.Count}/{maxPoolSize} (Created: {totalCreated}, Returned: {totalReturned})");
}
#if UNITY_EDITOR
private float _lastLogTime = 0f;
protected virtual void Update()
{
// Log pool stats every 5 seconds if in the editor
if (Time.time - _lastLogTime > 5f)
{
LogPoolStats();
_lastLogTime = Time.time;
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 693c1b68b3cd4ad695dce000d53b04ee
timeCreated: 1758019542

View File

@@ -0,0 +1,21 @@
namespace Pooling
{
/// <summary>
/// Interface for objects that can be pooled.
/// Implement this interface on any component that should be managed by an ObjectPool.
/// </summary>
public interface IPoolable
{
/// <summary>
/// Called when the object is retrieved from the pool.
/// Use this to reset the object's state.
/// </summary>
void OnSpawn();
/// <summary>
/// Called when the object is returned to the pool.
/// Use this to clean up any resources or state.
/// </summary>
void OnDespawn();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 13d9095c12514097b9d5953c5b2a45a2
timeCreated: 1758019051

View File

@@ -0,0 +1,18 @@
using UnityEngine;
namespace Pooling
{
/// <summary>
/// Interface for poolable objects that need to reference their parent pool.
/// Implement this interface on objects that need to return themselves to their pool.
/// </summary>
/// <typeparam name="T">The type of pool component</typeparam>
public interface IPoolableWithReference<T> : IPoolable where T : Component
{
/// <summary>
/// Sets the parent pool for this object
/// </summary>
/// <param name="pool">The pool this object belongs to</param>
void SetPool(T pool);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 99a99ac50049492195a73883d9129ea8
timeCreated: 1758019587

View File

@@ -0,0 +1,266 @@
using System.Collections.Generic;
using UnityEngine;
namespace Pooling
{
/// <summary>
/// Object pool that supports multiple prefab types with usage tracking and intelligent trimming.
/// </summary>
/// <typeparam name="T">The type of component to pool</typeparam>
public abstract class MultiPrefabPool<T> : MonoBehaviour where T : Component
{
[Tooltip("Whether to pre-instantiate objects during initialization or create them on demand")]
public bool preInstantiatePrefabs = false;
[Tooltip("Initial number of objects to pre-instantiate per prefab (if preInstantiatePrefabs is true)")]
public int initialObjectsPerPrefab = 2;
[Tooltip("Maximum number of objects to keep in the pool across all prefab types")]
public int totalMaxPoolSize = 50;
[Tooltip("Maximum number of inactive instances to keep per prefab type")]
public int maxPerPrefabPoolSize = 5;
protected Dictionary<int, Stack<T>> pooledObjects = new Dictionary<int, Stack<T>>();
protected Dictionary<int, int> prefabUsageCount = new Dictionary<int, int>();
protected List<T> prefabs;
protected int totalPooledCount = 0;
/// <summary>
/// Initialize the pool with the specified prefabs
/// </summary>
/// <param name="prefabsToPool">The list of prefabs to use for this pool</param>
public virtual void Initialize(List<T> prefabsToPool)
{
prefabs = prefabsToPool;
// Initialize usage tracking
for (int i = 0; i < prefabs.Count; i++)
{
prefabUsageCount[i] = 0;
pooledObjects[i] = new Stack<T>();
}
// Pre-instantiate objects only if enabled
if (preInstantiatePrefabs)
{
// Calculate how many to pre-instantiate based on available pool size
int totalToCreate = Mathf.Min(totalMaxPoolSize, prefabs.Count * initialObjectsPerPrefab);
int perPrefab = Mathf.Max(1, totalToCreate / prefabs.Count);
for (int i = 0; i < prefabs.Count; i++)
{
for (int j = 0; j < perPrefab; j++)
{
if (totalPooledCount >= totalMaxPoolSize) break;
CreateNew(i);
}
}
}
Debug.Log($"[{GetType().Name}] Initialized with {prefabs.Count} prefab types");
}
/// <summary>
/// Creates a new instance and adds it to the pool
/// </summary>
protected virtual T CreateNew(int prefabIndex)
{
if (prefabs == null || prefabIndex >= prefabs.Count)
{
Debug.LogError($"[{GetType().Name}] Invalid prefab index or prefabs list is null!");
return null;
}
T prefab = prefabs[prefabIndex];
T obj = Instantiate(prefab, transform);
obj.gameObject.SetActive(false);
// Initialize IPoolable components if present
IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnDespawn();
}
if (!pooledObjects.ContainsKey(prefabIndex))
{
pooledObjects[prefabIndex] = new Stack<T>();
}
pooledObjects[prefabIndex].Push(obj);
totalPooledCount++;
return obj;
}
/// <summary>
/// Gets an object from the pool for the specified prefab index, or creates a new one if the pool is empty
/// </summary>
/// <param name="prefabIndex">The index of the prefab to get from the pool</param>
/// <returns>An object ready to use</returns>
public virtual T Get(int prefabIndex)
{
T obj;
// Track usage frequency
if (prefabUsageCount.ContainsKey(prefabIndex))
{
prefabUsageCount[prefabIndex]++;
}
else
{
prefabUsageCount[prefabIndex] = 1;
}
if (pooledObjects.ContainsKey(prefabIndex) && pooledObjects[prefabIndex].Count > 0)
{
obj = pooledObjects[prefabIndex].Pop();
totalPooledCount--;
}
else
{
// Create new object without adding to pool
T prefab = prefabs[prefabIndex];
obj = Instantiate(prefab, transform);
}
obj.gameObject.SetActive(true);
// Call OnSpawn for IPoolable components
IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnSpawn();
}
return obj;
}
/// <summary>
/// Returns an object to the pool
/// </summary>
/// <param name="obj">The object to return</param>
/// <param name="prefabIndex">The index of the prefab this object was created from</param>
public virtual void Return(T obj, int prefabIndex)
{
if (obj == null) return;
// Call OnDespawn for IPoolable components
IPoolable poolable = obj.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnDespawn();
}
// Check if we're under the maximum pool size for this prefab type
bool keepObject = totalPooledCount < totalMaxPoolSize;
// Additional constraint: don't keep too many of any single prefab type
if (pooledObjects.ContainsKey(prefabIndex) &&
pooledObjects[prefabIndex].Count >= maxPerPrefabPoolSize)
{
keepObject = false;
}
if (keepObject)
{
obj.gameObject.SetActive(false);
obj.transform.SetParent(transform);
if (!pooledObjects.ContainsKey(prefabIndex))
{
pooledObjects[prefabIndex] = new Stack<T>();
}
pooledObjects[prefabIndex].Push(obj);
totalPooledCount++;
}
else
{
Destroy(obj.gameObject);
}
}
/// <summary>
/// Trims the pool to remove excess objects
/// Can be called periodically or when memory pressure is high
/// </summary>
public virtual void TrimExcess()
{
// If we're under the limit, no need to trim
if (totalPooledCount <= totalMaxPoolSize) return;
// Calculate how many to remove
int excessCount = totalPooledCount - totalMaxPoolSize;
// Get prefab indices sorted by usage (least used first)
List<KeyValuePair<int, int>> sortedUsage = new List<KeyValuePair<int, int>>(prefabUsageCount);
sortedUsage.Sort((a, b) => a.Value.CompareTo(b.Value));
// Remove objects from least used prefabs first
foreach (var usage in sortedUsage)
{
int prefabIndex = usage.Key;
if (!pooledObjects.ContainsKey(prefabIndex) || pooledObjects[prefabIndex].Count == 0) continue;
// How many to remove from this prefab type
int toRemove = Mathf.Min(pooledObjects[prefabIndex].Count, excessCount);
for (int i = 0; i < toRemove; i++)
{
if (pooledObjects[prefabIndex].Count == 0) break;
T obj = pooledObjects[prefabIndex].Pop();
Destroy(obj.gameObject);
totalPooledCount--;
excessCount--;
if (excessCount <= 0) return;
}
}
}
/// <summary>
/// Logs pool statistics to the console
/// </summary>
public virtual void LogPoolStats()
{
Debug.Log($"[{GetType().Name}] Total pooled objects: {totalPooledCount}/{totalMaxPoolSize}");
string prefabDetails = "";
int index = 0;
foreach (var entry in pooledObjects)
{
int prefabIndex = entry.Key;
int count = entry.Value.Count;
int usageCount = prefabUsageCount.ContainsKey(prefabIndex) ? prefabUsageCount[prefabIndex] : 0;
string prefabName = prefabIndex < prefabs.Count ? prefabs[prefabIndex].name : "Unknown";
prefabDetails += $"\n - {prefabName}: {count} pooled, {usageCount} usages";
// Limit the output to avoid too much text
if (++index >= 10 && pooledObjects.Count > 10)
{
prefabDetails += $"\n - ...and {pooledObjects.Count - 10} more prefab types";
break;
}
}
Debug.Log($"[{GetType().Name}] Pool details:{prefabDetails}");
}
#if UNITY_EDITOR
private float _lastLogTime = 0f;
protected virtual void Update()
{
// Log pool stats every 5 seconds if in the editor
if (Time.time - _lastLogTime > 5f)
{
LogPoolStats();
_lastLogTime = Time.time;
}
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 344a70ff5291463bbd636705705ace79
timeCreated: 1758019578

19
Assets/Scripts/Tile.cs Normal file
View File

@@ -0,0 +1,19 @@
using UnityEngine;
/// <summary>
/// A simple marker component to identify game objects as tiles.
/// This allows the pool system to specifically track and manage tile objects.
/// </summary>
public class Tile : MonoBehaviour
{
// This is primarily a marker component, but we could add tile-specific properties here if needed
// Optional: Add properties that might be useful for all tiles
[SerializeField] private int tileIndex;
public int TileIndex
{
get => tileIndex;
set => tileIndex = value;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 41def183b6714aca97663d74cc2d0678
timeCreated: 1758027131