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., from SpawnDirector) 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 pools

Key concepts

  • Base vs Override: Each LocalAreaSpawner still 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 from RuntimeSpawner. When density rises again, it re-registers.

  • Tags: The controller asks the region for semantic tags to match rules:

    • Implement RegionPopulationController.IRegionTagProvider on 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, call ApplyStepFromDirector(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 any LocalAreaSpawner already 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)

  1. Add a RegionPopulationController to your gameplay scene.

  2. Assign your scene’s RuntimeSpawner.

  3. (Optional) Assign your SpawnDirector (or call ApplyStepFromDirector yourself).

  4. Create a RegionSpawnLibrary asset and assign it.

  5. Author your tiles with LocalAreaSpawner (BoxCollider isTrigger). Set base Min/Max.

  6. (Optional) Tag tiles via Unity Tag or an IRegionTagProvider component.

  7. 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; pass localDensityOverride ≥ 0 to force a density multiplier for this pass.

  • RegisterRegion(LocalAreaSpawner) / UnregisterRegion(LocalAreaSpawner) Usually handled via RuntimeSpawner events; 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 RuntimeSpawner and on SpawnEntry.maxPopulation still 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_Grunt

    • Required Tags: industrial

    • Any Tags: street

    • Min Step: 2, Max Step: 4

    • Weight 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_EliteGuard

    • Required Tags: fortress

    • Min Step: 3, Max Step: 4

    • Replace 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 IRegionTagProvider when tiles need multiple tags.

  • Keep tag vocabulary short; rules are easier to reason about.


  1. Place LocalAreaSpawner volumes per tile. Set Min/Max for that tile’s “typical” density.

  2. Add RegionPopulationController to the scene and assign a RegionSpawnLibrary.

  3. Model your pacing in SpawnDirector (steps), then set the controller’s density curve:

    • Low steps below enableThreshold to quiet down.

    • Mid steps around 1.0×.

    • High steps up to 1.5–1.8× (watch your global caps).

  4. 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.

  5. 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.

  • RuntimeSpawner must raise region registration events. If you’re on an older version, update the package or bind regions manually with RegisterRegion/UnregisterRegion.


One-minute checklist

Last updated