Skip to content

Instantly share code, notes, and snippets.

@onewinter
Last active March 22, 2023 20:17
Show Gist options
  • Save onewinter/2680facb018c1473cb2565af49a46dfc to your computer and use it in GitHub Desktop.
Save onewinter/2680facb018c1473cb2565af49a46dfc to your computer and use it in GitHub Desktop.
A Unity 2021+ Pooling Framework using TypeObjects and ScriptableObjects; GameEffect & GameEffectSetup show how to use it to easily start/stop Particle Systems effects; Pickup & PickupSetup show how to use it to create pickups like Gems or Coins.
using UnityEngine;
public class GameEffect : PooledObjectBase
{
public Transform EffectTarget;
public float EffectDuration;
[SerializeField] private float timer;
public override void BeforeEnable()
{
base.BeforeEnable();
timer = 0;
}
private void Update()
{
timer += Time.deltaTime;
if (EffectDuration > 0f && timer >= EffectDuration) // first see if the effect has a timer
{
ReleaseToPool();
return;
}
// if no timer, see if we should be following a target
if (EffectTarget && EffectTarget.gameObject.activeInHierarchy) transform.position = EffectTarget.position;
// otherwise quit if we have no target
else if (EffectDuration == 0 && !EffectTarget) ReleaseToPool();
}
}
using UnityEngine;
[CreateAssetMenu(fileName = "GameEffect_", menuName = "Scriptable Objects/Game Effect", order = 51)]
public class GameEffectSetup : PooledObjectSetup
{
public GameEffect PlayEffect(Vector3 position, Transform target = null, float duration = 0f)
{
var newEffect = (GameEffect)GetPooledObject(position);
newEffect.EffectDuration = duration;
newEffect.EffectTarget = target;
return newEffect;
}
public GameEffect PlayEffect(Vector3 position, float duration = 0f) => PlayEffect(position, null, duration);
public void StopEffect(GameEffect effect)
{
effect.EffectDuration = 0;
effect.EffectTarget = null;
}
}
using System;
using UnityEngine;
using Random = UnityEngine.Random;
public enum PickupTypes{Wood, Stone, Material, Meat}
public class Pickup : PooledObjectBase
{
[SerializeField] private GameData gameData;
[SerializeField] private GameEvents gameEvents;
[SerializeField] private PickupTypes pickupType;
[SerializeField] private int amount;
private float timer = 0;
private PickupSetup PickupObjectSetup => (PickupSetup)ObjectSetup;
public override void InitializeObjectSetup()
{
base.InitializeObjectSetup();
pickupType = PickupObjectSetup.PickupType;
}
private void OnTriggerEnter(Collider other)
{
var player = other.GetComponent<Player>();
if (!player) return;
var message = "Picked up " + amount + " " + PickupObjectSetup.PickupName + ".";
switch(pickupType)
{
case PickupTypes.Wood:
gameData.Wood += amount;
gameData.Food += amount / 4f;
gameEvents.WoodCollected.Raise(amount);
break;
case PickupTypes.Stone:
gameData.Stone += amount;
gameData.Food += amount / 4f;
gameEvents.StoneCollected.Raise(amount);
break;
case PickupTypes.Material:
gameData.Material += amount;
gameData.Food += amount / 4f;
gameEvents.MaterialCollected.Raise(amount);
break;
case PickupTypes.Meat:
gameData.Meat += amount;
gameData.Food += amount;
gameEvents.MeatCollected.Raise(amount);
break;
default:
throw new ArgumentOutOfRangeException();
}
gameEvents.AudioEvent.Raise(PickupObjectSetup.PickupSound);
gameEvents.GameMessageEvent.Raise(new Message(PickupObjectSetup.PickupIcon, message, 1f));
ReleaseToPool();
}
public override void BeforeEnable()
{
base.BeforeEnable();
amount = Random.Range(PickupObjectSetup.PickupMin, PickupObjectSetup.PickupMax) + gameData.AdditionalPickup;
timer = 0;
}
void Update()
{
timer += Time.deltaTime;
// pickups expire after N secs
if (timer > GameData.PickupExpireTime) ReleaseToPool();
}
}
using UnityEngine;
[CreateAssetMenu (fileName = "Pickup_", menuName = "Pickup Setup", order = 51)]
public class PickupSetup : PooledObjectSetup
{
public string PickupName;
public int PickupMin = 1;
public int PickupMax = 3;
public PickupTypes PickupType;
public AudioClip PickupSound;
public Sprite PickupIcon;
}
using System;
using UnityEngine;
using UnityEngine.Pool;
public abstract class PooledObjectBase : MonoBehaviour
{
protected PooledObjectSetup ObjectSetup;
private IObjectPool<PooledObjectBase> objectPool;
public virtual void BeforeDisable() { } // called when object returned to pool
public virtual void BeforeDestroy() { } // called when object deleted from pool
public virtual void InitializeObjectSetup() { } // called when object created in pool
public virtual void BeforeEnable() { } // called when object taken from pool
// called when pooled object first created
public void AssignObjectSetup(PooledObjectSetup newSetup, IObjectPool<PooledObjectBase> newPool)
{
ObjectSetup = newSetup;
objectPool = newPool;
}
// called to return object to pool
public void ReleaseToPool()
{
StopAllCoroutines(); // this might not be necessary, SetActive(false) automatically does this
// only reason is if we want the coroutine to stop mid loop
try
{
// try/catch in case we try to recycle the same enemy twice
objectPool.Release(this);
}
catch (Exception e)
{
// ignored
}
}
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
public abstract class PooledObjectSetup : ScriptableObject
{
[SerializeField] private PooledObjectBase basePrefab;
[SerializeField] private List<GameObject> modelPrefabs;
private IObjectPool<PooledObjectBase> objectPool;
private Transform poolOrganizer;
private GameObject masterParent;
private void OnEnable()
{
objectPool = new ObjectPool<PooledObjectBase>(CreatePooledObject, OnTakeObjectFromPool, OnObjectReturnedToPool,
OnDestroyObject);
}
private void OnTakeObjectFromPool(PooledObjectBase pooledObject)
{
if (!pooledObject) return; // sometimes the pool destroys an object before we can grab it...
pooledObject.BeforeEnable();
pooledObject.gameObject.SetActive(true);
// then OnEnable() runs
}
private void OnObjectReturnedToPool(PooledObjectBase pooledObject)
{
if (!pooledObject) return; // sometimes the pool destroys an object before we can return it...
pooledObject.BeforeDisable();
pooledObject.gameObject.SetActive(false);
// then OnDisable() runs
}
private void OnDestroyObject(PooledObjectBase pooledObject)
{
if (!pooledObject) return; // sometimes the pool destroys an object before we can return it...
pooledObject.BeforeDestroy();
Destroy(pooledObject.gameObject);
// then OnDestroy() runs
}
private void CheckPoolOrganizer()
{
if (poolOrganizer) return;
var poolOrganizerGO = new GameObject("Pool: " + this.name);
poolOrganizer = poolOrganizerGO.transform;
if (!masterParent) masterParent = GameObject.Find("Pools");
if (!masterParent) masterParent = new GameObject("Pools");
poolOrganizer.parent = masterParent.transform;
}
private PooledObjectBase CreatePooledObject()
{
var newObject = Instantiate(basePrefab);
newObject.AssignObjectSetup(this, objectPool); // assign our type object setup
newObject.InitializeObjectSetup(); // run whatever init code this object has using its setup
if (modelPrefabs.Count > 0)
{
// bring in a random model from our list
var model = Instantiate(modelPrefabs.RandomItem(), newObject.transform);
model.name = "Model";
}
CheckPoolOrganizer();
newObject.transform.parent = poolOrganizer;
newObject.name = this.name;
return newObject;
}
public PooledObjectBase GetPooledObject(Vector3 position, Transform parent = null)
{
PooledObjectBase newObject;
do
{
newObject = objectPool.Get();
} while (!newObject); // ensure we actually get an object from the pool
newObject.transform.position = position;
if (parent) newObject.transform.parent = parent;
//newObject.Initialize(this);
return newObject;
}
}
public static class Extensions
{
public static T RandomItem<T>(this T[] array)
{
return array.Length == 0 ? default : array[Random.Range(0, array.Length)];
}
public static T RandomItem<T>(this IList<T> array)
{
return array.Count == 0 ? default : array[Random.Range(0, array.Count)];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment