com.amanotes.prefabpool 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.0] - 2026-07-22
4
+
5
+ ### Added
6
+ - `UnityObjectId` / `UnityObjectIdentity` — cross-version object identity using `EntityId` on Unity 6.0.4+ and `GetInstanceID()` on earlier versions (`UnityObjectIdentity.cs`)
7
+
8
+ ### Changed
9
+ - **Breaking:** `MaterialPool.Take(Material, int borrowId)` → `MaterialPool.Take(Material, UnityObject borrower)` (`MaterialPool.cs`)
10
+ - **Breaking:** `MaterialPool.Return(int borrowId)` → `MaterialPool.Return(UnityObject borrower)` (`MaterialPool.cs`)
11
+ - Internal pool dictionaries use `UnityObjectId` keys instead of `int` instance IDs (`MaterialPool.cs`, `PrefabPool.cs`)
12
+
13
+ ## [1.3.0] - 2026-06-12
14
+
15
+ ### Added
16
+ - `MaterialPool` static class — material instance pooling with borrow/return pattern by `borrowId` (`MaterialPool.cs`)
17
+ - `MaterialExtensions.SetAlpha(Material, float)` — convenience method for cloned material alpha modification (`MaterialPool.cs`)
18
+ - `PoolEntryDrawer` — custom `PropertyDrawer` for compact `PoolEntry` inspector display with inline id, prefab, and prewarm count (`PoolEntryDrawer.cs`)
19
+ - `AssemblyInfo.cs` — `InternalsVisibleTo` for test assembly (`AssemblyInfo.cs`)
20
+ - Unit test suite: `PrefabPoolLifecycleTests`, `PrefabPoolGroupTests`, `PrefabPoolPerformanceTests` (`Tests/Runtime/`)
21
+
22
+ ### Changed
23
+ - README.md rewritten with accurate API reference and quick start guide
24
+ - `Docs/PrefabPool-API.md` — complete technical API reference
25
+
3
26
  ## [1.2.0] - 2026-05-20
4
27
 
5
28
  ### Added
@@ -175,22 +175,24 @@ Static utility for pooling material instances.
175
175
  ```csharp
176
176
  public static class MaterialPool
177
177
  {
178
- public static Material Take(Material sourceOrClone, int borrowId);
179
- public static void Return(int borrowId);
178
+ public static Material Take(Material sourceOrClone, UnityObject borrower);
179
+ public static void Return(UnityObject borrower);
180
180
  }
181
181
  ```
182
182
 
183
183
  **Example:**
184
184
  ```csharp
185
+ using UnityEngine;
186
+
185
187
  // Borrow material
186
- Material mat = MaterialPool.Take(originalMaterial, GetInstanceID());
187
- renderer.material = mat;
188
+ Material mat = MaterialPool.Take(originalMaterial, renderer);
189
+ renderer.sharedMaterial = mat;
188
190
  mat.SetAlpha(0.5f);
189
191
 
190
192
  // Return on destroy
191
193
  void OnDestroy()
192
194
  {
193
- MaterialPool.Return(GetInstanceID());
195
+ MaterialPool.Return(renderer);
194
196
  }
195
197
  ```
196
198
 
@@ -313,17 +315,19 @@ void UpdateTileCount(int count)
313
315
  ### Pattern 4: Material Pooling
314
316
 
315
317
  ```csharp
318
+ using UnityEngine;
319
+
316
320
  // When to use: Dynamic color/alpha changes without creating material instances
317
321
  public class ColoredTile : MonoBehaviour
318
322
  {
319
323
  private Material instanceMaterial;
320
- private int borrowId;
324
+ private Renderer _renderer;
321
325
 
322
326
  void Start()
323
327
  {
324
- borrowId = GetInstanceID();
325
- instanceMaterial = MaterialPool.Take(GetComponent<Renderer>().sharedMaterial, borrowId);
326
- GetComponent<Renderer>().material = instanceMaterial;
328
+ _renderer = GetComponent<Renderer>();
329
+ instanceMaterial = MaterialPool.Take(_renderer.sharedMaterial, _renderer);
330
+ _renderer.sharedMaterial = instanceMaterial;
327
331
  }
328
332
 
329
333
  public void SetColor(Color color)
@@ -333,7 +337,7 @@ public class ColoredTile : MonoBehaviour
333
337
 
334
338
  void OnDestroy()
335
339
  {
336
- MaterialPool.Return(borrowId);
340
+ MaterialPool.Return(_renderer);
337
341
  }
338
342
  }
339
343
  ```
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # PrefabPool
2
2
 
3
3
  **Package:** `com.amanotes.prefabpool`
4
- **Version:** 1.2.0
4
+ **Version:** 2.0.0
5
5
 
6
6
  Generic object pooling system for Unity with lifecycle callbacks and material pooling support.
7
7
 
@@ -30,7 +30,7 @@ Add the package to your Unity project via `Packages/manifest.json`:
30
30
  ```json
31
31
  {
32
32
  "dependencies": {
33
- "com.amanotes.prefabpool": "1.0.3"
33
+ "com.amanotes.prefabpool": "2.0.0"
34
34
  },
35
35
  "scopedRegistries": [
36
36
  {
@@ -23,28 +23,29 @@ namespace Amanotes.Core
23
23
 
24
24
  public partial class MaterialPool
25
25
  {
26
- [NonSerialized] private static readonly Dictionary<int, MaterialPool> _map = new Dictionary<int, MaterialPool>();
27
- [NonSerialized] private static readonly Dictionary<int, MaterialPool> _cloned = new Dictionary<int, MaterialPool>();
26
+ [NonSerialized] private static readonly Dictionary<UnityObjectId, MaterialPool> _map = new Dictionary<UnityObjectId, MaterialPool>();
27
+ [NonSerialized] private static readonly Dictionary<UnityObjectId, MaterialPool> _cloned = new Dictionary<UnityObjectId, MaterialPool>();
28
28
 
29
29
  private static MaterialPool GetPool(Material sourceOrClone)
30
30
  {
31
31
  if (sourceOrClone == null) return null;
32
- var instId = sourceOrClone.GetInstanceID();
32
+ var instId = UnityObjectIdentity.GetId(sourceOrClone);
33
33
  var isCloned = sourceOrClone.hideFlags == HideFlags.DontSave;
34
34
  return isCloned
35
35
  ? _cloned.GetValueOrDefault(instId)
36
36
  : _map.GetValueOrDefault(instId);
37
37
  }
38
38
 
39
- public static Material Take(Material sourceOrClone, int borrowId)
39
+ public static Material Take(Material sourceOrClone, UnityObject borrower)
40
40
  {
41
41
  if (sourceOrClone == null) return null;
42
42
  var pool = GetPool(sourceOrClone) ?? new MaterialPool(sourceOrClone);
43
- return pool.InternalTake(borrowId);
43
+ return pool.InternalTake(UnityObjectIdentity.GetId(borrower));
44
44
  }
45
45
 
46
- public static void Return(int borrowId)
46
+ public static void Return(UnityObject borrower)
47
47
  {
48
+ var borrowId = UnityObjectIdentity.GetId(borrower);
48
49
  foreach (var kvp in _map)
49
50
  {
50
51
  kvp.Value.InternalReturn(borrowId);
@@ -56,12 +57,12 @@ namespace Amanotes.Core
56
57
  {
57
58
  private readonly Material original;
58
59
  private readonly List<Material> pool = new List<Material>();
59
- private readonly Dictionary<int, Material> borrowed = new Dictionary<int, Material>();
60
+ private readonly Dictionary<UnityObjectId, Material> borrowed = new Dictionary<UnityObjectId, Material>();
60
61
 
61
62
  private MaterialPool(Material source)
62
63
  {
63
64
  original = source;
64
- _map[original.GetInstanceID()] = this;
65
+ _map[UnityObjectIdentity.GetId(original)] = this;
65
66
 
66
67
  if (source.hideFlags == HideFlags.DontSave)
67
68
  {
@@ -78,7 +79,7 @@ namespace Amanotes.Core
78
79
  if (pool.Count <= 0)
79
80
  {
80
81
  result = new Material(original) { hideFlags = HideFlags.DontSave };
81
- _cloned[result.GetInstanceID()] = this;
82
+ _cloned[UnityObjectIdentity.GetId(result)] = this;
82
83
  return result;
83
84
  }
84
85
 
@@ -87,7 +88,7 @@ namespace Amanotes.Core
87
88
  return result;
88
89
  }
89
90
 
90
- private Material InternalTake(int borrowId)
91
+ private Material InternalTake(UnityObjectId borrowId)
91
92
  {
92
93
  if (borrowed.TryGetValue(borrowId, out var mat)) return mat;
93
94
 
@@ -97,7 +98,7 @@ namespace Amanotes.Core
97
98
  return material;
98
99
  }
99
100
 
100
- private void InternalReturn(int borrowId)
101
+ private void InternalReturn(UnityObjectId borrowId)
101
102
  {
102
103
  if (borrowed.Remove(borrowId, out var mat)) pool.Add(mat);
103
104
  }
@@ -16,19 +16,24 @@ namespace Amanotes.Core
16
16
  var tilePrefabProp = property.FindPropertyRelative(nameof(PoolEntry.prefab));
17
17
  var prewarmProp = property.FindPropertyRelative(nameof(PoolEntry.prewarmCount));
18
18
 
19
- var thirdWidth = position.width / 3f - 2f;
20
- var nameRect = new Rect(position.x, position.y, thirdWidth, EditorGUIUtility.singleLineHeight);
21
- var prefabRect = new Rect(position.x + thirdWidth + 3f, position.y, thirdWidth, EditorGUIUtility.singleLineHeight);
22
- var sliderRect = new Rect(position.x + (thirdWidth + 3f) * 2, position.y, thirdWidth, EditorGUIUtility.singleLineHeight);
19
+ const float gap = 2f;
20
+ const float prewarmWidth = 80f;
21
+ var remaining = position.width - prewarmWidth - gap * 2;
22
+ var nameWidth = remaining * 0.35f;
23
+ var prefabWidth = remaining * 0.65f;
24
+
25
+ var nameRect = new Rect(position.x, position.y, nameWidth, EditorGUIUtility.singleLineHeight);
26
+ var prefabRect = new Rect(nameRect.xMax + gap, position.y, prefabWidth, EditorGUIUtility.singleLineHeight);
27
+ var sliderRect = new Rect(prefabRect.xMax + gap, position.y, prewarmWidth, EditorGUIUtility.singleLineHeight);
23
28
 
24
29
  EditorGUI.PropertyField(nameRect, tileNameProp, GUIContent.none);
25
30
  EditorGUI.PropertyField(prefabRect, tilePrefabProp, GUIContent.none);
26
31
 
27
32
  EditorGUI.BeginChangeCheck();
28
- var sliderValue = EditorGUI.IntSlider(sliderRect, prewarmProp.intValue, 0, 10);
33
+ var newValue = EditorGUI.IntField(sliderRect, prewarmProp.intValue);
29
34
  if (EditorGUI.EndChangeCheck())
30
35
  {
31
- prewarmProp.intValue = sliderValue;
36
+ prewarmProp.intValue = Mathf.Max(0, newValue);
32
37
  }
33
38
 
34
39
  EditorGUI.EndProperty();
@@ -23,14 +23,14 @@ namespace Amanotes.Core
23
23
  [NonSerialized] private PoolGroup _parentGroup;
24
24
 
25
25
  // Per-instance IPoolPrefab cache — components don't change on pooled prefabs
26
- [NonSerialized] private readonly Dictionary<int, IPoolPrefab[]> _poolPrefabCache = new Dictionary<int, IPoolPrefab[]>(32);
26
+ [NonSerialized] private readonly Dictionary<UnityObjectId, IPoolPrefab[]> _poolPrefabCache = new Dictionary<UnityObjectId, IPoolPrefab[]>(32);
27
27
  [NonSerialized] private static readonly List<IPoolPrefab> _tempPoolPrefabs = new List<IPoolPrefab>(8);
28
28
 
29
29
  public void SetParentGroup(PoolGroup parentGroup) => _parentGroup = parentGroup;
30
30
 
31
31
  private IPoolPrefab[] GetCachedPoolPrefabs(GameObject go)
32
32
  {
33
- var instanceId = go.GetInstanceID();
33
+ var instanceId = UnityObjectIdentity.GetId(go);
34
34
  if (_poolPrefabCache.TryGetValue(instanceId, out var cached)) return cached;
35
35
 
36
36
  go.GetComponents(_tempPoolPrefabs);
@@ -311,7 +311,7 @@ namespace Amanotes.Core
311
311
  [NonSerialized] private bool _isReady = false;
312
312
  [NonSerialized] private bool _isInitializing = false;
313
313
  [NonSerialized] private readonly Dictionary<string, PoolGroup> _groups = new Dictionary<string, PoolGroup>();
314
- [NonSerialized] private readonly Dictionary<int, PoolEntry> _instanceTypeMap = new Dictionary<int, PoolEntry>();
314
+ [NonSerialized] private readonly Dictionary<UnityObjectId, PoolEntry> _instanceTypeMap = new Dictionary<UnityObjectId, PoolEntry>();
315
315
  [NonSerialized] private readonly Dictionary<GameObject, PoolEntry> _customPool = new Dictionary<GameObject, PoolEntry>();
316
316
 
317
317
  public void Resize<T>(GameObject prefab, List<T> list, int count, Transform parent) where T: Component
@@ -385,7 +385,7 @@ namespace Amanotes.Core
385
385
  var result = entry.Get(transform, p, localPosition);
386
386
  if (result != null)
387
387
  {
388
- _instanceTypeMap[result.GetInstanceID()] = entry;
388
+ _instanceTypeMap[UnityObjectIdentity.GetId(result)] = entry;
389
389
  }
390
390
 
391
391
  return result;
@@ -395,7 +395,7 @@ namespace Amanotes.Core
395
395
  {
396
396
  if (_isDestroyed || group == null || go == null) return;
397
397
 
398
- var instanceId = go.GetInstanceID();
398
+ var instanceId = UnityObjectIdentity.GetId(go);
399
399
  if (_instanceTypeMap.TryGetValue(instanceId, out var entry))
400
400
  {
401
401
  entry.Return(go, transform);
@@ -407,10 +407,10 @@ namespace Amanotes.Core
407
407
  {
408
408
  #if UNITY_EDITOR
409
409
  if (_instanceTypeMap.Count == 0) return;
410
- var stale = new List<int>();
410
+ var stale = new List<UnityObjectId>();
411
411
  foreach (var kvp in _instanceTypeMap)
412
412
  {
413
- if (EditorUtility.InstanceIDToObject(kvp.Key) == null) stale.Add(kvp.Key);
413
+ if (UnityObjectIdentity.IdToObject(kvp.Key) == null) stale.Add(kvp.Key);
414
414
  }
415
415
  foreach (var id in stale) _instanceTypeMap.Remove(id);
416
416
  #endif
@@ -482,7 +482,7 @@ namespace Amanotes.Core
482
482
  #if UNITY_EDITOR
483
483
  foreach (var kvp in _instanceTypeMap)
484
484
  {
485
- var obj = EditorUtility.InstanceIDToObject(kvp.Key);
485
+ var obj = UnityObjectIdentity.IdToObject(kvp.Key);
486
486
  if (obj != null) UnityObject.DestroyImmediate(obj);
487
487
  }
488
488
  #endif
@@ -0,0 +1,60 @@
1
+ using System;
2
+ using UnityEngine;
3
+ using UnityObject = UnityEngine.Object;
4
+
5
+ namespace Amanotes.Core
6
+ {
7
+ internal readonly struct UnityObjectId : IEquatable<UnityObjectId>
8
+ {
9
+ #if UNITY_6000_4_OR_NEWER
10
+ private readonly EntityId _id;
11
+
12
+ public UnityObjectId(UnityObject obj) => _id = obj != null ? obj.GetEntityId() : default;
13
+
14
+ public bool IsValid => _id.IsValid();
15
+
16
+ public bool Equals(UnityObjectId other) => _id.Equals(other._id);
17
+
18
+ public override int GetHashCode() => _id.GetHashCode();
19
+
20
+ public override string ToString() => _id.ToString();
21
+
22
+ #if UNITY_EDITOR
23
+ public UnityObject ToObject() => UnityEditor.EditorUtility.EntityIdToObject(_id);
24
+ #endif
25
+ #else
26
+ private readonly int _id;
27
+
28
+ public UnityObjectId(UnityObject obj) => _id = obj != null ? obj.GetInstanceID() : 0;
29
+
30
+ public bool IsValid => _id != 0;
31
+
32
+ public bool Equals(UnityObjectId other) => _id == other._id;
33
+
34
+ public override int GetHashCode() => _id;
35
+
36
+ public override string ToString() => _id.ToString();
37
+
38
+ #if UNITY_EDITOR
39
+ public UnityObject ToObject() => UnityEditor.EditorUtility.InstanceIDToObject(_id);
40
+ #endif
41
+ #endif
42
+
43
+ public static bool operator ==(UnityObjectId left, UnityObjectId right) => left.Equals(right);
44
+
45
+ public static bool operator !=(UnityObjectId left, UnityObjectId right) => !left.Equals(right);
46
+
47
+ public override bool Equals(object obj) => obj is UnityObjectId other && Equals(other);
48
+ }
49
+
50
+ internal static class UnityObjectIdentity
51
+ {
52
+ public static UnityObjectId GetId(UnityObject obj) => new UnityObjectId(obj);
53
+
54
+ public static bool IsValid(UnityObjectId id) => id.IsValid;
55
+
56
+ #if UNITY_EDITOR
57
+ public static UnityObject IdToObject(UnityObjectId id) => id.ToObject();
58
+ #endif
59
+ }
60
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: 082b2708eccfb47e1b78c452d24ec500
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
@@ -119,11 +119,8 @@ namespace Amanotes.Core
119
119
  var objFromB = _groupB.Get("PrefabB");
120
120
  Assert.IsNotNull(objFromB, $"Iteration {i}: GroupB should return valid GameObject");
121
121
 
122
- var instanceIdA = objFromA.GetInstanceID();
123
- var instanceIdB = objFromB.GetInstanceID();
124
-
125
- Assert.AreNotEqual(instanceIdA, instanceIdB,
126
- $"Iteration {i}: Objects from different groups should have different instance IDs");
122
+ Assert.AreNotSame(objFromA, objFromB,
123
+ $"Iteration {i}: Objects from different groups should be different instances");
127
124
 
128
125
  Assert.IsTrue(objFromA.name.Contains("PrefabA"),
129
126
  $"Iteration {i}: Object from GroupA should be PrefabA instance");
@@ -136,9 +133,9 @@ namespace Amanotes.Core
136
133
  var reusedFromA = _groupA.Get("PrefabA");
137
134
  var reusedFromB = _groupB.Get("PrefabB");
138
135
 
139
- Assert.AreEqual(instanceIdA, reusedFromA.GetInstanceID(),
136
+ Assert.AreSame(objFromA, reusedFromA,
140
137
  $"Iteration {i}: GroupA should reuse its own objects");
141
- Assert.AreEqual(instanceIdB, reusedFromB.GetInstanceID(),
138
+ Assert.AreSame(objFromB, reusedFromB,
142
139
  $"Iteration {i}: GroupB should reuse its own objects");
143
140
 
144
141
  Assert.IsTrue(reusedFromA.name.Contains("PrefabA"),
@@ -168,11 +165,6 @@ namespace Amanotes.Core
168
165
  Assert.IsNotNull(objB1, $"Iteration {i}: GroupB Get 1 should return valid GameObject");
169
166
  Assert.IsNotNull(objB2, $"Iteration {i}: GroupB Get 2 should return valid GameObject");
170
167
 
171
- var instanceIdA1 = objA1.GetInstanceID();
172
- var instanceIdA2 = objA2.GetInstanceID();
173
- var instanceIdB1 = objB1.GetInstanceID();
174
- var instanceIdB2 = objB2.GetInstanceID();
175
-
176
168
  _groupA.Return(objA1);
177
169
  _groupA.Return(objA2);
178
170
  _groupB.Return(objB1);
@@ -183,35 +175,27 @@ namespace Amanotes.Core
183
175
  var reusedB1 = _groupB.Get("PrefabB");
184
176
  var reusedB2 = _groupB.Get("PrefabB");
185
177
 
186
- var reusedInstanceIdsA = new HashSet<int>
187
- {
188
- reusedA1.GetInstanceID(),
189
- reusedA2.GetInstanceID()
190
- };
191
- var reusedInstanceIdsB = new HashSet<int>
192
- {
193
- reusedB1.GetInstanceID(),
194
- reusedB2.GetInstanceID()
195
- };
196
-
197
- Assert.IsTrue(reusedInstanceIdsA.Contains(instanceIdA1),
178
+ var reusedObjectsA = new HashSet<GameObject> { reusedA1, reusedA2 };
179
+ var reusedObjectsB = new HashSet<GameObject> { reusedB1, reusedB2 };
180
+
181
+ Assert.IsTrue(reusedObjectsA.Contains(objA1),
198
182
  $"Iteration {i}: Object returned to GroupA should be retrieved from GroupA");
199
- Assert.IsTrue(reusedInstanceIdsA.Contains(instanceIdA2),
183
+ Assert.IsTrue(reusedObjectsA.Contains(objA2),
200
184
  $"Iteration {i}: Object returned to GroupA should be retrieved from GroupA");
201
185
 
202
- Assert.IsTrue(reusedInstanceIdsB.Contains(instanceIdB1),
186
+ Assert.IsTrue(reusedObjectsB.Contains(objB1),
203
187
  $"Iteration {i}: Object returned to GroupB should be retrieved from GroupB");
204
- Assert.IsTrue(reusedInstanceIdsB.Contains(instanceIdB2),
188
+ Assert.IsTrue(reusedObjectsB.Contains(objB2),
205
189
  $"Iteration {i}: Object returned to GroupB should be retrieved from GroupB");
206
190
 
207
- Assert.IsFalse(reusedInstanceIdsA.Contains(instanceIdB1),
191
+ Assert.IsFalse(reusedObjectsA.Contains(objB1),
208
192
  $"Iteration {i}: GroupA should not return objects from GroupB");
209
- Assert.IsFalse(reusedInstanceIdsA.Contains(instanceIdB2),
193
+ Assert.IsFalse(reusedObjectsA.Contains(objB2),
210
194
  $"Iteration {i}: GroupA should not return objects from GroupB");
211
195
 
212
- Assert.IsFalse(reusedInstanceIdsB.Contains(instanceIdA1),
196
+ Assert.IsFalse(reusedObjectsB.Contains(objA1),
213
197
  $"Iteration {i}: GroupB should not return objects from GroupA");
214
- Assert.IsFalse(reusedInstanceIdsB.Contains(instanceIdA2),
198
+ Assert.IsFalse(reusedObjectsB.Contains(objA2),
215
199
  $"Iteration {i}: GroupB should not return objects from GroupA");
216
200
 
217
201
  _groupA.Return(reusedA1);
@@ -81,16 +81,12 @@ namespace Amanotes.Core
81
81
  var firstGet = _testGroup.Get("TestPrefab");
82
82
  Assert.IsNotNull(firstGet, $"Iteration {i}: First Get should return valid GameObject");
83
83
 
84
- var firstInstanceId = firstGet.GetInstanceID();
85
-
86
84
  _testGroup.Return(firstGet);
87
85
 
88
86
  var secondGet = _testGroup.Get("TestPrefab");
89
87
  Assert.IsNotNull(secondGet, $"Iteration {i}: Second Get should return valid GameObject");
90
88
 
91
- var secondInstanceId = secondGet.GetInstanceID();
92
-
93
- Assert.AreEqual(firstInstanceId, secondInstanceId,
89
+ Assert.AreSame(firstGet, secondGet,
94
90
  $"Iteration {i}: Get-Return-Get should reuse same instance (validates pooling eliminates instantiation)");
95
91
 
96
92
  _testGroup.Return(secondGet);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.amanotes.prefabpool",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "displayName": "[Ama] Prefab Pool",
5
5
  "description": "Generic object pooling system for Unity",
6
6
  "unity": "2022.3",