com.amanotes.prefabpool 1.0.0 → 1.0.2

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CHANGELOG.md.meta +7 -0
  3. package/Docs/PrefabPool-API.md +366 -0
  4. package/Docs/PrefabPool-API.md.meta +7 -0
  5. package/Docs.meta +8 -0
  6. package/README.md +80 -69
  7. package/README.md.meta +7 -0
  8. package/Runtime/AssemblyInfo.cs +3 -0
  9. package/Runtime/AssemblyInfo.cs.meta +11 -0
  10. package/Runtime/MaterialPool.cs.meta +11 -0
  11. package/Runtime/PoolEntryDrawer.cs.meta +11 -0
  12. package/Runtime/PrefabPool.asmdef +3 -0
  13. package/Runtime/PrefabPool.asmdef.meta +7 -0
  14. package/Runtime/PrefabPool.cs +42 -10
  15. package/Runtime/PrefabPool.cs.meta +11 -0
  16. package/Runtime.meta +8 -0
  17. package/Tests/Runtime/PrefabPool.Test.asmdef +11 -0
  18. package/Tests/Runtime/PrefabPool.Test.asmdef.meta +7 -0
  19. package/Tests/Runtime/PrefabPoolGroupTests.cs +224 -0
  20. package/Tests/Runtime/PrefabPoolGroupTests.cs.meta +11 -0
  21. package/Tests/Runtime/PrefabPoolLifecycleTests.cs +257 -0
  22. package/Tests/Runtime/PrefabPoolLifecycleTests.cs.meta +11 -0
  23. package/Tests/Runtime/PrefabPoolPerformanceTests.cs +212 -0
  24. package/Tests/Runtime/PrefabPoolPerformanceTests.cs.meta +11 -0
  25. package/Tests/Runtime/PrefabPoolTestsSetup.cs +14 -0
  26. package/Tests/Runtime/PrefabPoolTestsSetup.cs.meta +11 -0
  27. package/Tests/Runtime/TestHelpers/MockPoolPrefab.cs +26 -0
  28. package/Tests/Runtime/TestHelpers/MockPoolPrefab.cs.meta +11 -0
  29. package/Tests/Runtime/TestHelpers.meta +8 -0
  30. package/Tests/Runtime.meta +8 -0
  31. package/Tests.meta +8 -0
  32. package/package.json +1 -1
  33. package/package.json.meta +7 -0
  34. package/Runtime/Amanotes.PrefabPool.asmdef +0 -14
  35. package/Runtime/MeshRendererExtensions.cs +0 -80
  36. /package/{Editor → Runtime}/PoolEntryDrawer.cs +0 -0
@@ -48,13 +48,12 @@ namespace Amanotes.Core
48
48
  resultGameObject.hideFlags = HideFlags.DontSave;
49
49
  }
50
50
 
51
- // Move to spawn parent when retrieved
52
51
  var t = resultGameObject.transform;
53
52
  t.SetParent(spawnParent, false);
54
53
  t.localPosition = localPosition;
55
54
  t.localRotation = Quaternion.identity;
55
+ resultGameObject.SetActive(true);
56
56
 
57
- // Call OnGet on any IPoolPrefab components
58
57
  var poolComponents = resultGameObject.GetComponents<IPoolPrefab>();
59
58
  foreach (var pp in poolComponents)
60
59
  {
@@ -68,7 +67,6 @@ namespace Amanotes.Core
68
67
  {
69
68
  if (gameObject == null) return;
70
69
 
71
- // Call OnReturn on any IPoolPrefab components
72
70
  var poolComponents = gameObject.GetComponents<IPoolPrefab>();
73
71
  foreach (var pp in poolComponents)
74
72
  {
@@ -76,6 +74,7 @@ namespace Amanotes.Core
76
74
  }
77
75
 
78
76
  var t = gameObject.transform;
77
+ gameObject.SetActive(false);
79
78
  t.SetParent(poolParent, false);
80
79
  t.localPosition = Vector3.zero;
81
80
  _pool.Add(gameObject);
@@ -93,12 +92,15 @@ namespace Amanotes.Core
93
92
  _pool.Clear();
94
93
  }
95
94
 
95
+ public void PurgeDestroyed() => _pool.RemoveAll(obj => obj == null);
96
+
96
97
  public IEnumerator PreAllocate(Transform poolParent)
97
98
  {
98
99
  for (var i = 0; i < prewarmCount; i++)
99
100
  {
100
101
  var instance = UnityObject.Instantiate(prefab, poolParent.position, Quaternion.identity, poolParent);
101
102
  instance.hideFlags = HideFlags.DontSave;
103
+ instance.SetActive(false);
102
104
  _pool.Add(instance);
103
105
  }
104
106
 
@@ -120,16 +122,16 @@ namespace Amanotes.Core
120
122
  _prefabPool = prefabPool;
121
123
  _spawnParent = spawnParent;
122
124
 
123
- // Set parent group reference for each entry
124
125
  foreach (var entry in entries)
125
126
  {
126
127
  entry.SetParentGroup(this);
128
+ entry.PurgeDestroyed();
127
129
  }
128
130
 
129
- // Set parent group reference for fallback entry
130
131
  if (fallbackEntry != null)
131
132
  {
132
133
  fallbackEntry.SetParentGroup(this);
134
+ fallbackEntry.PurgeDestroyed();
133
135
  }
134
136
  }
135
137
 
@@ -166,6 +168,16 @@ namespace Amanotes.Core
166
168
 
167
169
  public static class PrefabPoolExtensions
168
170
  {
171
+ internal static T GetComponentWithInterface<T>(this GameObject go)
172
+ {
173
+ var components = go.GetComponents<MonoBehaviour>();
174
+ for (var i = 0; i < components.Length; i++)
175
+ {
176
+ if (components[i] is T t) return t;
177
+ }
178
+ return default(T);
179
+ }
180
+
169
181
  public static void DestroyLast<T>(this List<T> list) where T : Component
170
182
  {
171
183
  if (list.Count == 0) return;
@@ -247,6 +259,7 @@ namespace Amanotes.Core
247
259
  {
248
260
  [NonSerialized] private bool _isDestroyed = false;
249
261
  [NonSerialized] private bool _isReady = false;
262
+ [NonSerialized] private bool _isInitializing = false;
250
263
  [NonSerialized] private readonly Dictionary<string, PoolGroup> _groups = new Dictionary<string, PoolGroup>();
251
264
  [NonSerialized] private readonly Dictionary<int, PoolEntry> _instanceTypeMap = new Dictionary<int, PoolEntry>();
252
265
  [NonSerialized] private readonly Dictionary<GameObject, PoolEntry> _customPool = new Dictionary<GameObject, PoolEntry>();
@@ -297,6 +310,7 @@ namespace Amanotes.Core
297
310
 
298
311
  _groups[group.id] = group;
299
312
  group.Register(this, spawnParent);
313
+ PurgeStaleInstanceMap();
300
314
  }
301
315
 
302
316
  public void SetSpawnParent(string groupId, Transform spawnParent)
@@ -342,10 +356,23 @@ namespace Amanotes.Core
342
356
  }
343
357
  }
344
358
 
359
+ private void PurgeStaleInstanceMap()
360
+ {
361
+ #if UNITY_EDITOR
362
+ if (_instanceTypeMap.Count == 0) return;
363
+ var stale = new List<int>();
364
+ foreach (var kvp in _instanceTypeMap)
365
+ {
366
+ if (EditorUtility.InstanceIDToObject(kvp.Key) == null) stale.Add(kvp.Key);
367
+ }
368
+ foreach (var id in stale) _instanceTypeMap.Remove(id);
369
+ #endif
370
+ }
371
+
345
372
  public IEnumerator Initialize()
346
373
  {
347
- if (_isReady) yield break;
348
- _isReady = false;
374
+ if (_isReady || _isInitializing) yield break;
375
+ _isInitializing = true;
349
376
 
350
377
  #if UNITY_EDITOR
351
378
  EditorApplication.playModeStateChanged -= HandlePlayModeChanged;
@@ -367,6 +394,7 @@ namespace Amanotes.Core
367
394
  }
368
395
  }
369
396
 
397
+ _isInitializing = false;
370
398
  _isReady = true;
371
399
  }
372
400
 
@@ -391,23 +419,27 @@ namespace Amanotes.Core
391
419
  entry.DestroyPool();
392
420
  }
393
421
 
394
- // Destroy fallback entry if available
395
422
  if (group.fallbackEntry != null)
396
423
  {
397
424
  group.fallbackEntry.DestroyPool();
398
425
  }
399
426
  }
400
-
401
427
  _groups.Clear();
402
428
 
429
+ foreach (var entry in _customPool.Values)
430
+ {
431
+ entry.DestroyPool();
432
+ }
433
+ _customPool.Clear();
434
+
403
435
  #if UNITY_EDITOR
404
436
  foreach (var kvp in _instanceTypeMap)
405
437
  {
406
438
  var obj = EditorUtility.InstanceIDToObject(kvp.Key);
407
439
  if (obj != null) UnityObject.DestroyImmediate(obj);
408
440
  }
409
- _instanceTypeMap.Clear();
410
441
  #endif
442
+ _instanceTypeMap.Clear();
411
443
  }
412
444
  }
413
445
  }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: c06d5864764314d7d87c6e4a21b27c9d
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant:
package/Runtime.meta ADDED
@@ -0,0 +1,8 @@
1
+ fileFormatVersion: 2
2
+ guid: dbf3beacea8f4482b90d80481369ac31
3
+ folderAsset: yes
4
+ DefaultImporter:
5
+ externalObjects: {}
6
+ userData:
7
+ assetBundleName:
8
+ assetBundleVariant:
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "PrefabPool.Tests",
3
+ "rootNamespace": "Amanotes.Core",
4
+ "references": [
5
+ "PrefabPool",
6
+ "Unity.PerformanceTesting"
7
+ ],
8
+ "optionalUnityReferences": [
9
+ "TestAssemblies"
10
+ ]
11
+ }
@@ -0,0 +1,7 @@
1
+ fileFormatVersion: 2
2
+ guid: f4f4c6bd22a404f98bd5cc47335df5d8
3
+ AssemblyDefinitionImporter:
4
+ externalObjects: {}
5
+ userData:
6
+ assetBundleName:
7
+ assetBundleVariant:
@@ -0,0 +1,224 @@
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
+ 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");
127
+
128
+ Assert.IsTrue(objFromA.name.Contains("PrefabA"),
129
+ $"Iteration {i}: Object from GroupA should be PrefabA instance");
130
+ Assert.IsTrue(objFromB.name.Contains("PrefabB"),
131
+ $"Iteration {i}: Object from GroupB should be PrefabB instance");
132
+
133
+ _groupA.Return(objFromA);
134
+ _groupB.Return(objFromB);
135
+
136
+ var reusedFromA = _groupA.Get("PrefabA");
137
+ var reusedFromB = _groupB.Get("PrefabB");
138
+
139
+ Assert.AreEqual(instanceIdA, reusedFromA.GetInstanceID(),
140
+ $"Iteration {i}: GroupA should reuse its own objects");
141
+ Assert.AreEqual(instanceIdB, reusedFromB.GetInstanceID(),
142
+ $"Iteration {i}: GroupB should reuse its own objects");
143
+
144
+ Assert.IsTrue(reusedFromA.name.Contains("PrefabA"),
145
+ $"Iteration {i}: GroupA should never return PrefabB instances");
146
+ Assert.IsTrue(reusedFromB.name.Contains("PrefabB"),
147
+ $"Iteration {i}: GroupB should never return PrefabA instances");
148
+
149
+ _groupA.Return(reusedFromA);
150
+ _groupB.Return(reusedFromB);
151
+ }
152
+ }
153
+
154
+ [UnityTest]
155
+ public IEnumerator ReturnGroupRouting_ReturnToGroup_SubsequentGetRetrievesSameInstanceFromSameGroup()
156
+ {
157
+ yield return _pool.Initialize();
158
+
159
+ for (int i = 0; i < 100; i++)
160
+ {
161
+ var objA1 = _groupA.Get("PrefabA");
162
+ var objA2 = _groupA.Get("PrefabA");
163
+ var objB1 = _groupB.Get("PrefabB");
164
+ var objB2 = _groupB.Get("PrefabB");
165
+
166
+ Assert.IsNotNull(objA1, $"Iteration {i}: GroupA Get 1 should return valid GameObject");
167
+ Assert.IsNotNull(objA2, $"Iteration {i}: GroupA Get 2 should return valid GameObject");
168
+ Assert.IsNotNull(objB1, $"Iteration {i}: GroupB Get 1 should return valid GameObject");
169
+ Assert.IsNotNull(objB2, $"Iteration {i}: GroupB Get 2 should return valid GameObject");
170
+
171
+ var instanceIdA1 = objA1.GetInstanceID();
172
+ var instanceIdA2 = objA2.GetInstanceID();
173
+ var instanceIdB1 = objB1.GetInstanceID();
174
+ var instanceIdB2 = objB2.GetInstanceID();
175
+
176
+ _groupA.Return(objA1);
177
+ _groupA.Return(objA2);
178
+ _groupB.Return(objB1);
179
+ _groupB.Return(objB2);
180
+
181
+ var reusedA1 = _groupA.Get("PrefabA");
182
+ var reusedA2 = _groupA.Get("PrefabA");
183
+ var reusedB1 = _groupB.Get("PrefabB");
184
+ var reusedB2 = _groupB.Get("PrefabB");
185
+
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),
198
+ $"Iteration {i}: Object returned to GroupA should be retrieved from GroupA");
199
+ Assert.IsTrue(reusedInstanceIdsA.Contains(instanceIdA2),
200
+ $"Iteration {i}: Object returned to GroupA should be retrieved from GroupA");
201
+
202
+ Assert.IsTrue(reusedInstanceIdsB.Contains(instanceIdB1),
203
+ $"Iteration {i}: Object returned to GroupB should be retrieved from GroupB");
204
+ Assert.IsTrue(reusedInstanceIdsB.Contains(instanceIdB2),
205
+ $"Iteration {i}: Object returned to GroupB should be retrieved from GroupB");
206
+
207
+ Assert.IsFalse(reusedInstanceIdsA.Contains(instanceIdB1),
208
+ $"Iteration {i}: GroupA should not return objects from GroupB");
209
+ Assert.IsFalse(reusedInstanceIdsA.Contains(instanceIdB2),
210
+ $"Iteration {i}: GroupA should not return objects from GroupB");
211
+
212
+ Assert.IsFalse(reusedInstanceIdsB.Contains(instanceIdA1),
213
+ $"Iteration {i}: GroupB should not return objects from GroupA");
214
+ Assert.IsFalse(reusedInstanceIdsB.Contains(instanceIdA2),
215
+ $"Iteration {i}: GroupB should not return objects from GroupA");
216
+
217
+ _groupA.Return(reusedA1);
218
+ _groupA.Return(reusedA2);
219
+ _groupB.Return(reusedB1);
220
+ _groupB.Return(reusedB2);
221
+ }
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,11 @@
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:
@@ -0,0 +1,257 @@
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
+ var firstInstanceId = firstGet.GetInstanceID();
85
+
86
+ _testGroup.Return(firstGet);
87
+
88
+ var secondGet = _testGroup.Get("TestPrefab");
89
+ Assert.IsNotNull(secondGet, $"Iteration {i}: Second Get should return valid GameObject");
90
+
91
+ var secondInstanceId = secondGet.GetInstanceID();
92
+
93
+ Assert.AreEqual(firstInstanceId, secondInstanceId,
94
+ $"Iteration {i}: Get-Return-Get should reuse same instance (validates pooling eliminates instantiation)");
95
+
96
+ _testGroup.Return(secondGet);
97
+ }
98
+ }
99
+
100
+ [UnityTest]
101
+ public IEnumerator LifecycleCallbackInvocation_GetReturnCycle_InvokesOnGetAndOnReturnExactlyOnce()
102
+ {
103
+ yield return _pool.Initialize();
104
+
105
+ for (int i = 0; i < 100; i++)
106
+ {
107
+ var obj = _testGroup.Get("TestPrefab");
108
+ Assert.IsNotNull(obj, $"Iteration {i}: Get should return valid GameObject");
109
+
110
+ var mock = obj.GetComponent<MockPoolPrefab>();
111
+ Assert.IsNotNull(mock, $"Iteration {i}: GameObject should have MockPoolPrefab component");
112
+
113
+ mock.ResetCounts();
114
+
115
+ var objAgain = _testGroup.Get("TestPrefab");
116
+ var mockAgain = objAgain.GetComponent<MockPoolPrefab>();
117
+ mockAgain.ResetCounts();
118
+
119
+ _testGroup.Return(obj);
120
+
121
+ Assert.AreEqual(1, mock.returnCallCount,
122
+ $"Iteration {i}: OnReturn should be called exactly once during Return");
123
+ Assert.AreEqual(0, mock.getCallCount,
124
+ $"Iteration {i}: OnGet should not be called during Return");
125
+
126
+ var reusedObj = _testGroup.Get("TestPrefab");
127
+ var reusedMock = reusedObj.GetComponent<MockPoolPrefab>();
128
+
129
+ Assert.AreEqual(1, reusedMock.getCallCount,
130
+ $"Iteration {i}: OnGet should be called exactly once during Get");
131
+
132
+ _testGroup.Return(objAgain);
133
+ _testGroup.Return(reusedObj);
134
+ }
135
+ }
136
+
137
+ [UnityTest]
138
+ public IEnumerator PreallocationCorrectness_PrewarmCount_CreatesExactNumberOfPreallocatedObjects()
139
+ {
140
+ for (int iteration = 0; iteration < 100; iteration++)
141
+ {
142
+ var prewarmCount = UnityEngine.Random.Range(1, 20);
143
+
144
+ var poolObject = new GameObject($"TestPool_{iteration}");
145
+ var pool = poolObject.AddComponent<PrefabPool>();
146
+
147
+ var spawnParentObject = new GameObject($"SpawnParent_{iteration}");
148
+ var spawnParent = spawnParentObject.transform;
149
+
150
+ var prefab = new GameObject($"Prefab_{iteration}");
151
+ prefab.AddComponent<MockPoolPrefab>();
152
+
153
+ var group = new PoolGroup
154
+ {
155
+ id = $"Group_{iteration}",
156
+ entries = new System.Collections.Generic.List<PoolEntry>
157
+ {
158
+ new PoolEntry
159
+ {
160
+ id = "TestPrefab",
161
+ prefab = prefab,
162
+ prewarmCount = prewarmCount
163
+ }
164
+ }
165
+ };
166
+
167
+ pool.RegisterGroup(group, spawnParent);
168
+
169
+ yield return pool.Initialize();
170
+
171
+ var retrievedObjects = new System.Collections.Generic.List<GameObject>();
172
+
173
+ for (int i = 0; i < prewarmCount; i++)
174
+ {
175
+ var obj = group.Get("TestPrefab");
176
+ Assert.IsNotNull(obj, $"Iteration {iteration}: Should retrieve pre-allocated object {i}");
177
+ retrievedObjects.Add(obj);
178
+ }
179
+
180
+ Assert.AreEqual(prewarmCount, retrievedObjects.Count,
181
+ $"Iteration {iteration}: Should retrieve exactly {prewarmCount} pre-allocated objects");
182
+
183
+ foreach (var obj in retrievedObjects)
184
+ {
185
+ group.Return(obj);
186
+ }
187
+
188
+ pool.DestroyPool();
189
+ UnityObject.DestroyImmediate(poolObject);
190
+ UnityObject.DestroyImmediate(spawnParentObject);
191
+ UnityObject.DestroyImmediate(prefab);
192
+ }
193
+ }
194
+
195
+ [UnityTest]
196
+ public IEnumerator DynamicPoolExpansion_GetBeyondCapacity_SuccessfullyReturnsValidGameObjects()
197
+ {
198
+ for (int iteration = 0; iteration < 100; iteration++)
199
+ {
200
+ var initialCapacity = UnityEngine.Random.Range(2, 10);
201
+ var extraRequests = UnityEngine.Random.Range(5, 15);
202
+
203
+ var poolObject = new GameObject($"TestPool_{iteration}");
204
+ var pool = poolObject.AddComponent<PrefabPool>();
205
+
206
+ var spawnParentObject = new GameObject($"SpawnParent_{iteration}");
207
+ var spawnParent = spawnParentObject.transform;
208
+
209
+ var prefab = new GameObject($"Prefab_{iteration}");
210
+ prefab.AddComponent<MockPoolPrefab>();
211
+
212
+ var group = new PoolGroup
213
+ {
214
+ id = $"Group_{iteration}",
215
+ entries = new System.Collections.Generic.List<PoolEntry>
216
+ {
217
+ new PoolEntry
218
+ {
219
+ id = "TestPrefab",
220
+ prefab = prefab,
221
+ prewarmCount = initialCapacity
222
+ }
223
+ }
224
+ };
225
+
226
+ pool.RegisterGroup(group, spawnParent);
227
+
228
+ yield return pool.Initialize();
229
+
230
+ var retrievedObjects = new System.Collections.Generic.List<GameObject>();
231
+
232
+ var totalRequests = initialCapacity + extraRequests;
233
+
234
+ for (int i = 0; i < totalRequests; i++)
235
+ {
236
+ var obj = group.Get("TestPrefab");
237
+ Assert.IsNotNull(obj,
238
+ $"Iteration {iteration}: Get beyond capacity should succeed (request {i + 1}/{totalRequests})");
239
+ retrievedObjects.Add(obj);
240
+ }
241
+
242
+ Assert.AreEqual(totalRequests, retrievedObjects.Count,
243
+ $"Iteration {iteration}: Should retrieve {totalRequests} objects (capacity {initialCapacity} + expansion {extraRequests})");
244
+
245
+ foreach (var obj in retrievedObjects)
246
+ {
247
+ group.Return(obj);
248
+ }
249
+
250
+ pool.DestroyPool();
251
+ UnityObject.DestroyImmediate(poolObject);
252
+ UnityObject.DestroyImmediate(spawnParentObject);
253
+ UnityObject.DestroyImmediate(prefab);
254
+ }
255
+ }
256
+ }
257
+ }