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

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 LocalAreaSpawner regions.

  • 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 RuntimeSpawner instance in the scene that raises:

    • RuntimeSpawner.onRegionRegistered

    • RuntimeSpawner.onRegionUnregistered

  • LocalAreaSpawner must expose:

    • public (int min, int max) DesiredRangeOverride { get; set; }

    • public float DensityMult { get; set; }

  • A RegionSpawnLibrary ScriptableObject 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.IRegionTagProvider If 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:

  1. Enables or disables the region based on a density multiplier.

  2. Computes per-region target (min, max) population and writes them to LocalAreaSpawner.DesiredRangeOverride (runtime-only, non-destructive).

  3. Builds LocalAreaSpawner.CustomRegionSpawners by consulting a global RegionSpawnLibrary.

  4. Prewarms pools for each resolved SpawnEntry via RuntimeSpawner.EnsurePoolForEntry.

It can be driven automatically from SpawnDirector or from custom code.


Key Fields and Properties

References

  • RuntimeSpawner spawner Owning RuntimeSpawner used to:

    • Register/unregister regions when scaled below/above thresholds.

    • Prewarm pools for resolved SpawnEntry assets.

  • SpawnDirector director (optional) If assigned, the controller listens to director.OnStepChanged and automatically calls ApplyStepFromDirector.

  • RegionSpawnLibrary library Global 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 autoDiscoverAtStart When enabled, collects all existing LocalAreaSpawner instances 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 the RuntimeSpawner. Above this threshold, they are registered and given scaled min/max targets.

  • AnimationCurve localDensityByStep Maps a normalized step index (0–1) to a local density multiplier. Defaults to 0.8 at step 0 and 1.7 at step 1.

  • bool useJitter When enabled, applies a deterministic ±10% per-region jitter to the density multiplier, based on a hash of the region’s name.

Diagnostics

  • bool logChanges When enabled, logs enable/disable operations and spawn list rebuilds to the Console.


Unity Lifecycle

  • Awake()

    • Finds RuntimeSpawner and SpawnDirector automatically if not assigned.

    • Optionally discovers all LocalAreaSpawner instances if autoDiscoverAtStart is true.

  • OnEnable()

    • Subscribes to:

      • RuntimeSpawner.onRegionRegistered

      • RuntimeSpawner.onRegionUnregistered

    • Subscribes to director.OnStepChanged if a director is assigned.

    • Immediately applies current step state via ApplyStepFromDirector.

  • OnDisable()

    • Unsubscribes from all events.

  • OnValidate()

    • Clamps enableThreshold to [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:

  • step 0-based step index to apply.

  • stepsCount Total number of steps used to normalize into [0, 1]. The normalized value is used to evaluate localDensityByStep.

  • localDensityOverride Optional override for density multiplier:

    • If >= 0, this value is used directly.

    • If < 0, localDensityByStep is 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/or

  • RuntimeSpawner.onRegionRegistered event.

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 via DesiredRangeOverride

Recompute a single region

private void RecomputeRegion(LocalAreaSpawner region, float localDensityOverride)

Steps:

  1. Resolve spawn entries from the RegionSpawnLibrary using tile tags and step ranges:

    • RefreshRegionEntries(region, _step, _stepsCount);

  2. Compute effective density multiplier:

    • Normalize step to [0, 1].

    • Evaluate localDensityByStep (or use localDensityOverride).

    • Optionally apply ±10% deterministic jitter per region when useJitter is enabled.

  3. 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 / MaxObjectCount from 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:

  1. Determine region tags via GetTags(region):

    • If any IRegionTagProvider is found on the region or its children, use the first non-empty tag set.

    • Otherwise, use the GameObject’s Unity Tag if not "Untagged".

  2. Evaluate all rules in RegionSpawnLibrary.rules:

    • Skip rules outside of [minStep, maxStep] for the current step.

    • 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) when w > 0.

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

  4. Fallback:

    • If the resulting list is empty and library.fallbackEntries is defined, those entries are added (deduplicated).

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

  1. Add RuntimeSpawner to the scene and set up regions (LocalAreaSpawners).

  2. Add a SpawnDirector and configure its intensity/step profile.

  3. Add RegionPopulationController once in the scene.

  4. Assign:

    • spawner to the RuntimeSpawner instance.

    • director to the SpawnDirector.

    • library to a RegionSpawnLibrary asset.

  5. Enable autoDiscoverAtStart if 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 TileTagProvider to the same GameObject as LocalAreaSpawner or to a child.

  • Enter tags such as "industrial", "residential", "downtown", "rooftop".

  • Rules in RegionSpawnLibrary can 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