Building Your Own Systems
How to design small, reusable pieces of game code, shown by building your own object pool and cooldown timer from scratch.
A good system is like a vending machine. It has a few clear buttons on the front (put money in, pick a snack), and you never need to open it up to use it. Inside it can be as complicated as it likes. Your other scripts should use your systems the same way: press a button, get a result, never reach inside.
What counts as a "system"?
Any piece of code that does one job for the rest of your game: spawning and reusing objects, cooldowns, saving, playing sounds, tracking score. Unity gives you some of these already (like ObjectPool), but building your own teaches you how they work and lets you shape them to your game.
Five rules for a good system
- One job. A pool reuses objects. It doesn't also play sounds and track score.
- Few, clear buttons. Keep the public methods small:
Get(),Release(). Make everything elseprivate. - Doesn't know about your game. A pool shouldn't know what a "Zombie" is. That way you can reuse it in your next project.
- Plain C# if it doesn't need Unity. A cooldown timer doesn't need to be on a GameObject, so don't make it a MonoBehaviour.
- Start simple. Build the smallest version that works, use it, and add features only when you actually need them.
Build 1: your own object pool
Instead of destroying bullets and creating new ones, we'll switch them off and keep them on a shelf for next time. (Why this helps: Object Pooling.)
Step 1: a way for objects to reset themselves
A reused bullet still has its old speed and position. We'll give poolable objects two "hooks" to reset themselves. An interface is a promise that a class has certain methods:
public interface IPoolable
{
void OnTakenFromPool(); // reset here: health, speed, timers...
void OnReturnedToPool(); // clean up here: stop sounds, clear targets...
}
Step 2: the pool
using System.Collections.Generic;
using UnityEngine;
public class SimplePool<T> where T : Component
{
private readonly T prefab;
private readonly Transform parent;
private readonly Queue<T> shelf = new Queue<T>();
public SimplePool(T prefab, int startCount, Transform parent = null)
{
this.prefab = prefab;
this.parent = parent;
for (int i = 0; i < startCount; i++)
shelf.Enqueue(CreateNew()); // make some up front, during loading
}
public T Get(Vector3 position, Quaternion rotation)
{
T item = shelf.Count > 0 ? shelf.Dequeue() : CreateNew();
item.transform.SetPositionAndRotation(position, rotation);
item.gameObject.SetActive(true);
if (item is IPoolable p) p.OnTakenFromPool();
return item;
}
public void Release(T item)
{
if (!item.gameObject.activeSelf) return; // already on the shelf
if (item is IPoolable p) p.OnReturnedToPool();
item.gameObject.SetActive(false);
shelf.Enqueue(item);
}
private T CreateNew()
{
T item = Object.Instantiate(prefab, parent);
item.gameObject.SetActive(false);
return item;
}
}
A few things to notice:
<T> where T : Componentmakes it a generic: the same code works for a pool ofBullet,EnemyorParticleSystem.- It's not a MonoBehaviour. Whoever owns it creates it with
new. - The
activeSelfcheck stops the same bullet being put on the shelf twice.
Step 3: use it
using UnityEngine;
public class Gun : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
private SimplePool<Bullet> pool;
void Awake() => pool = new SimplePool<Bullet>(bulletPrefab, 20);
public void Fire()
{
Bullet b = pool.Get(transform.position, transform.rotation);
b.Launch(pool); // tell the bullet which pool to go back to
}
}
using UnityEngine;
public class Bullet : MonoBehaviour, IPoolable
{
[SerializeField] private float speed = 30f;
[SerializeField] private float lifetime = 2f;
private SimplePool<Bullet> pool;
private float timeLeft;
public void Launch(SimplePool<Bullet> owner) => pool = owner;
public void OnTakenFromPool() { timeLeft = lifetime; } // fresh start
public void OnReturnedToPool() { } // nothing to clean up yet
void Update()
{
transform.position += transform.forward * speed * Time.deltaTime;
timeLeft -= Time.deltaTime;
if (timeLeft <= 0) pool.Release(this); // instead of Destroy(gameObject)
}
}
Build 2: a cooldown timer
Almost every game needs "you can only do this once every X seconds": dashes, attacks, abilities. Instead of writing timer code in every script, write it once:
using UnityEngine;
public class Cooldown
{
private readonly float duration;
private float readyAt;
public Cooldown(float seconds) => duration = seconds;
public bool IsReady => Time.time >= readyAt;
public float Remaining => Mathf.Max(0f, readyAt - Time.time);
public float Progress01 => duration <= 0 ? 1f : 1f - Remaining / duration; // for UI bars
// Returns true (and starts the cooldown) only if it was ready
public bool TryUse()
{
if (!IsReady) return false;
readyAt = Time.time + duration;
return true;
}
}
Now any script gets a cooldown in two lines:
// needs: using UnityEngine.InputSystem;
private readonly Cooldown dash = new Cooldown(1.5f);
void Update()
{
if (Keyboard.current.leftShiftKey.wasPressedThisFrame && dash.TryUse())
Dash();
}
Notice how this follows the rules: one job, three simple buttons (IsReady, TryUse, Remaining), plain C#, and it knows nothing about dashing.
Where to put shared systems
| Approach | When |
|---|---|
| A field on the script that uses it | Only one script needs it, like the Gun's pool. Simplest; start here. |
| A ScriptableObject asset | Several objects share data or events, set up in the Inspector. |
| A single manager in the scene (a "singleton") | Truly global things like an audio manager. Use sparingly: it makes everything depend on it. |
More systems worth building yourself
- A state machine: idle / patrol / chase for enemies.
- A save system: turn game data into a file and back.
- An audio manager: a pool of AudioSources so many sounds can play at once.
- A floating damage number spawner, using your pool.
Common Mistakes
- Building a huge "GameManager" that does everything. Split it into small systems.
- Making every system a singleton "just in case".
- Resetting pooled objects in
StartorAwake. Those only run once; reset in your "taken from pool" hook. - Adding features you might need one day. Build what you need today.
See also
- Object Pooling (Unity's built-in pool)
- Generics and Inheritance & Interfaces
- Better Ways to Do Things
This page is a simplified, AI-assisted interpretation. For the complete and authoritative reference, see the official documentation.