Region Population System
This pair of features—RegionPopulationController (component) and RegionSpawnLibrary (ScriptableObject)—adds designer-friendly, macro control over local/“tile” spawns on top of the Runtime Spawner package. You get global knobs for density and encounter mix without hand-editing every LocalAreaSpawner.
RegionPopulationController (component)
What it does
A scene-level controller that:
Scales each active
LocalAreaSpawner’s desired min/max population based on a step (e.g., fromSpawnDirector) plus an authorable density curve.Enables or disables regions dynamically (e.g., quiet during “calm” steps) using an enable threshold.
Builds each region’s spawn list from a global RegionSpawnLibrary (tags + step ranges), optionally replacing or appending to what’s authored on the region.
Pre-warms pools for all resolved entries to keep hitches down.
It does not spawn by itself; it orchestrates inputs to the existing spawn loops.
How it works (in one picture)
SpawnDirector (step 0..N) ─┐
│
density curve │ LocalAreaSpawner (per tile)
+ enable threshold│ - Min/Max (base)
├─► RegionPopulationController
│ - Computes per-tile min/max override
RegionSpawnLibrary (rules) ─┘ - Resolves per-tile spawn list (tags + step)
- Prewarms poolsKey concepts
Base vs Override: Each
LocalAreaSpawnerstill has MinObjectCount / MaxObjectCount. The controller computes a scaled “DesiredRangeOverride” at runtime (per region). The region spawn loop honors the override if present; otherwise it uses the base values.Enable Threshold: If the effective density for a region falls below
enableThreshold, the controller quietly unregisters that region fromRuntimeSpawner. When density rises again, it re-registers.Tags: The controller asks the region for semantic tags to match rules:
Implement
RegionPopulationController.IRegionTagProvideron any component under the region root and return strings (e.g.,"industrial","residential").If none is provided, the region’s Unity Tag (if not “Untagged”) is used.
If neither exists, the region has no tags (only rules with no tag filters will match).
Step input: By default, the controller listens to
SpawnDirector.OnStepChanged. If you drive difficulty elsewhere, callApplyStepFromDirector(step, stepsCount)yourself.
Inspector (fields you’ll see)
References
RuntimeSpawner(required)SpawnDirector(optional; used to auto-receive step changes)RegionSpawnLibrary(global rules asset)
Discovery
autoDiscoverAtStart: also picks up anyLocalAreaSpawneralready in the scene (useful for static scenes).
Scaling
enableThreshold(0–1): regions under this effective density are disabled.localDensityByStep(Curve): x = normalized step (0..1) → multiplier (e.g., 0.8 → 1.7).useJitter: adds ±10% deterministic per-region variety.
Diagnostics
logChanges: logs enables/disables & list refreshes.
Setup (quick start)
Add a
RegionPopulationControllerto your gameplay scene.Assign your scene’s
RuntimeSpawner.(Optional) Assign your
SpawnDirector(or callApplyStepFromDirectoryourself).Create a
RegionSpawnLibraryasset and assign it.Author your tiles with
LocalAreaSpawner(BoxCollider isTrigger). Set base Min/Max.(Optional) Tag tiles via Unity Tag or an
IRegionTagProvidercomponent.Press Play—step changes will scale density and rebuild per-tile spawn lists automatically.
Runtime API (for advanced control)
ApplyStepFromDirector(int step, int stepsCount, float localDensityOverride = -1f)Apply a new step; passlocalDensityOverride ≥ 0to force a density multiplier for this pass.RegisterRegion(LocalAreaSpawner)/UnregisterRegion(LocalAreaSpawner)Usually handled viaRuntimeSpawnerevents; exposed for procedural flows.SetLibrary(RegionSpawnLibrary)Swap libraries at runtime.
Troubleshooting
“My region never spawns.”
Check the enableThreshold: if density is below threshold, the controller unregisters the region.
Verify your RegionSpawnLibrary has matching rules or fallback entries.
Confirm the region has a BoxCollider (isTrigger) and is being registered by
RuntimeSpawner.
“New enemy didn’t show up across tiles.”
Add a Rule for it (see below), confirm tags/step range, and ensure weight > 0.
Pools are pre-warmed, but the Global/Region caps in
RuntimeSpawnerand onSpawnEntry.maxPopulationstill apply.
“Design wants a quiet step.”
Lower the density curve at that step so regions fall below
enableThreshold(they’ll disable cleanly).
RegionSpawnLibrary (asset)
What it is
A global, designer-authored rule table that says:
“When the director is at step X and a region has tags Y/Z, include SpawnEntry ‘Grunt’ with weight W,” …optionally replacing the tile’s authoring instead of appending.
This lets you add (or remove) enemies once and have all matching tiles update automatically.
Create one
Assets → Create → Runtime Spawner → Region Spawn Library
Assign it to the RegionPopulationController.
Anatomy of a Rule
Each Rule contains:
Name: For readability in inspectors/logs.
Entry: A
SpawnEntry(what to spawn).Filters:
Required Tags (ALL): every tag must be present on the region.
Any Tags (ANY): at least one must be present.
Min/Max Step: inclusive step range for the rule to be active.
Weights:
Weight by Step (Curve): x = normalized step (0..1). If empty, weight = 1.
When multiple rules match a region, higher weight sorts first; ties break by name.
Behavior:
Replace Instead of Append: if any matched rule sets this, the final list replaces the tile’s authored entries (rather than appending to them).
Caps (optional):
Per-Region Cap: extra hard cap for this entry in this region (0 = off). (Note: the core loops always respect
SpawnEntry.maxPopulation. This field is a hook for custom policies if you choose to enforce it.)
Fallback Entries
If no rules match, the library’s Fallback Entries list is used (or nothing, if empty).
Authoring patterns
Add an enemy across “industrial street” tiles at mid/late steps
Rule:
Entry:
Enemy_GruntRequired Tags:
industrialAny Tags:
streetMin Step:
2, Max Step:4Weight by Step: curve that rises from 0.5 at 0.5 to 1.0 at 1.0
Replace Instead of Append: off (keeps tile flavor)
Force a bespoke palette for “fortress” tiles at high threat
Rule A:
Entry:
Enemy_EliteGuardRequired Tags:
fortressMin Step:
3, Max Step:4Replace Instead of Append: on
(Add more Rules with the same flag; replacement is global for the resolved set.)
Ensure there’s always something
Fallback Entries:
Enemy_Scavenger,Enemy_Wolf
Tagging guidelines
Prefer stable, semantic tags:
industrial,residential,forest,river,outskirts.Use Unity Tag for quick prototypes; move to
IRegionTagProviderwhen tiles need multiple tags.Keep tag vocabulary short; rules are easier to reason about.
Designer workflow (recommended)
Place
LocalAreaSpawnervolumes per tile. Set Min/Max for that tile’s “typical” density.Add
RegionPopulationControllerto the scene and assign aRegionSpawnLibrary.Model your pacing in
SpawnDirector(steps), then set the controller’s density curve:Low steps below
enableThresholdto quiet down.Mid steps around
1.0×.High steps up to
1.5–1.8×(watch your global caps).
Author global rules in the RegionSpawnLibrary:
Tag tiles (Unity Tag or provider).
Add rules for each enemy or set.
Use Replace for curated, step-gated takeovers; otherwise append.
Provide Fallback for safety.
Playtest:
Watch the controller’s inspector runtime panel for each region:
Effective DesiredRangeOverride (min/max)
DensityMult
Resolved entries (via the region’s inspector)
Tweak density curve, enable threshold, and rule weights.
Performance notes
The controller pre-warms pools for all resolved entries per region.
Rebuilds happen on step changes or when new regions register (procedural tiles).
The per-step recompute is lightweight: list filtering + small sorts.
FAQs
Q: Do I still need to set Min/Max on LocalAreaSpawner?
A: Yes—those are your base values. The controller computes a runtime override (scaled) that the region loop honors first.
Q: What if I don’t use SpawnDirector?
A: Call ApplyStepFromDirector(step, stepsCount) yourself (e.g., from your own difficulty system).
Q: What if a tile has no tags? A: Only rules with no tag filters (or your Fallback) will apply.
Q: I added a rule with Replace, but the old entries still show. A: Replacement applies to the resolved set the controller writes at runtime. Make sure:
The region matches the rule’s tags and step range.
The rule’s weight > 0 at the current step.
You’re inspecting during play (the runtime list is not serialized).
Versioning / Compatibility
Requires Runtime Spawner 1.4.0 or higher with region loop support for
LocalAreaSpawner.DesiredRangeOverride.RuntimeSpawnermust raise region registration events. If you’re on an older version, update the package or bind regions manually withRegisterRegion/UnregisterRegion.
One-minute checklist
Fundamentals
RegionPopulationController is a central controller that manages region densities and region-specific spawn lists for all LocalAreaSpawner components in a scene. It is designed to connect high-level pacing (via SpawnDirector or custom logic) to per-region spawn behavior using a rule-based RegionSpawnLibrary.
At runtime it:
Tracks all registered
LocalAreaSpawnerregions.Applies a step-driven density curve to scale each region’s target min/max population.
Resolves region spawn lists from a global
RegionSpawnLibrary, based on region tags and step ranges.Prewarms pools for the resolved entries via
RuntimeSpawner.EnsurePoolForEntry.
Requirements
A
RuntimeSpawnerinstance in the scene that raises:RuntimeSpawner.onRegionRegisteredRuntimeSpawner.onRegionUnregistered
LocalAreaSpawnermust expose:public (int min, int max) DesiredRangeOverride { get; set; }public float DensityMult { get; set; }
A
RegionSpawnLibraryScriptableObject to define:Rule-based spawn entries filtered by tags and step ranges.
A fallback list used when no rules match.
To supply semantic tags for regions, any component under a region root can implement:
RegionPopulationController.IRegionTagProviderIf not present, the GameObject’s Unity Tag is used (if not"Untagged").
There should typically be one RegionPopulationController per gameplay scene.
Responsibilities
For each tracked LocalAreaSpawner region, the controller:
Enables or disables the region based on a density multiplier.
Computes per-region target
(min, max)population and writes them toLocalAreaSpawner.DesiredRangeOverride(runtime-only, non-destructive).Builds
LocalAreaSpawner.CustomRegionSpawnersby consulting a globalRegionSpawnLibrary.Prewarms pools for each resolved
SpawnEntryviaRuntimeSpawner.EnsurePoolForEntry.
It can be driven automatically from SpawnDirector or from custom code.
Key Fields and Properties
References
RuntimeSpawner spawnerOwningRuntimeSpawnerused to:Register/unregister regions when scaled below/above thresholds.
Prewarm pools for resolved
SpawnEntryassets.
SpawnDirector director(optional) If assigned, the controller listens todirector.OnStepChangedand automatically callsApplyStepFromDirector.RegionSpawnLibrary libraryGlobal rule asset that:Contains region spawn rules keyed by tags and step ranges.
Provides a fallback list when no rules match.
public void SetLibrary(RegionSpawnLibrary lib)Allows changing the library at runtime.
Discovery
bool autoDiscoverAtStartWhen enabled, collects all existingLocalAreaSpawnerinstances at startup in addition to listening for runtime registration events. This is useful for scenes with pre-placed regions.
Scaling
float enableThreshold(0–1) Tiles whose effective density multiplier falls below this threshold are quietly unregistered from theRuntimeSpawner. Above this threshold, they are registered and given scaled min/max targets.AnimationCurve localDensityByStepMaps a normalized step index (0–1) to a local density multiplier. Defaults to0.8at step 0 and1.7at step 1.bool useJitterWhen enabled, applies a deterministic ±10% per-region jitter to the density multiplier, based on a hash of the region’s name.
Diagnostics
bool logChangesWhen enabled, logs enable/disable operations and spawn list rebuilds to the Console.
Unity Lifecycle
Awake()Finds
RuntimeSpawnerandSpawnDirectorautomatically if not assigned.Optionally discovers all
LocalAreaSpawnerinstances ifautoDiscoverAtStartis true.
OnEnable()Subscribes to:
RuntimeSpawner.onRegionRegisteredRuntimeSpawner.onRegionUnregistered
Subscribes to
director.OnStepChangedif a director is assigned.Immediately applies current step state via
ApplyStepFromDirector.
OnDisable()Unsubscribes from all events.
OnValidate()Clamps
enableThresholdto [0, 1].
Public API
Apply director steps
public void ApplyStepFromDirector(int step, int stepsCount, float localDensityOverride = -1f)Applies a new pacing step (from SpawnDirector or any custom driver) and recomputes all regions.
Parameters:
step0-based step index to apply.stepsCountTotal number of steps used to normalize into [0, 1]. The normalized value is used to evaluatelocalDensityByStep.localDensityOverrideOptional override for density multiplier:If
>= 0, this value is used directly.If
< 0,localDensityByStepis used instead.
Manual region registration
public void RegisterRegion(LocalAreaSpawner region)
public void UnregisterRegion(LocalAreaSpawner region)Allows explicit registration/unregistration outside of the automatic RuntimeSpawner events. These are safe to call redundantly.
Core Behaviour
Region tracking
The controller tracks regions in a private list:
private readonly List<LocalAreaSpawner> _regions = new();Regions are added via:
Initial discovery (
autoDiscoverAtStart), and/orRuntimeSpawner.onRegionRegisteredevent.
They are removed when:
Unregistered via event, or
Explicitly unregistered via
UnregisterRegion.
Handling region registration events
private void OnRegionRegistered(LocalAreaSpawner region)
{
if (!region) return;
RegisterRegion(region);
RecomputeRegion(region, localDensityOverride: -1f);
}When a new region is registered:
It is added to the internal list.
Its spawn list and density are immediately recomputed for the current step.
private void OnRegionUnregistered(LocalAreaSpawner region)
{
UnregisterRegion(region);
}Handling director step changes
private void OnDirectorStepChanged(int step)
{
int stepsCount = Mathf.Max(1, director.StepsCount);
ApplyStepFromDirector(step, stepsCount);
}When the director signals a step change, the controller recomputes all regions using the updated step index and total steps.
Recompute all regions
private void RecomputeAll(float localDensityOverride)
{
foreach (var region in _regions)
RecomputeRegion(region, localDensityOverride);
}Iterates all tracked regions and recomputes each one’s:
Spawn list (
CustomRegionSpawners)Enable/disable state
Target
(min, max)population viaDesiredRangeOverride
Recompute a single region
private void RecomputeRegion(LocalAreaSpawner region, float localDensityOverride)Steps:
Resolve spawn entries from the
RegionSpawnLibraryusing tile tags and step ranges:RefreshRegionEntries(region, _step, _stepsCount);
Compute effective density multiplier:
Normalize step to [0, 1].
Evaluate
localDensityByStep(or uselocalDensityOverride).Optionally apply ±10% deterministic jitter per region when
useJitteris enabled.
Enable/disable region and set targets:
If density multiplier
< enableThreshold:Unregister region from
RuntimeSpawner.Set
DensityMult = 0.Set
DesiredRangeOverride = (-1, -1)to indicate “ignore” in the region loop.
If density multiplier
>= enableThreshold:Ensure the region is registered with
RuntimeSpawner.Read
MinObjectCount/MaxObjectCountfrom the region.Scale them by the effective multiplier.
Clamp
tMax >= tMin.Set:
region.DensityMult = 1f;region.DesiredRangeOverride = (tMin, tMax);
This preserves authored values while applying runtime scaling through the override.
Building region spawn lists
private void RefreshRegionEntries(LocalAreaSpawner region, int step, int stepsCount)For a given region:
Determine region tags via
GetTags(region):If any
IRegionTagProvideris found on the region or its children, use the first non-empty tag set.Otherwise, use the GameObject’s Unity Tag if not
"Untagged".
Evaluate all rules in
RegionSpawnLibrary.rules:Skip rules outside of
[minStep, maxStep]for the currentstep.Skip rules whose tag filters (
requiredTags/anyTags) are not satisfied.Use
library.EvaluateWeight(r, step, stepsCount)to compute weight.Collect candidates
(SpawnEntry e, float w)whenw > 0.
Combine with authored entries from
region.CustomRegionSpawners:If no rule specifies
replaceInsteadOfAppend, existing region entries are included.Candidates are sorted by weight (descending), then by name.
Only unique entries are kept.
Fallback:
If the resulting list is empty and
library.fallbackEntriesis defined, those entries are added (deduplicated).
Apply and prewarm:
Assign the final list to
region.CustomRegionSpawners.For each entry, call
spawner.EnsurePoolForEntry(e).
Typical Usage Patterns
1. Basic setup with SpawnDirector
A typical setup wiring the controller to the director:
Add
RuntimeSpawnerto the scene and set up regions (LocalAreaSpawners).Add a
SpawnDirectorand configure its intensity/step profile.Add
RegionPopulationControlleronce in the scene.Assign:
spawnerto theRuntimeSpawnerinstance.directorto theSpawnDirector.libraryto aRegionSpawnLibraryasset.
Enable
autoDiscoverAtStartif regions are pre-placed in the scene.
At runtime:
As the director advances steps (e.g.,
OnStepChanged), the controller:Rebuilds region spawn lists according to their tags and current step.
Scales each region’s population targets.
Enables/disables low-density regions via the
enableThreshold.
2. Driving steps from a custom game mode
If no SpawnDirector is used:
using UnityEngine;
using MegaCrush.Spawner;
public class CustomStepDriver : MonoBehaviour
{
[SerializeField] private RegionPopulationController regionController;
[SerializeField] private int stepsCount = 4;
private int currentStep;
public void NextStep()
{
currentStep = Mathf.Min(currentStep + 1, stepsCount - 1);
regionController.ApplyStepFromDirector(currentStep, stepsCount);
}
public void SetStep(int step)
{
currentStep = Mathf.Clamp(step, 0, stepsCount - 1);
regionController.ApplyStepFromDirector(currentStep, stepsCount);
}
}Attach this to a game mode object and call NextStep or SetStep in response to mission progress, time elapsed, or player actions.
3. Tagging regions via IRegionTagProvider
To provide semantic tags for tiles without relying on Unity tags:
using UnityEngine;
using MegaCrush.Spawner;
public class TileTagProvider : MonoBehaviour, RegionPopulationController.IRegionTagProvider
{
[SerializeField] private string[] tags;
public string[] GetRegionTags() => tags;
}Attach
TileTagProviderto the same GameObject asLocalAreaSpawneror to a child.Enter tags such as
"industrial","residential","downtown","rooftop".Rules in
RegionSpawnLibrarycan then target these tags to control spawns.
4. Changing the RegionSpawnLibrary at runtime
To swap spawn rules based on campaign progression or difficulty:
using UnityEngine;
using MegaCrush.Spawner;
public class LibrarySwitcher : MonoBehaviour
{
[SerializeField] private RegionPopulationController regionController;
[SerializeField] private RegionSpawnLibrary earlyGameLibrary;
[SerializeField] private RegionSpawnLibrary lateGameLibrary;
public void SwitchToEarlyGame()
{
regionController.SetLibrary(earlyGameLibrary);
regionController.ApplyStepFromDirector(0, 1);
}
public void SwitchToLateGame()
{
regionController.SetLibrary(lateGameLibrary);
regionController.ApplyStepFromDirector(0, 1);
}
}This supports different spawn rule sets for early-game vs late-game, or per-biome.
5. Debugging region scaling
With logChanges enabled, the controller logs:
When a region is disabled due to low effective density.
The number of spawn entries assigned to each region after a rebuild.
This can be used together with custom editor tooling to visualize region density and spawn composition over time.
Last updated