enemy spawner
This commit is contained in:
264
Game/Scripts/Runtime/Pooling/ObjectPooler.cs
Normal file
264
Game/Scripts/Runtime/Pooling/ObjectPooler.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Game.Scripts.Runtime.Abstractions;
|
||||
using Game.Scripts.Runtime.Data;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Game.Scripts.Runtime.Pooling
|
||||
{
|
||||
[DefaultExecutionOrder(-300)]
|
||||
[DisallowMultipleComponent]
|
||||
public class ObjectPooler : MonoBehaviour, IPool<EnemyDefinition, GameObject>, IEnemyFactory
|
||||
{
|
||||
[SerializeField] private Transform poolRoot;
|
||||
[SerializeField] private bool markAsDontDestroyOnLoad = true;
|
||||
[SerializeField] private int defaultHardCap = 64;
|
||||
|
||||
private readonly Dictionary<string, PoolBucket> _pools = new();
|
||||
private readonly Dictionary<GameObject, PoolItem> _lookup = new();
|
||||
|
||||
public static ObjectPooler SharedInstance { get; private set; }
|
||||
|
||||
public event Action<GameObject, EnemyDefinition> InstanceSpawned;
|
||||
public event Action<GameObject, EnemyDefinition> InstanceDespawned;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
EnsureRoot();
|
||||
|
||||
if (markAsDontDestroyOnLoad && Application.isPlaying)
|
||||
{
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
if (SharedInstance == null)
|
||||
{
|
||||
SharedInstance = this;
|
||||
}
|
||||
}
|
||||
|
||||
public void Prewarm(EnemyDefinition definition, int count)
|
||||
{
|
||||
if (definition == null || definition.Prefab == null || count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = GetKey(definition);
|
||||
var bucket = GetOrCreateBucket(key);
|
||||
bucket.HardCap = bucket.HardCap <= 0 ? defaultHardCap : bucket.HardCap;
|
||||
|
||||
var remaining = Mathf.Max(0, Math.Min(count, bucket.HardCap) - bucket.TotalCount);
|
||||
for (var i = 0; i < remaining; i++)
|
||||
{
|
||||
var instance = CreateInstance(definition, key);
|
||||
if (instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bucket.Available.Enqueue(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetHardCap(EnemyDefinition definition, int hardCap)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = GetKey(definition);
|
||||
var bucket = GetOrCreateBucket(key);
|
||||
bucket.HardCap = Mathf.Max(1, hardCap);
|
||||
}
|
||||
|
||||
public GameObject Spawn(EnemyDefinition definition, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
if (definition == null || definition.Prefab == null)
|
||||
{
|
||||
Debug.LogWarning("Cannot spawn without a valid definition or prefab.", this);
|
||||
return null;
|
||||
}
|
||||
|
||||
var key = GetKey(definition);
|
||||
var bucket = GetOrCreateBucket(key);
|
||||
bucket.HardCap = bucket.HardCap <= 0 ? defaultHardCap : bucket.HardCap;
|
||||
|
||||
if (bucket.Available.Count == 0 && bucket.TotalCount >= bucket.HardCap)
|
||||
{
|
||||
Debug.LogWarning($"Pool for {key} reached hard cap of {bucket.HardCap}", this);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bucket.Available.Count == 0)
|
||||
{
|
||||
var instance = CreateInstance(definition, key);
|
||||
if (instance != null)
|
||||
{
|
||||
bucket.Available.Enqueue(instance);
|
||||
}
|
||||
}
|
||||
|
||||
if (bucket.Available.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var go = bucket.Available.Dequeue();
|
||||
if (!_lookup.TryGetValue(go, out var item))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
bucket.Active.Add(go);
|
||||
go.transform.SetParent(null, true);
|
||||
go.transform.SetPositionAndRotation(position + definition.PrefabPivotOffset, rotation);
|
||||
go.SetActive(true);
|
||||
|
||||
InstanceSpawned?.Invoke(go, definition);
|
||||
return go;
|
||||
}
|
||||
|
||||
public void Despawn(GameObject instance)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_lookup.TryGetValue(instance, out var item))
|
||||
{
|
||||
Destroy(instance);
|
||||
return;
|
||||
}
|
||||
|
||||
var bucket = GetOrCreateBucket(item.Key);
|
||||
bucket.Active.Remove(instance);
|
||||
|
||||
instance.SetActive(false);
|
||||
instance.transform.SetParent(poolRoot, false);
|
||||
bucket.Available.Enqueue(instance);
|
||||
item.Handle?.NotifyDespawned();
|
||||
InstanceDespawned?.Invoke(instance, item.Definition);
|
||||
}
|
||||
|
||||
void IPool<EnemyDefinition, GameObject>.Prewarm(EnemyDefinition key, int count) => Prewarm(key, count);
|
||||
|
||||
GameObject IPool<EnemyDefinition, GameObject>.Get(EnemyDefinition key) => Spawn(key, Vector3.zero, Quaternion.identity);
|
||||
|
||||
void IPool<EnemyDefinition, GameObject>.Release(EnemyDefinition key, GameObject value) => Despawn(value);
|
||||
|
||||
GameObject IEnemyFactory.Spawn(EnemyDefinition definition, Vector3 position, Quaternion rotation) => Spawn(definition, position, rotation);
|
||||
|
||||
void IEnemyFactory.Despawn(GameObject instance) => Despawn(instance);
|
||||
|
||||
private PoolBucket GetOrCreateBucket(string key)
|
||||
{
|
||||
if (!_pools.TryGetValue(key, out var bucket))
|
||||
{
|
||||
bucket = new PoolBucket { HardCap = defaultHardCap };
|
||||
_pools.Add(key, bucket);
|
||||
}
|
||||
|
||||
return bucket;
|
||||
}
|
||||
|
||||
private GameObject CreateInstance(EnemyDefinition definition, string key)
|
||||
{
|
||||
if (definition == null || definition.Prefab == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
EnsureRoot();
|
||||
var go = Instantiate(definition.Prefab, poolRoot);
|
||||
go.name = $"{definition.Prefab.name}_Pooled";
|
||||
go.SetActive(false);
|
||||
|
||||
var handle = go.GetComponent<PooledInstance>();
|
||||
if (handle == null)
|
||||
{
|
||||
handle = go.AddComponent<PooledInstance>();
|
||||
}
|
||||
|
||||
handle.Initialize(this, key, definition);
|
||||
_lookup[go] = new PoolItem(definition, key, handle);
|
||||
return go;
|
||||
}
|
||||
|
||||
private static string GetKey(EnemyDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return string.IsNullOrEmpty(definition.Id) ? definition.name : definition.Id;
|
||||
}
|
||||
|
||||
private sealed class PoolBucket
|
||||
{
|
||||
public readonly Queue<GameObject> Available = new();
|
||||
public readonly HashSet<GameObject> Active = new();
|
||||
public int HardCap;
|
||||
public int TotalCount => Available.Count + Active.Count;
|
||||
}
|
||||
|
||||
private sealed class PoolItem
|
||||
{
|
||||
public readonly EnemyDefinition Definition;
|
||||
public readonly string Key;
|
||||
public readonly PooledInstance Handle;
|
||||
|
||||
public PoolItem(EnemyDefinition definition, string key, PooledInstance handle)
|
||||
{
|
||||
Definition = definition;
|
||||
Key = key;
|
||||
Handle = handle;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRoot()
|
||||
{
|
||||
if (poolRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var root = new GameObject("[Pool]");
|
||||
root.transform.SetParent(transform, false);
|
||||
poolRoot = root.transform;
|
||||
}
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class PooledInstance : MonoBehaviour
|
||||
{
|
||||
private ObjectPooler _pool;
|
||||
private string _key;
|
||||
private EnemyDefinition _definition;
|
||||
|
||||
public event Action<PooledInstance> Despawned;
|
||||
|
||||
internal void Initialize(ObjectPooler pool, string key, EnemyDefinition definition)
|
||||
{
|
||||
_pool = pool;
|
||||
_key = key;
|
||||
_definition = definition;
|
||||
}
|
||||
|
||||
public void ReturnToPool()
|
||||
{
|
||||
_pool?.Despawn(gameObject);
|
||||
}
|
||||
|
||||
internal void NotifyDespawned()
|
||||
{
|
||||
Despawned?.Invoke(this);
|
||||
}
|
||||
|
||||
public EnemyDefinition Definition => _definition;
|
||||
public string Key => _key;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user