com.amanotes.prefabpool 2.0.0 → 2.0.3

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.3] - 2026-09-24
4
+
5
+ ### Added
6
+ - `IPrefabPool.TryReturn` / `PoolGroup.TryReturn` — returns whether the instance was in the checkout ledger (`Return` remains a silent no-op when unknown).
7
+
8
+ ## [2.0.2] - 2026-09-14
9
+
10
+ ### Fixed
11
+ - Pool-group registration during prewarm updates the registration without invalidating the initialization traversal.
12
+
13
+ ### Removed
14
+ - Runtime tests from the distributable UPM package; tests remain in the development project.
15
+
16
+ ## [2.0.1] - 2026-08-15
17
+
18
+ ### Fixed
19
+ - `PrefabPool.DestroyPool` now destroys checked-out instances (not only inactive `_pool` lists). Previously live clones with `HideFlags.DontSave` and a null spawn parent survived scene unload on device because `_instanceTypeMap` was only resolved in the Editor.
20
+ - `PrefabPool.OnDestroy` calls `DestroyPool` so scene teardown always wipes every clone the pool instantiated.
21
+
3
22
  ## [2.0.0] - 2026-07-22
4
23
 
5
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  # PrefabPool API Reference
2
2
 
3
- **Package:** `com.amanotes.prefabpool` | **Version:** 1.2.0 | **Namespace:** `Amanotes.Core`
3
+ **Package:** `com.amanotes.prefabpool` | **Version:** 2.0.3 | **Namespace:** `Amanotes.Core`
4
4
 
5
5
  Generic object pooling system to eliminate garbage collection spikes from frequent instantiation/destruction.
6
6
 
@@ -22,6 +22,8 @@ public interface IPrefabPool
22
22
  GameObject Get(PoolGroup group, string prefabId, Vector3 localPosition = default, Transform customParent = null);
23
23
  void Return(PoolGroup group, GameObject gameObject);
24
24
  IEnumerator Initialize(); // Pre-allocate pooled objects
25
+ void MarkShutdown();
26
+ void DestroyPool(); // Inactive + checked-out clones
25
27
  }
26
28
  ```
27
29
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # PrefabPool
2
2
 
3
3
  **Package:** `com.amanotes.prefabpool`
4
- **Version:** 2.0.0
4
+ **Version:** 2.0.3
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": "2.0.0"
33
+ "com.amanotes.prefabpool": "2.0.3"
34
34
  },
35
35
  "scopedRegistries": [
36
36
  {
@@ -196,6 +196,12 @@ namespace Amanotes.Core
196
196
  {
197
197
  _prefabPool?.Return(this, gameObject);
198
198
  }
199
+
200
+ public bool TryReturn(GameObject gameObject)
201
+ {
202
+ if (_prefabPool == null || gameObject == null) return false;
203
+ return _prefabPool.TryReturn(this, gameObject);
204
+ }
199
205
 
200
206
  public Transform SpawnParent => _spawnParent;
201
207
  }
@@ -207,6 +213,8 @@ namespace Amanotes.Core
207
213
  void SetSpawnParent(string groupId, Transform spawnParent);
208
214
  GameObject Get(PoolGroup group, string prefabId, Vector3 localPosition = default, Transform customParent = null);
209
215
  void Return(PoolGroup group, GameObject gameObject);
216
+ /// <summary>Returns false when the instance was not checked out from this pool (silent no-op in <see cref="Return"/>).</summary>
217
+ bool TryReturn(PoolGroup group, GameObject gameObject);
210
218
  IEnumerator Initialize();
211
219
  /// <summary>
212
220
  /// Marks the pool as shut down so all subsequent Get/Return calls no-op safely.
@@ -214,6 +222,10 @@ namespace Amanotes.Core
214
222
  /// when the pool's hierarchy is being deactivated.
215
223
  /// </summary>
216
224
  void MarkShutdown();
225
+ /// <summary>
226
+ /// Destroys every clone this pool instantiated (inactive lists and checked-out live instances).
227
+ /// </summary>
228
+ void DestroyPool();
217
229
  }
218
230
 
219
231
  public static class PrefabPoolExtensions
@@ -311,7 +323,10 @@ namespace Amanotes.Core
311
323
  [NonSerialized] private bool _isReady = false;
312
324
  [NonSerialized] private bool _isInitializing = false;
313
325
  [NonSerialized] private readonly Dictionary<string, PoolGroup> _groups = new Dictionary<string, PoolGroup>();
326
+ // Registration order for Initialize — only appended on first RegisterGroup per id.
327
+ [NonSerialized] private readonly List<PoolGroup> _groupOrder = new List<PoolGroup>();
314
328
  [NonSerialized] private readonly Dictionary<UnityObjectId, PoolEntry> _instanceTypeMap = new Dictionary<UnityObjectId, PoolEntry>();
329
+ [NonSerialized] private readonly Dictionary<UnityObjectId, GameObject> _liveInstances = new Dictionary<UnityObjectId, GameObject>();
315
330
  [NonSerialized] private readonly Dictionary<GameObject, PoolEntry> _customPool = new Dictionary<GameObject, PoolEntry>();
316
331
 
317
332
  public void Resize<T>(GameObject prefab, List<T> list, int count, Transform parent) where T: Component
@@ -359,8 +374,24 @@ namespace Amanotes.Core
359
374
  public void RegisterGroup(PoolGroup group, Transform spawnParent)
360
375
  {
361
376
  if (group == null) return;
362
-
377
+
378
+ if (_groups.TryGetValue(group.id, out var existing))
379
+ {
380
+ // AttachHost re-registers tile/vfx groups from Start while Initialize is yielding.
381
+ // Updating spawn parent must not touch _groups — that invalidates Dictionary enumeration.
382
+ group.Register(this, spawnParent);
383
+ if (!ReferenceEquals(existing, group))
384
+ {
385
+ _groups[group.id] = group;
386
+ var index = _groupOrder.IndexOf(existing);
387
+ if (index >= 0) _groupOrder[index] = group;
388
+ }
389
+ PurgeStaleInstanceMap();
390
+ return;
391
+ }
392
+
363
393
  _groups[group.id] = group;
394
+ _groupOrder.Add(group);
364
395
  group.Register(this, spawnParent);
365
396
  PurgeStaleInstanceMap();
366
397
  }
@@ -385,7 +416,9 @@ namespace Amanotes.Core
385
416
  var result = entry.Get(transform, p, localPosition);
386
417
  if (result != null)
387
418
  {
388
- _instanceTypeMap[UnityObjectIdentity.GetId(result)] = entry;
419
+ var id = UnityObjectIdentity.GetId(result);
420
+ _instanceTypeMap[id] = entry;
421
+ _liveInstances[id] = result;
389
422
  }
390
423
 
391
424
  return result;
@@ -393,14 +426,21 @@ namespace Amanotes.Core
393
426
 
394
427
  public void Return(PoolGroup group, GameObject go)
395
428
  {
396
- if (_isDestroyed || group == null || go == null) return;
397
-
429
+ TryReturn(group, go);
430
+ }
431
+
432
+ public bool TryReturn(PoolGroup group, GameObject go)
433
+ {
434
+ if (_isDestroyed || group == null || go == null) return false;
435
+
398
436
  var instanceId = UnityObjectIdentity.GetId(go);
399
- if (_instanceTypeMap.TryGetValue(instanceId, out var entry))
400
- {
401
- entry.Return(go, transform);
402
- _instanceTypeMap.Remove(instanceId);
403
- }
437
+ if (!_instanceTypeMap.TryGetValue(instanceId, out var entry))
438
+ return false;
439
+
440
+ entry.Return(go, transform);
441
+ _instanceTypeMap.Remove(instanceId);
442
+ _liveInstances.Remove(instanceId);
443
+ return true;
404
444
  }
405
445
 
406
446
  private void PurgeStaleInstanceMap()
@@ -412,7 +452,11 @@ namespace Amanotes.Core
412
452
  {
413
453
  if (UnityObjectIdentity.IdToObject(kvp.Key) == null) stale.Add(kvp.Key);
414
454
  }
415
- foreach (var id in stale) _instanceTypeMap.Remove(id);
455
+ foreach (var id in stale)
456
+ {
457
+ _instanceTypeMap.Remove(id);
458
+ _liveInstances.Remove(id);
459
+ }
416
460
  #endif
417
461
  }
418
462
 
@@ -425,26 +469,29 @@ namespace Amanotes.Core
425
469
  EditorApplication.playModeStateChanged -= HandlePlayModeChanged;
426
470
  EditorApplication.playModeStateChanged += HandlePlayModeChanged;
427
471
  #endif
428
-
429
- foreach (var group in _groups.Values)
430
- {
431
- foreach (var entry in group.entries)
432
- {
433
- if (entry.prefab == null) continue;
434
- yield return entry.PreAllocate(transform);
435
- }
436
-
437
- // Pre-allocate fallback entry if available
438
- if (group.fallbackEntry != null && group.fallbackEntry.prefab != null)
439
- {
440
- yield return group.fallbackEntry.PreAllocate(transform);
441
- }
442
- }
472
+
473
+ // Drain _groupOrder by index so groups appended during PreAllocate yields still prewarm.
474
+ for (var i = 0; i < _groupOrder.Count; i++)
475
+ yield return PrewarmGroup(_groupOrder[i]);
443
476
 
444
477
  _isInitializing = false;
445
478
  _isReady = true;
446
479
  }
447
480
 
481
+ IEnumerator PrewarmGroup(PoolGroup group)
482
+ {
483
+ if (group == null) yield break;
484
+
485
+ foreach (var entry in group.entries)
486
+ {
487
+ if (entry.prefab == null) continue;
488
+ yield return entry.PreAllocate(transform);
489
+ }
490
+
491
+ if (group.fallbackEntry != null && group.fallbackEntry.prefab != null)
492
+ yield return group.fallbackEntry.PreAllocate(transform);
493
+ }
494
+
448
495
  #if UNITY_EDITOR
449
496
  protected void HandlePlayModeChanged(PlayModeStateChange changed)
450
497
  {
@@ -454,39 +501,54 @@ namespace Amanotes.Core
454
501
  }
455
502
  }
456
503
  #endif
504
+
505
+ void OnDestroy()
506
+ {
507
+ #if UNITY_EDITOR
508
+ EditorApplication.playModeStateChanged -= HandlePlayModeChanged;
509
+ #endif
510
+ DestroyPool();
511
+ }
457
512
 
458
513
  public void DestroyPool()
459
514
  {
460
515
  _isDestroyed = true;
516
+
517
+ foreach (var go in _liveInstances.Values)
518
+ DestroyClone(go);
519
+ _liveInstances.Clear();
520
+ _instanceTypeMap.Clear();
461
521
 
462
- foreach (var group in _groups.Values)
522
+ foreach (var group in _groupOrder)
463
523
  {
464
524
  foreach (var entry in group.entries)
465
- {
466
525
  entry.DestroyPool();
467
- }
468
-
526
+
469
527
  if (group.fallbackEntry != null)
470
- {
471
528
  group.fallbackEntry.DestroyPool();
472
- }
473
529
  }
474
530
  _groups.Clear();
531
+ _groupOrder.Clear();
475
532
 
476
533
  foreach (var entry in _customPool.Values)
477
534
  {
478
535
  entry.DestroyPool();
479
536
  }
480
537
  _customPool.Clear();
481
-
538
+ }
539
+
540
+ static void DestroyClone(GameObject go)
541
+ {
542
+ if (go == null) return;
482
543
  #if UNITY_EDITOR
483
- foreach (var kvp in _instanceTypeMap)
544
+ if (!Application.isPlaying)
484
545
  {
485
- var obj = UnityObjectIdentity.IdToObject(kvp.Key);
486
- if (obj != null) UnityObject.DestroyImmediate(obj);
546
+ UnityObject.DestroyImmediate(go);
547
+ return;
487
548
  }
488
549
  #endif
489
- _instanceTypeMap.Clear();
550
+ UnityObject.Destroy(go);
490
551
  }
491
552
  }
492
- }
553
+ }
554
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.amanotes.prefabpool",
3
- "version": "2.0.0",
3
+ "version": "2.0.3",
4
4
  "displayName": "[Ama] Prefab Pool",
5
5
  "description": "Generic object pooling system for Unity",
6
6
  "unity": "2022.3",
@@ -1,11 +0,0 @@
1
- {
2
- "name": "PrefabPool.Tests",
3
- "rootNamespace": "Amanotes.Core",
4
- "references": [
5
- "PrefabPool",
6
- "Unity.PerformanceTesting"
7
- ],
8
- "optionalUnityReferences": [
9
- "TestAssemblies"
10
- ]
11
- }
@@ -1,7 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: f4f4c6bd22a404f98bd5cc47335df5d8
3
- AssemblyDefinitionImporter:
4
- externalObjects: {}
5
- userData:
6
- assetBundleName:
7
- assetBundleVariant:
@@ -1,208 +0,0 @@
1
- using System.Collections;
2
- using System.Collections.Generic;
3
- using NUnit.Framework;
4
- using UnityEngine;
5
- using UnityEngine.TestTools;
6
- using UnityObject = UnityEngine.Object;
7
-
8
- namespace Amanotes.Core
9
- {
10
- public class PrefabPoolGroupTests
11
- {
12
- private PrefabPool _pool;
13
- private PoolGroup _groupA;
14
- private PoolGroup _groupB;
15
- private GameObject _prefabA;
16
- private GameObject _prefabB;
17
- private Transform _spawnParentA;
18
- private Transform _spawnParentB;
19
-
20
- [SetUp]
21
- public void SetUp()
22
- {
23
- var poolObject = new GameObject("TestPool");
24
- _pool = poolObject.AddComponent<PrefabPool>();
25
-
26
- var spawnParentObjectA = new GameObject("SpawnParentA");
27
- _spawnParentA = spawnParentObjectA.transform;
28
-
29
- var spawnParentObjectB = new GameObject("SpawnParentB");
30
- _spawnParentB = spawnParentObjectB.transform;
31
-
32
- _prefabA = new GameObject("PrefabA");
33
- _prefabA.AddComponent<MockPoolPrefab>();
34
-
35
- _prefabB = new GameObject("PrefabB");
36
- _prefabB.AddComponent<MockPoolPrefab>();
37
-
38
- _groupA = new PoolGroup
39
- {
40
- id = "GroupA",
41
- entries = new List<PoolEntry>
42
- {
43
- new PoolEntry
44
- {
45
- id = "PrefabA",
46
- prefab = _prefabA,
47
- prewarmCount = 5
48
- }
49
- }
50
- };
51
-
52
- _groupB = new PoolGroup
53
- {
54
- id = "GroupB",
55
- entries = new List<PoolEntry>
56
- {
57
- new PoolEntry
58
- {
59
- id = "PrefabB",
60
- prefab = _prefabB,
61
- prewarmCount = 5
62
- }
63
- }
64
- };
65
-
66
- _pool.RegisterGroup(_groupA, _spawnParentA);
67
- _pool.RegisterGroup(_groupB, _spawnParentB);
68
- }
69
-
70
- [TearDown]
71
- public void TearDown()
72
- {
73
- if (_pool != null)
74
- {
75
- _pool.DestroyPool();
76
- UnityObject.DestroyImmediate(_pool.gameObject);
77
- }
78
-
79
- if (_spawnParentA != null)
80
- {
81
- UnityObject.DestroyImmediate(_spawnParentA.gameObject);
82
- }
83
-
84
- if (_spawnParentB != null)
85
- {
86
- UnityObject.DestroyImmediate(_spawnParentB.gameObject);
87
- }
88
-
89
- if (_prefabA != null)
90
- {
91
- UnityObject.DestroyImmediate(_prefabA);
92
- }
93
-
94
- if (_prefabB != null)
95
- {
96
- UnityObject.DestroyImmediate(_prefabB);
97
- }
98
-
99
- var allObjects = UnityObject.FindObjectsOfType<GameObject>();
100
- foreach (var obj in allObjects)
101
- {
102
- if (obj.hideFlags == HideFlags.DontSave)
103
- {
104
- UnityObject.DestroyImmediate(obj);
105
- }
106
- }
107
- }
108
-
109
- [UnityTest]
110
- public IEnumerator PoolGroupIsolation_MultipleGroups_ObjectsNeverCrossBetweenGroups()
111
- {
112
- yield return _pool.Initialize();
113
-
114
- for (int i = 0; i < 100; i++)
115
- {
116
- var objFromA = _groupA.Get("PrefabA");
117
- Assert.IsNotNull(objFromA, $"Iteration {i}: GroupA should return valid GameObject");
118
-
119
- var objFromB = _groupB.Get("PrefabB");
120
- Assert.IsNotNull(objFromB, $"Iteration {i}: GroupB should return valid GameObject");
121
-
122
- Assert.AreNotSame(objFromA, objFromB,
123
- $"Iteration {i}: Objects from different groups should be different instances");
124
-
125
- Assert.IsTrue(objFromA.name.Contains("PrefabA"),
126
- $"Iteration {i}: Object from GroupA should be PrefabA instance");
127
- Assert.IsTrue(objFromB.name.Contains("PrefabB"),
128
- $"Iteration {i}: Object from GroupB should be PrefabB instance");
129
-
130
- _groupA.Return(objFromA);
131
- _groupB.Return(objFromB);
132
-
133
- var reusedFromA = _groupA.Get("PrefabA");
134
- var reusedFromB = _groupB.Get("PrefabB");
135
-
136
- Assert.AreSame(objFromA, reusedFromA,
137
- $"Iteration {i}: GroupA should reuse its own objects");
138
- Assert.AreSame(objFromB, reusedFromB,
139
- $"Iteration {i}: GroupB should reuse its own objects");
140
-
141
- Assert.IsTrue(reusedFromA.name.Contains("PrefabA"),
142
- $"Iteration {i}: GroupA should never return PrefabB instances");
143
- Assert.IsTrue(reusedFromB.name.Contains("PrefabB"),
144
- $"Iteration {i}: GroupB should never return PrefabA instances");
145
-
146
- _groupA.Return(reusedFromA);
147
- _groupB.Return(reusedFromB);
148
- }
149
- }
150
-
151
- [UnityTest]
152
- public IEnumerator ReturnGroupRouting_ReturnToGroup_SubsequentGetRetrievesSameInstanceFromSameGroup()
153
- {
154
- yield return _pool.Initialize();
155
-
156
- for (int i = 0; i < 100; i++)
157
- {
158
- var objA1 = _groupA.Get("PrefabA");
159
- var objA2 = _groupA.Get("PrefabA");
160
- var objB1 = _groupB.Get("PrefabB");
161
- var objB2 = _groupB.Get("PrefabB");
162
-
163
- Assert.IsNotNull(objA1, $"Iteration {i}: GroupA Get 1 should return valid GameObject");
164
- Assert.IsNotNull(objA2, $"Iteration {i}: GroupA Get 2 should return valid GameObject");
165
- Assert.IsNotNull(objB1, $"Iteration {i}: GroupB Get 1 should return valid GameObject");
166
- Assert.IsNotNull(objB2, $"Iteration {i}: GroupB Get 2 should return valid GameObject");
167
-
168
- _groupA.Return(objA1);
169
- _groupA.Return(objA2);
170
- _groupB.Return(objB1);
171
- _groupB.Return(objB2);
172
-
173
- var reusedA1 = _groupA.Get("PrefabA");
174
- var reusedA2 = _groupA.Get("PrefabA");
175
- var reusedB1 = _groupB.Get("PrefabB");
176
- var reusedB2 = _groupB.Get("PrefabB");
177
-
178
- var reusedObjectsA = new HashSet<GameObject> { reusedA1, reusedA2 };
179
- var reusedObjectsB = new HashSet<GameObject> { reusedB1, reusedB2 };
180
-
181
- Assert.IsTrue(reusedObjectsA.Contains(objA1),
182
- $"Iteration {i}: Object returned to GroupA should be retrieved from GroupA");
183
- Assert.IsTrue(reusedObjectsA.Contains(objA2),
184
- $"Iteration {i}: Object returned to GroupA should be retrieved from GroupA");
185
-
186
- Assert.IsTrue(reusedObjectsB.Contains(objB1),
187
- $"Iteration {i}: Object returned to GroupB should be retrieved from GroupB");
188
- Assert.IsTrue(reusedObjectsB.Contains(objB2),
189
- $"Iteration {i}: Object returned to GroupB should be retrieved from GroupB");
190
-
191
- Assert.IsFalse(reusedObjectsA.Contains(objB1),
192
- $"Iteration {i}: GroupA should not return objects from GroupB");
193
- Assert.IsFalse(reusedObjectsA.Contains(objB2),
194
- $"Iteration {i}: GroupA should not return objects from GroupB");
195
-
196
- Assert.IsFalse(reusedObjectsB.Contains(objA1),
197
- $"Iteration {i}: GroupB should not return objects from GroupA");
198
- Assert.IsFalse(reusedObjectsB.Contains(objA2),
199
- $"Iteration {i}: GroupB should not return objects from GroupA");
200
-
201
- _groupA.Return(reusedA1);
202
- _groupA.Return(reusedA2);
203
- _groupB.Return(reusedB1);
204
- _groupB.Return(reusedB2);
205
- }
206
- }
207
- }
208
- }
@@ -1,11 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: 52ad4e4f94f0b41ff9df7f00bcbdd701
3
- MonoImporter:
4
- externalObjects: {}
5
- serializedVersion: 2
6
- defaultReferences: []
7
- executionOrder: 0
8
- icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
@@ -1,253 +0,0 @@
1
- using System.Collections;
2
- using NUnit.Framework;
3
- using UnityEngine;
4
- using UnityEngine.TestTools;
5
- using UnityObject = UnityEngine.Object;
6
-
7
- namespace Amanotes.Core
8
- {
9
- public class PrefabPoolLifecycleTests
10
- {
11
- private PrefabPool _pool;
12
- private GameObject _testPrefab;
13
- private PoolGroup _testGroup;
14
- private Transform _spawnParent;
15
-
16
- [SetUp]
17
- public void SetUp()
18
- {
19
- var poolObject = new GameObject("TestPool");
20
- _pool = poolObject.AddComponent<PrefabPool>();
21
-
22
- var spawnParentObject = new GameObject("SpawnParent");
23
- _spawnParent = spawnParentObject.transform;
24
-
25
- _testPrefab = new GameObject("TestPrefab");
26
- _testPrefab.AddComponent<MockPoolPrefab>();
27
-
28
- _testGroup = new PoolGroup
29
- {
30
- id = "TestGroup",
31
- entries = new System.Collections.Generic.List<PoolEntry>
32
- {
33
- new PoolEntry
34
- {
35
- id = "TestPrefab",
36
- prefab = _testPrefab,
37
- prewarmCount = 5
38
- }
39
- }
40
- };
41
-
42
- _pool.RegisterGroup(_testGroup, _spawnParent);
43
- }
44
-
45
- [TearDown]
46
- public void TearDown()
47
- {
48
- if (_pool != null)
49
- {
50
- _pool.DestroyPool();
51
- UnityObject.DestroyImmediate(_pool.gameObject);
52
- }
53
-
54
- if (_spawnParent != null)
55
- {
56
- UnityObject.DestroyImmediate(_spawnParent.gameObject);
57
- }
58
-
59
- if (_testPrefab != null)
60
- {
61
- UnityObject.DestroyImmediate(_testPrefab);
62
- }
63
-
64
- var allObjects = UnityObject.FindObjectsOfType<GameObject>();
65
- foreach (var obj in allObjects)
66
- {
67
- if (obj.hideFlags == HideFlags.DontSave)
68
- {
69
- UnityObject.DestroyImmediate(obj);
70
- }
71
- }
72
- }
73
-
74
- [UnityTest]
75
- public IEnumerator PoolObjectRoundTrip_GetReturnGet_ReuseSameInstance()
76
- {
77
- yield return _pool.Initialize();
78
-
79
- for (int i = 0; i < 100; i++)
80
- {
81
- var firstGet = _testGroup.Get("TestPrefab");
82
- Assert.IsNotNull(firstGet, $"Iteration {i}: First Get should return valid GameObject");
83
-
84
- _testGroup.Return(firstGet);
85
-
86
- var secondGet = _testGroup.Get("TestPrefab");
87
- Assert.IsNotNull(secondGet, $"Iteration {i}: Second Get should return valid GameObject");
88
-
89
- Assert.AreSame(firstGet, secondGet,
90
- $"Iteration {i}: Get-Return-Get should reuse same instance (validates pooling eliminates instantiation)");
91
-
92
- _testGroup.Return(secondGet);
93
- }
94
- }
95
-
96
- [UnityTest]
97
- public IEnumerator LifecycleCallbackInvocation_GetReturnCycle_InvokesOnGetAndOnReturnExactlyOnce()
98
- {
99
- yield return _pool.Initialize();
100
-
101
- for (int i = 0; i < 100; i++)
102
- {
103
- var obj = _testGroup.Get("TestPrefab");
104
- Assert.IsNotNull(obj, $"Iteration {i}: Get should return valid GameObject");
105
-
106
- var mock = obj.GetComponent<MockPoolPrefab>();
107
- Assert.IsNotNull(mock, $"Iteration {i}: GameObject should have MockPoolPrefab component");
108
-
109
- mock.ResetCounts();
110
-
111
- var objAgain = _testGroup.Get("TestPrefab");
112
- var mockAgain = objAgain.GetComponent<MockPoolPrefab>();
113
- mockAgain.ResetCounts();
114
-
115
- _testGroup.Return(obj);
116
-
117
- Assert.AreEqual(1, mock.returnCallCount,
118
- $"Iteration {i}: OnReturn should be called exactly once during Return");
119
- Assert.AreEqual(0, mock.getCallCount,
120
- $"Iteration {i}: OnGet should not be called during Return");
121
-
122
- var reusedObj = _testGroup.Get("TestPrefab");
123
- var reusedMock = reusedObj.GetComponent<MockPoolPrefab>();
124
-
125
- Assert.AreEqual(1, reusedMock.getCallCount,
126
- $"Iteration {i}: OnGet should be called exactly once during Get");
127
-
128
- _testGroup.Return(objAgain);
129
- _testGroup.Return(reusedObj);
130
- }
131
- }
132
-
133
- [UnityTest]
134
- public IEnumerator PreallocationCorrectness_PrewarmCount_CreatesExactNumberOfPreallocatedObjects()
135
- {
136
- for (int iteration = 0; iteration < 100; iteration++)
137
- {
138
- var prewarmCount = UnityEngine.Random.Range(1, 20);
139
-
140
- var poolObject = new GameObject($"TestPool_{iteration}");
141
- var pool = poolObject.AddComponent<PrefabPool>();
142
-
143
- var spawnParentObject = new GameObject($"SpawnParent_{iteration}");
144
- var spawnParent = spawnParentObject.transform;
145
-
146
- var prefab = new GameObject($"Prefab_{iteration}");
147
- prefab.AddComponent<MockPoolPrefab>();
148
-
149
- var group = new PoolGroup
150
- {
151
- id = $"Group_{iteration}",
152
- entries = new System.Collections.Generic.List<PoolEntry>
153
- {
154
- new PoolEntry
155
- {
156
- id = "TestPrefab",
157
- prefab = prefab,
158
- prewarmCount = prewarmCount
159
- }
160
- }
161
- };
162
-
163
- pool.RegisterGroup(group, spawnParent);
164
-
165
- yield return pool.Initialize();
166
-
167
- var retrievedObjects = new System.Collections.Generic.List<GameObject>();
168
-
169
- for (int i = 0; i < prewarmCount; i++)
170
- {
171
- var obj = group.Get("TestPrefab");
172
- Assert.IsNotNull(obj, $"Iteration {iteration}: Should retrieve pre-allocated object {i}");
173
- retrievedObjects.Add(obj);
174
- }
175
-
176
- Assert.AreEqual(prewarmCount, retrievedObjects.Count,
177
- $"Iteration {iteration}: Should retrieve exactly {prewarmCount} pre-allocated objects");
178
-
179
- foreach (var obj in retrievedObjects)
180
- {
181
- group.Return(obj);
182
- }
183
-
184
- pool.DestroyPool();
185
- UnityObject.DestroyImmediate(poolObject);
186
- UnityObject.DestroyImmediate(spawnParentObject);
187
- UnityObject.DestroyImmediate(prefab);
188
- }
189
- }
190
-
191
- [UnityTest]
192
- public IEnumerator DynamicPoolExpansion_GetBeyondCapacity_SuccessfullyReturnsValidGameObjects()
193
- {
194
- for (int iteration = 0; iteration < 100; iteration++)
195
- {
196
- var initialCapacity = UnityEngine.Random.Range(2, 10);
197
- var extraRequests = UnityEngine.Random.Range(5, 15);
198
-
199
- var poolObject = new GameObject($"TestPool_{iteration}");
200
- var pool = poolObject.AddComponent<PrefabPool>();
201
-
202
- var spawnParentObject = new GameObject($"SpawnParent_{iteration}");
203
- var spawnParent = spawnParentObject.transform;
204
-
205
- var prefab = new GameObject($"Prefab_{iteration}");
206
- prefab.AddComponent<MockPoolPrefab>();
207
-
208
- var group = new PoolGroup
209
- {
210
- id = $"Group_{iteration}",
211
- entries = new System.Collections.Generic.List<PoolEntry>
212
- {
213
- new PoolEntry
214
- {
215
- id = "TestPrefab",
216
- prefab = prefab,
217
- prewarmCount = initialCapacity
218
- }
219
- }
220
- };
221
-
222
- pool.RegisterGroup(group, spawnParent);
223
-
224
- yield return pool.Initialize();
225
-
226
- var retrievedObjects = new System.Collections.Generic.List<GameObject>();
227
-
228
- var totalRequests = initialCapacity + extraRequests;
229
-
230
- for (int i = 0; i < totalRequests; i++)
231
- {
232
- var obj = group.Get("TestPrefab");
233
- Assert.IsNotNull(obj,
234
- $"Iteration {iteration}: Get beyond capacity should succeed (request {i + 1}/{totalRequests})");
235
- retrievedObjects.Add(obj);
236
- }
237
-
238
- Assert.AreEqual(totalRequests, retrievedObjects.Count,
239
- $"Iteration {iteration}: Should retrieve {totalRequests} objects (capacity {initialCapacity} + expansion {extraRequests})");
240
-
241
- foreach (var obj in retrievedObjects)
242
- {
243
- group.Return(obj);
244
- }
245
-
246
- pool.DestroyPool();
247
- UnityObject.DestroyImmediate(poolObject);
248
- UnityObject.DestroyImmediate(spawnParentObject);
249
- UnityObject.DestroyImmediate(prefab);
250
- }
251
- }
252
- }
253
- }
@@ -1,11 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: 9c6969a47577843fb977ab5e2c7c1ad2
3
- MonoImporter:
4
- externalObjects: {}
5
- serializedVersion: 2
6
- defaultReferences: []
7
- executionOrder: 0
8
- icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
@@ -1,212 +0,0 @@
1
- using System.Collections;
2
- using NUnit.Framework;
3
- using Unity.PerformanceTesting;
4
- using UnityEngine;
5
- using UnityEngine.TestTools;
6
- using UnityObject = UnityEngine.Object;
7
-
8
- namespace Amanotes.Core
9
- {
10
- public class PrefabPoolPerformanceTests
11
- {
12
- private PrefabPool _pool;
13
- private GameObject _testPrefab;
14
- private PoolGroup _testGroup;
15
- private Transform _spawnParent;
16
-
17
- [SetUp]
18
- public void SetUp()
19
- {
20
- var poolObject = new GameObject("TestPool");
21
- _pool = poolObject.AddComponent<PrefabPool>();
22
-
23
- var spawnParentObject = new GameObject("SpawnParent");
24
- _spawnParent = spawnParentObject.transform;
25
-
26
- _testPrefab = new GameObject("TestPrefab");
27
- _testPrefab.AddComponent<MockPoolPrefab>();
28
-
29
- _testGroup = new PoolGroup
30
- {
31
- id = "TestGroup",
32
- entries = new System.Collections.Generic.List<PoolEntry>
33
- {
34
- new PoolEntry
35
- {
36
- id = "TestPrefab",
37
- prefab = _testPrefab,
38
- prewarmCount = 10
39
- }
40
- }
41
- };
42
-
43
- _pool.RegisterGroup(_testGroup, _spawnParent);
44
- }
45
-
46
- [TearDown]
47
- public void TearDown()
48
- {
49
- if (_pool != null)
50
- {
51
- _pool.DestroyPool();
52
- UnityObject.DestroyImmediate(_pool.gameObject);
53
- }
54
-
55
- if (_spawnParent != null)
56
- {
57
- UnityObject.DestroyImmediate(_spawnParent.gameObject);
58
- }
59
-
60
- if (_testPrefab != null)
61
- {
62
- UnityObject.DestroyImmediate(_testPrefab);
63
- }
64
-
65
- var allObjects = UnityObject.FindObjectsOfType<GameObject>();
66
- foreach (var obj in allObjects)
67
- {
68
- if (obj.hideFlags == HideFlags.DontSave)
69
- {
70
- UnityObject.DestroyImmediate(obj);
71
- }
72
- }
73
- }
74
-
75
- [UnityTest, Performance]
76
- public IEnumerator Performance_GetReturnCycles()
77
- {
78
- yield return _pool.Initialize();
79
-
80
- Measure.Method(() =>
81
- {
82
- for (int i = 0; i < 1000; i++)
83
- {
84
- var obj = _testGroup.Get("TestPrefab");
85
- _testGroup.Return(obj);
86
- }
87
- })
88
- .WarmupCount(5)
89
- .MeasurementCount(10)
90
- .IterationsPerMeasurement(1)
91
- .GC()
92
- .Run();
93
- }
94
-
95
- [UnityTest, Performance]
96
- public IEnumerator Performance_GCAllocation()
97
- {
98
- yield return _pool.Initialize();
99
-
100
- Measure.Method(() =>
101
- {
102
- for (int i = 0; i < 100; i++)
103
- {
104
- var obj = _testGroup.Get("TestPrefab");
105
- _testGroup.Return(obj);
106
- }
107
- })
108
- .WarmupCount(5)
109
- .MeasurementCount(10)
110
- .IterationsPerMeasurement(1)
111
- .GC()
112
- .Run();
113
- }
114
-
115
- [UnityTest, Performance]
116
- public IEnumerator Performance_PoolExpansion()
117
- {
118
- yield return _pool.Initialize();
119
-
120
- var retrievedObjects = new System.Collections.Generic.List<GameObject>();
121
-
122
- for (int i = 0; i < 10; i++)
123
- {
124
- var obj = _testGroup.Get("TestPrefab");
125
- retrievedObjects.Add(obj);
126
- }
127
-
128
- Measure.Method(() =>
129
- {
130
- for (int i = 0; i < 100; i++)
131
- {
132
- var obj = _testGroup.Get("TestPrefab");
133
- retrievedObjects.Add(obj);
134
- }
135
- })
136
- .WarmupCount(2)
137
- .MeasurementCount(10)
138
- .IterationsPerMeasurement(1)
139
- .GC()
140
- .Run();
141
-
142
- foreach (var obj in retrievedObjects)
143
- {
144
- _testGroup.Return(obj);
145
- }
146
- }
147
-
148
- [UnityTest, Performance]
149
- public IEnumerator Performance_ParentTransformOverhead()
150
- {
151
- yield return _pool.Initialize();
152
-
153
- var withParentTime = 0.0;
154
- var withoutParentTime = 0.0;
155
-
156
- Measure.Method(() =>
157
- {
158
- for (int i = 0; i < 100; i++)
159
- {
160
- var obj = _testGroup.Get("TestPrefab");
161
- _testGroup.Return(obj);
162
- }
163
- })
164
- .WarmupCount(5)
165
- .MeasurementCount(10)
166
- .IterationsPerMeasurement(1)
167
- .GC()
168
- .Run();
169
-
170
- var poolObjectNoParent = new GameObject("TestPoolNoParent");
171
- var poolNoParent = poolObjectNoParent.AddComponent<PrefabPool>();
172
-
173
- var prefabNoParent = new GameObject("TestPrefabNoParent");
174
- prefabNoParent.AddComponent<MockPoolPrefab>();
175
-
176
- var groupNoParent = new PoolGroup
177
- {
178
- id = "TestGroupNoParent",
179
- entries = new System.Collections.Generic.List<PoolEntry>
180
- {
181
- new PoolEntry
182
- {
183
- id = "TestPrefab",
184
- prefab = prefabNoParent,
185
- prewarmCount = 10
186
- }
187
- }
188
- };
189
-
190
- poolNoParent.RegisterGroup(groupNoParent, null);
191
- yield return poolNoParent.Initialize();
192
-
193
- Measure.Method(() =>
194
- {
195
- for (int i = 0; i < 100; i++)
196
- {
197
- var obj = groupNoParent.Get("TestPrefab");
198
- groupNoParent.Return(obj);
199
- }
200
- })
201
- .WarmupCount(5)
202
- .MeasurementCount(10)
203
- .IterationsPerMeasurement(1)
204
- .GC()
205
- .Run();
206
-
207
- poolNoParent.DestroyPool();
208
- UnityObject.DestroyImmediate(poolObjectNoParent);
209
- UnityObject.DestroyImmediate(prefabNoParent);
210
- }
211
- }
212
- }
@@ -1,11 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: 16aa39925a01643c3894a56145c56a21
3
- MonoImporter:
4
- externalObjects: {}
5
- serializedVersion: 2
6
- defaultReferences: []
7
- executionOrder: 0
8
- icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
@@ -1,14 +0,0 @@
1
- using NUnit.Framework;
2
- using Amanotes.Core;
3
-
4
- namespace Amanotes.Core
5
- {
6
- public class PrefabPoolTestsSetup
7
- {
8
- [Test]
9
- public void TestInfrastructureVerification()
10
- {
11
- Assert.IsTrue(true, "PrefabPool test infrastructure is working");
12
- }
13
- }
14
- }
@@ -1,11 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: e61f89af128b84b429ac96d86bd0554f
3
- MonoImporter:
4
- externalObjects: {}
5
- serializedVersion: 2
6
- defaultReferences: []
7
- executionOrder: 0
8
- icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
@@ -1,26 +0,0 @@
1
- using UnityEngine;
2
-
3
- namespace Amanotes.Core
4
- {
5
- public class MockPoolPrefab : MonoBehaviour, IPoolPrefab
6
- {
7
- public int getCallCount;
8
- public int returnCallCount;
9
-
10
- public void OnGet()
11
- {
12
- getCallCount++;
13
- }
14
-
15
- public void OnReturn()
16
- {
17
- returnCallCount++;
18
- }
19
-
20
- public void ResetCounts()
21
- {
22
- getCallCount = 0;
23
- returnCallCount = 0;
24
- }
25
- }
26
- }
@@ -1,11 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: 8e7402f0e0f2440338429ebbb480ffc3
3
- MonoImporter:
4
- externalObjects: {}
5
- serializedVersion: 2
6
- defaultReferences: []
7
- executionOrder: 0
8
- icon: {instanceID: 0}
9
- userData:
10
- assetBundleName:
11
- assetBundleVariant:
@@ -1,8 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: 2ce9db2584cc4444f930d1ab257afcaf
3
- folderAsset: yes
4
- DefaultImporter:
5
- externalObjects: {}
6
- userData:
7
- assetBundleName:
8
- assetBundleVariant:
@@ -1,8 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: f4d670095ee40448b93f6f5dbc2c07e6
3
- folderAsset: yes
4
- DefaultImporter:
5
- externalObjects: {}
6
- userData:
7
- assetBundleName:
8
- assetBundleVariant:
package/Tests.meta DELETED
@@ -1,8 +0,0 @@
1
- fileFormatVersion: 2
2
- guid: 518067fd8d1ac4415b0468af746cce4a
3
- folderAsset: yes
4
- DefaultImporter:
5
- externalObjects: {}
6
- userData:
7
- assetBundleName:
8
- assetBundleVariant: