From 2e0d3ec4721c41f8963c89f8991d8249cd37167d Mon Sep 17 00:00:00 2001 From: Miguel Tomas Date: Sun, 19 Jan 2020 23:24:24 +0000 Subject: [PATCH] - Added new *ObjectPool* & *GameObjectPool* pools to allow to allow to use object pools independent from the *PoolService*. This allows to have different pools of the same type in the project in different object controllers - Added new interface *IPoolEntityClear* that allows a callback method for entities when they are cleared from the pool - Added new unit tests for the *ObjectPool* **Changed**: - Now the PoolService.Clear() does not take any action parameters. To have a callback when the entity is cleared, please have the entity implement the *IPoolEntityClear* interface --- CHANGELOG.md | 9 + Runtime/PoolService.cs | 285 +++++++++++++++++----------- Tests/Editor/MainInstallerTest.cs | 2 +- Tests/Editor/ObjectPoolTest.cs | 99 ++++++++++ Tests/Editor/ObjectPoolTest.cs.meta | 11 ++ Tests/Editor/PoolServiceTest.cs | 58 +++--- package.json | 2 +- 7 files changed, 318 insertions(+), 148 deletions(-) create mode 100644 Tests/Editor/ObjectPoolTest.cs create mode 100644 Tests/Editor/ObjectPoolTest.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 931ecaa..d288f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2020-01-19 + +- Added new *ObjectPool* & *GameObjectPool* pools to allow to allow to use object pools independent from the *PoolService*. This allows to have different pools of the same type in the project in different object controllers +- Added new interface *IPoolEntityClear* that allows a callback method for entities when they are cleared from the pool +- Added new unit tests for the *ObjectPool* + +**Changed**: +- Now the PoolService.Clear() does not take any action parameters. To have a callback when the entity is cleared, please have the entity implement the *IPoolEntityClear* interface + ## [0.1.1] - 2020-01-06 - Added License diff --git a/Runtime/PoolService.cs b/Runtime/PoolService.cs index dbafc63..bea7473 100644 --- a/Runtime/PoolService.cs +++ b/Runtime/PoolService.cs @@ -1,34 +1,15 @@ using System; using System.Collections.Generic; +using UnityEngine; +using Object = UnityEngine.Object; // ReSharper disable once CheckNamespace namespace GameLovers.Services { /// - /// This interface allows pooled objects to be notified when it is spawned - /// - public interface IPoolEntitySpawn - { - /// - /// Invoked when the Entity is spawned - /// - void OnSpawn(); - } - - /// - /// This interface allows pooled objects to be notified when it is despawned - /// - public interface IPoolEntityDespawn - { - /// - /// Invoked when the entity is despawned - /// - void OnDespawn(); - } - - /// - /// Simple pool implementation that can handle any type of entity objects + /// This service allows to manage multiple pools of different types. + /// The service can only a single pool of the same type. /// public interface IPoolService { @@ -36,13 +17,13 @@ public interface IPoolService /// Initializes a new pool with the given /// It invokes the function every time a new entity is created in the pool /// - void InitPool(int initialSize, Func instantiator); + void InitPool(int initialSize, Func instantiator) where T : new(); /// /// Initializes a new pool with the given and a sample entity given back in the /// It invokes the function every time a new entity is created in the pool /// - void InitPool(int initialSize, T sampleEntity, Func instantiator); + void InitPool(int initialSize, T sampleEntity, Func instantiator) where T : Object; /// /// Checks if exists a pool of the given type already exists or needs to be initialized with @@ -53,65 +34,34 @@ public interface IPoolService /// bool HasPool(Type type); - /// - /// Spawns and returns an entity of the given type - /// This function does not initialize the entity. For that, have the entity implement or do it externally - /// This function throws a if the pool is empty - /// + /// T Spawn(); - /// - /// Despawns the given and returns it back to the pool to be used again later - /// This function does not reset the entity. For that, have the entity implement or do it externally - /// + /// void Despawn(T entity); - /// - /// Despawns all active spawned entities of the given type and returns them back to the pool to be used again later - /// This function does not reset the entity. For that, have the entity implement or do it externally - /// + /// void DespawnAll(); - /// - /// Clears the pool of the given type - /// It calls with every entity remaining in the pool and managed by the pool - /// - void Clear(Action clearAction); + /// + void Clear(); } /// public class PoolService : IPoolService { - private readonly Dictionary pools = new Dictionary(); + private readonly Dictionary _pools = new Dictionary(); /// - public void InitPool(int initialSize, Func instantiator) + public void InitPool(int initialSize, Func instantiator) where T : new() { - InitPool(initialSize, instantiator.Invoke(), newEntity => instantiator.Invoke()); + _pools.Add(typeof(T), new ObjectPool(initialSize, instantiator)); } /// - public void InitPool(int initialSize, T sampleEntity, Func instantiator) + public void InitPool(int initialSize, T sampleEntity, Func instantiator) where T : Object { - if (pools.ContainsKey(typeof(T))) - { - throw new InvalidOperationException($"The pool of type {typeof(T)} was already initialized"); - } - - var pool = new PoolStack - { - Stack = new Stack(), - SpawnedEntities = new List(), - SampleEntity = sampleEntity, - Instatiator = instantiator - }; - - pools.Add(typeof(T), pool); - - for (int i = 0; i < initialSize; i++) - { - pool.Stack.Push(instantiator.Invoke(sampleEntity)); - } + _pools.Add(typeof(T), new GameObjectPool(initialSize, sampleEntity, instantiator)); } /// @@ -123,88 +73,195 @@ public bool HasPool() /// public bool HasPool(Type type) { - return pools.ContainsKey(type); + return _pools.ContainsKey(type); } /// public T Spawn() { - var pool = GetPool(); - T entity = pool.Stack.Count == 0 ? pool.Instatiator.Invoke(pool.SampleEntity) : pool.Stack.Pop(); - var poolEntity = entity as IPoolEntitySpawn; - - pool.SpawnedEntities.Add(entity); - poolEntity?.OnSpawn(); - - return entity; + return GetPool().Spawn(); } /// public void Despawn(T entity) { - var pool = GetPool(); - var poolEntity = entity as IPoolEntityDespawn; - - pool.Stack.Push(entity); - pool.SpawnedEntities.Remove(entity); - poolEntity?.OnDespawn(); + GetPool().Despawn(entity); } /// public void DespawnAll() { - var pool = GetPool(); - - foreach (T entity in pool.SpawnedEntities) - { - var poolEntity = entity as IPoolEntityDespawn; + GetPool().DespawnAll(); + } - pool.Stack.Push(entity); - poolEntity?.OnDespawn(); + /// + public void Clear() + { + GetPool().Clear(); + _pools.Remove(typeof(T)); + } + + private IObjectPool GetPool() + { + if (!_pools.TryGetValue(typeof(T), out IObjectPool pool)) + { + throw new ArgumentException("The pool was not initialized for the type " + typeof(T)); } - - pool.SpawnedEntities.Clear(); + + return pool as IObjectPool; } + } + + /// + /// This interface allows pooled objects to be notified when it is spawned + /// + public interface IPoolEntitySpawn + { + /// + /// Invoked when the Entity is spawned + /// + void OnSpawn(); + } + + /// + /// This interface allows pooled objects to be notified when it is despawned + /// + public interface IPoolEntityDespawn + { + /// + /// Invoked when the entity is despawned + /// + void OnDespawn(); + } + + /// + /// This interface allows pooled objects to be notified when they are cleared from the pool + /// + public interface IPoolEntityCleared + { + /// + /// Invoked when the entity is cleared + /// + void OnCleared(); + } + + /// + /// Simple object pool implementation that can handle any type of entity objects + /// + public interface IObjectPool + { + /// + /// Clears the pool + /// This function does not clear the entity. For that, have the entity implement or do it externally + /// + void Clear(); + + /// + /// Despawns all active spawned entities and returns them back to the pool to be used again later + /// This function does not reset the entity. For that, have the entity implement or do it externally + /// + void DespawnAll(); + } + + /// + public interface IObjectPool : IObjectPool + { + /// + /// Spawns and returns an entity of the given type + /// This function does not initialize the entity. For that, have the entity implement or do it externally + /// This function throws a if the pool is empty + /// + T Spawn(); + + /// + /// Despawns the given and returns it back to the pool to be used again later + /// This function does not reset the entity. For that, have the entity implement or do it externally + /// + void Despawn(T entity); + } + /// + public abstract class ObjectPoolBase : IObjectPool + { + private readonly Stack _stack = new Stack(); + private readonly IList _spawnedEntities = new List(); + private readonly Func _instantiator; + private readonly T _sampleEntity; + + protected ObjectPoolBase(int initSize, T sampleEntity, Func instantiator) + { + _sampleEntity = sampleEntity; + _instantiator = instantiator; + + for (var i = 0; i < initSize; i++) + { + _stack.Push(instantiator.Invoke(sampleEntity)); + } + } + /// - public void Clear(Action clearAction) + public void Clear() { - var pool = GetPool(); - - for (var i = 0; i < pool.Stack.Count; i++) + for (var i = 0; i < _stack.Count; i++) { - T entity = pool.Stack.Pop(); + var entity =_stack.Pop() as IPoolEntityCleared; - clearAction?.Invoke(entity); + entity?.OnCleared(); } - pool.SpawnedEntities.Clear(); - pools.Remove(typeof(T)); + _spawnedEntities.Clear(); } - private PoolStack GetPool() + /// + public T Spawn() { - if (!pools.TryGetValue(typeof(T), out IPoolStack poolStack)) - { - throw new ArgumentException("The pool was not initialized for the type " + typeof(T)); - } + var entity = _stack.Count == 0 ? _instantiator.Invoke(_sampleEntity) : _stack.Pop(); + var poolEntity = entity as IPoolEntitySpawn; + + _spawnedEntities.Add(entity); + poolEntity?.OnSpawn(); + + return entity; + } + + /// + public void Despawn(T entity) + { + var poolEntity = entity as IPoolEntityDespawn; + + _stack.Push(entity); + _spawnedEntities.Remove(entity); + poolEntity?.OnDespawn(); + } - if (poolStack is PoolStack pool) + /// + public void DespawnAll() + { + for (var i = 0; i < _spawnedEntities.Count; i++) { - return pool; + Despawn(_spawnedEntities[i]); } - - throw new ArgumentException("The pool was not properly initialized for the type " + typeof(T)); + + _spawnedEntities.Clear(); } - - private interface IPoolStack {} + } + + /// + public class ObjectPool : ObjectPoolBase where T : new() + { + public ObjectPool(int initSize, Func instantiator) : base(initSize, instantiator(), newEntity => instantiator.Invoke()) + { + } + } - private class PoolStack : IPoolStack + /// + /// + /// implementation for objects of type + /// + public class GameObjectPool : ObjectPoolBase where T : Object + { + public GameObjectPool(int initSize, T sampleEntity, Func instantiator) : base(initSize, sampleEntity, instantiator) { - public Stack Stack; - public List SpawnedEntities; - public Func Instatiator; - public T SampleEntity; } } } \ No newline at end of file diff --git a/Tests/Editor/MainInstallerTest.cs b/Tests/Editor/MainInstallerTest.cs index 6ad523e..dfc6778 100644 --- a/Tests/Editor/MainInstallerTest.cs +++ b/Tests/Editor/MainInstallerTest.cs @@ -44,7 +44,7 @@ public void Bind_NotImplementing_ThrowsException() [Test] public void Resolve_NotBinded_ThrowsException() { - Assert.Throws(() => MainInstaller.Resolve()); + Assert.Throws(() => MainInstaller.Resolve()); } } } \ No newline at end of file diff --git a/Tests/Editor/ObjectPoolTest.cs b/Tests/Editor/ObjectPoolTest.cs new file mode 100644 index 0000000..3a4e0dc --- /dev/null +++ b/Tests/Editor/ObjectPoolTest.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using GameLovers.Services; +using NSubstitute; +using NUnit.Framework; + +// ReSharper disable once CheckNamespace + +namespace GameLoversEditor.Services.Tests +{ + public class ObjectPoolTest + { + private ObjectPool _pool; + private PoolableEntity _poolableEntity; + private int initialSize = 5; + + public class PoolableEntity : IPoolEntitySpawn, IPoolEntityDespawn, IPoolEntityCleared + { + public void OnSpawn() {} + public void OnDespawn() {} + public void OnCleared() {} + } + + [SetUp] + public void Init() + { + _pool = new ObjectPool(initialSize, () => Substitute.For()); + _poolableEntity = Substitute.For(); + } + + [Test] + public void Spawn_Successfully() + { + var newEntity = _pool.Spawn(); + + newEntity.Received().OnSpawn(); + + Assert.AreNotSame(_poolableEntity, newEntity); + } + + [Test] + public void Spawn_EmptyPool_Successfully() + { + var pool = new ObjectPool(initialSize, () => Substitute.For()); + + var newEntity = pool.Spawn(); + + newEntity.Received().OnSpawn(); + + Assert.AreNotSame(_poolableEntity, newEntity); + } + + [Test] + public void Despawn_Successfully() + { + _pool.Despawn(_poolableEntity); + + _poolableEntity.Received().OnDespawn(); + } + + [Test] + public void DespawnAll_Successfully() + { + var entities = new List(); + + for (int i = 0; i < initialSize; i++) + { + entities.Add(Substitute.For()); + } + + _pool.DespawnAll(); + + foreach (var entity in entities) + { + entity.Received().OnDespawn(); + } + } + + [Test] + public void Clear_Successfully() + { + _pool.Despawn(_poolableEntity); + _pool.Clear(); + + _poolableEntity.Received().OnCleared(); + + Assert.DoesNotThrow(() => _pool.Spawn()); + Assert.DoesNotThrow(() => _pool.Despawn(_poolableEntity)); + } + + [Test] + public void Clear_Twice_NothingHappens() + { + _pool.Clear(); + + Assert.DoesNotThrow(() => _pool.Clear()); + } + } +} \ No newline at end of file diff --git a/Tests/Editor/ObjectPoolTest.cs.meta b/Tests/Editor/ObjectPoolTest.cs.meta new file mode 100644 index 0000000..4fa9628 --- /dev/null +++ b/Tests/Editor/ObjectPoolTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e5572d97c03ad4be8a5d0a16c3b9655f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Tests/Editor/PoolServiceTest.cs b/Tests/Editor/PoolServiceTest.cs index 72ef6c2..08ef23d 100644 --- a/Tests/Editor/PoolServiceTest.cs +++ b/Tests/Editor/PoolServiceTest.cs @@ -12,11 +12,13 @@ public class PoolServiceTest { private PoolService _poolService; private PoolableEntity _poolableEntity; + private int initialSize = 5; - public abstract class PoolableEntity : IPoolEntitySpawn, IPoolEntityDespawn + public class PoolableEntity : IPoolEntitySpawn, IPoolEntityDespawn, IPoolEntityCleared { - public abstract void OnSpawn(); - public abstract void OnDespawn(); + public void OnSpawn() {} + public void OnDespawn() {} + public void OnCleared() {} } [SetUp] @@ -24,28 +26,22 @@ public void Init() { _poolService = new PoolService(); _poolableEntity = Substitute.For(); + + _poolService.InitPool(initialSize, () => Substitute.For()); } [Test] public void Initialize_SameType_ThrowsException() { - var initialSize = 5; - - _poolService.InitPool(initialSize, _poolableEntity, poolableEntity => Substitute.For()); - - Assert.Throws(() => + Assert.Throws(() => { - _poolService.InitPool(initialSize, _poolableEntity, poolableEntity => Substitute.For()); + _poolService.InitPool(initialSize, () => Substitute.For()); }); } [Test] public void Spawn_Successfully() { - var initialSize = 5; - - _poolService.InitPool(initialSize, _poolableEntity, poolableEntity => Substitute.For()); - var newEntity = _poolService.Spawn(); newEntity.Received().OnSpawn(); @@ -57,8 +53,9 @@ public void Spawn_Successfully() public void Spawn_EmptyPool_Successfully() { var initialSize = 0; + var poolService = new PoolService(); - _poolService.InitPool(initialSize, _poolableEntity, poolableEntity => Substitute.For()); + poolService.InitPool(initialSize, () => Substitute.For()); var newEntity = _poolService.Spawn(); @@ -70,15 +67,14 @@ public void Spawn_EmptyPool_Successfully() [Test] public void Spawn_NotInitialized_ThrowsException() { - Assert.Throws(() => _poolService.Spawn()); + var poolService = new PoolService(); + + Assert.Throws(() => poolService.Spawn()); } [Test] public void Despawn_Successfully() { - var initialSize = 5; - - _poolService.InitPool(initialSize, _poolableEntity, poolableEntity => Substitute.For()); _poolService.Despawn(_poolableEntity); _poolableEntity.Received().OnDespawn(); @@ -87,20 +83,19 @@ public void Despawn_Successfully() [Test] public void Despawn_NotInitialized_ThrowsException() { - Assert.Throws(() => _poolService.Despawn(_poolableEntity)); + var poolService = new PoolService(); + + Assert.Throws(() => poolService.Despawn(_poolableEntity)); } [Test] public void DespawnAll_Successfully() { - var initialSize = 5; var entities = new List(); - - _poolService.InitPool(initialSize, _poolableEntity, poolableEntity => Substitute.For()); for (int i = 0; i < initialSize; i++) { - entities.Add(_poolService.Spawn()); + entities.Add(Substitute.For()); } _poolService.DespawnAll(); @@ -114,10 +109,10 @@ public void DespawnAll_Successfully() [Test] public void Clear_Successfully() { - var initialSize = 5; + _poolService.Despawn(_poolableEntity); + _poolService.Clear(); - _poolService.InitPool(initialSize, _poolableEntity, poolableEntity => Substitute.For()); - _poolService.Clear(poolableEntity => { }); + _poolableEntity.Received().OnCleared(); Assert.Throws(() => _poolService.Spawn()); Assert.Throws(() => _poolService.Despawn(_poolableEntity)); @@ -126,18 +121,17 @@ public void Clear_Successfully() [Test] public void Clear_NotInitialized_ThrowsException() { - Assert.Throws(() => _poolService.Clear(null)); + var poolService = new PoolService(); + + Assert.Throws(() => poolService.Clear()); } [Test] public void Clear_SameType_ThrowsException() { - var initialSize = 5; - - _poolService.InitPool(initialSize, _poolableEntity, poolableEntity => Substitute.For()); - _poolService.Clear(poolableEntity => { }); + _poolService.Clear(); - Assert.Throws(() => _poolService.Clear(null)); + Assert.Throws(() => _poolService.Clear()); } } } \ No newline at end of file diff --git a/package.json b/package.json index 1003beb..1a3fb98 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.gamelovers.services", "displayName": "Services", - "version": "0.1.1", + "version": "0.2.0", "unity": "2019.3", "description": "This package contains a set of services to ease the development of a basic game architecture. The services provided by this package are:\n\n- CoroutineService to control all coroutines with an end callback\n- PoolService to control all object pools by type\n- MessageBrokerService to help decoupled modules/systems to communicate with each other while maintaining the inversion of control principle\n- TimeService to have a precise control on the game's time (Unix, Unity or DateTime)\n- TickService to have a single control point on Unity update cycle\n- MainInstaller to have a simple dependency injection binding installer", "type": "library",