A Population Spawner I built once, then needed twice — so I packaged it instead of copy-pasting it.
Owed and Shrunken both needed the same thing: a way to scatter objects across a scene from a weighted pool, without hand-placing every instance. I built that system once for Owed. When Shrunken needed it too, I had a choice — copy the script and diverge, or pull it out into something both projects could depend on without duplicating the logic. I packaged it as a Unity Package Manager (UPM) module instead, with a custom Inspector UI so designers — including future me — could configure a spawn pool without touching code.
A spawner that only works for one project isn't a tool, it's a script. The moment Shrunken needed the same weighted-random population logic Owed used, I had to decide whether "shared" meant copy-and-modify or genuinely reusable. Copy-and-modify is faster the first time and a liability every time after — two divergent copies of the same logic is two places to find and fix the same bug.
I wanted the tool to be configurable entirely from the Inspector — no code changes needed to add a new prefab to a pool, adjust spawn weights, or swap which points to a different project's environment. That meant leaning on ScriptableObjects as the data layer and writing a custom Editor script rather than relying on Unity's default Inspector rendering.
The package centers on a Population Spawner: a ScriptableObject-driven pool of weighted prefabs, populated into a scene at designer-defined spawn points, with a custom Inspector UI for one-click generate and clear.
The package separates data from behavior the same way ScriptableObjects are meant to: the population pool is pure data, holding prefab references and weights with no logic of its own. The spawner component reads that data at runtime and owns the actual instantiation, weighted selection, and cleanup — meaning a designer can create a new pool asset, populate it with prefabs, and hand it to a spawner without a programmer touching either side.
PopulationPoolData (ScriptableObject)
→ holds weighted prefab entries, no logic
PopulationSpawner (MonoBehaviour)
→ reads pool data at runtime
→ owns weighted selection, instantiation, cleanup
→ exposes Generate() / Clear() to custom Inspector buttons
Because the pool is a ScriptableObject asset rather than a hardcoded list, the same spawner component can point at entirely different pools per project — Owed's environmental set dressing and Shrunken's puzzle-room hazards use the same underlying spawner logic with different data assets plugged in.
Custom Inspector buttons need editor-only guards. Generate() and Clear() are meant to run in
the editor for level-dressing, not at runtime — without guarding them behind
#if UNITY_EDITOR, the Inspector buttons and their supporting code would ship
into builds where they serve no purpose and add dead weight.
MaterialPropertyBlock over CreateDynamicMaterialInstance in edit mode. Early spawn-preview work created material instances every time the Inspector regenerated a preview, which leaked instances across editor sessions. Switching to MaterialPropertyBlock avoided creating new material assets just to preview a color or variant change.
Why It Matters
This is the first project I built specifically to be reused rather than shipped as-is. It's a small tool, but it's the credential that backs up "tools programmer" as a role I'm actually targeting, not just a resume line — it's infrastructure two other projects depend on today.