Special Encounter Manager

Overview

The Special Encounter Manager controls when and how “special” enemies or encounters appear in your game. It evaluates a Special Profile on an interval, checks all rule conditions, and spawns encounters through the active RuntimeSpawner.

Special encounters are designed to:

  • Layer rare, high-impact enemies on top of the baseline population.

  • React dynamically to pacing (steps, pressure, player HP).

  • Use flexible spawn anchors or fallback radial placement.

Inspector Properties

Special Profile

  • A SpecialProfile ScriptableObject containing all rules and global constraints.

  • Inline editing supported directly in the Manager inspector.

Create / Open Profile Buttons

  • Create a new profile asset in your project.

  • Ping/open an existing profile asset for editing.

Telemetry Provider

  • Optional field. Accepts a SpecialsTelemetryProvider ScriptableObject.

  • Supplies dynamic values like Pressure and Average Player HP to rule conditions.

  • If not set, rules that require telemetry will safely fail their conditions (no spawn).

Placement / Line of Sight (Occluder Mask)

  • Defines which layers block line-of-sight when a rule requires Require No LOS.

  • Anchors behind occluders become eligible for spawning.

Runtime Status (Play Mode)

  • Shows live scheduling data:

    • Next Spawn At (absolute Time.time)

    • Time Until Next Spawn (seconds)

    • Last Spawn At

    • Next Planned Tag (best-effort debug hint)

  • Includes an Editor-only Force Roll Now button to immediately test rule evaluation.


Profile Fields

Each SpecialProfile defines global caps and a set of rules:

  • Max Simultaneous Specials – Global cap of active specials.

  • Min Gap Seconds – Global cooldown between any special spawns.

  • Rules – List of SpecialRule objects.

SpecialRule Properties

  • Name – Identifier (used in metadata and debugging).

  • Spawn Entry – Prefab/agent to spawn.

  • Max Alive – Max of this type simultaneously.

  • Cooldown Seconds – Per-rule cooldown after a spawn.

  • Eval Every Seconds – Interval between condition checks.

  • Step Range – Intensity steps where this rule is valid.

  • Distance Range – Min/max distance from player to spawn.

  • Require No LOS – Only eligible if an occluder exists between player and anchor.

  • Spawn Tag – Optional filter for matching specific anchors.

  • Min Pressure – Requires telemetry; minimum Pressure threshold.

  • Min Avg Player HP – Requires telemetry; minimum average HP threshold.



Usage Patterns

  • Controlled Surprises – e.g. flankers spawn only if players are healthy.

  • Boss Gates – e.g. minibosses appear only at later steps and high pressure.

  • Replayable Variants – Swap profiles to change encounter mix per level/difficulty.

  • Dynamic Difficulty – Telemetry conditions let specials react to player stress.


Debugging & Validation

  • Runtime Inspector shows live scheduling, timers, and next tag hints.

  • Force Roll Now (Editor only) to immediately test rule evaluation.

  • Events available:

    • OnScheduleChanged – fired when next spawn time changes.

    • OnSpecialSpawned(string tag) – fired when a special spawns.

  • Best practice: Hook these events into your HUD/logs while tuning encounters.


Example

// Setup in code
var manager = FindObjectOfType<SpecialEncounterManager>();
manager.Profile = mySpecialProfile;
manager.SetTelemetryProvider(new MyGameTelemetry());
manager.SetRunning(true);

// Listen for spawns
manager.OnSpecialSpawned += tag => Debug.Log($"Special triggered: {tag}");

Also see:

Special Profile

Fundamental

SpecialEncounterManager manages opportunistic “special” encounters based on a SpecialProfile. It periodically evaluates rule conditions and, when a rule passes, spawns a special encounter via the bound RuntimeSpawner.

Specials can:

  • Spawn at tagged WaveSpawnPoint anchors (e.g. ambush points).

  • Fall back to a radial placement resolved by the spawner’s ISpawnLocator.

The manager runs as a scheduler: it tracks when each rule should be evaluated, enforces global and per-rule limits, and exposes timing info for HUD/diagnostics.


Responsibilities

  • Bind to a RuntimeSpawner and its collaborators:

    • ISpawnExecutor

    • ISpawnLocator

    • PopulationTracker

  • Drive a rule-based scheduler using a SpecialProfile:

    • Per-rule evaluation intervals

    • Per-rule alive caps

    • Step gating (via SpawnDirector)

    • Telemetry conditions (pressure/HP)

  • Select spawn locations:

    • Prefer matching WaveSpawnPoint anchors.

    • Fall back to global placement around the player.

  • Track and expose:

    • Next scheduled evaluation time (NextSpawnAt)

    • Last special spawn time (LastSpawnAt)

    • Optional hint for the next planned tag (NextPlannedTag)


Requirements

  • A RuntimeSpawner in the scene.

  • A SpecialProfile asset with one or more SpecialRule entries.

  • Optionally:

    • A SpawnDirector on the same GameObject as RuntimeSpawner, to gate rules by intensity step.

    • A SpecialsTelemetryProvider (or custom ISpecialsTelemetry) to drive pressure/HP conditions.

    • WaveSpawnPoint anchors in the scene for directed spawning.

RuntimeSpawner.Init() is expected to call SpecialEncounterManager.Init(spawner) once the spawner exists.


Inspector Fields

Profile

[SerializeField] private SpecialProfile profile;
public SpecialProfile Profile { get; set; }
  • profile defines:

    • Rules (SpecialRule list)

    • Global minimum gap between specials (minGapSeconds)

    • Other rule properties (distance ranges, cooldowns, etc.).

Placement / LOS

[Header("Placement / LOS")]
[SerializeField] private LayerMask occluderMask = ~0;
  • occluderMask Layers treated as occluders for “require no line-of-sight” rules. Used when a rule has requireNoLOS = true.

Telemetry

[Header("Telemetry")]
[SerializeField] private SpecialsTelemetryProvider telemetryProvider;
  • telemetryProvider Optional ScriptableObject implementing ISpecialsTelemetry. Provides:

    • Pressure values

    • Average player HP used in rule conditions.

Alternatively, a code-based provider can be injected via SetTelemetryProvider.


Public State & Events

Schedule and status

public float  NextSpawnAt    { get; private set; } = -1f;
public float  LastSpawnAt    { get; private set; } = -1f;
public string NextPlannedTag { get; private set; }
public bool   IsRunning      => _running;
  • NextSpawnAt Time.time when the next eligible evaluation is planned across all rules; -1 if nothing is scheduled.

  • LastSpawnAt Time.time when the last special successfully spawned; -1 if none have spawned yet.

  • NextPlannedTag Debug-only hint for the next chosen spawnTag (best effort).

  • IsRunning true while the evaluation loop is active.

Events

public event Action      OnScheduleChanged;
public event Action<string> OnSpecialSpawned;
  • OnScheduleChanged Raised when the next evaluation/roll time changes (e.g. HUD countdowns, inspectors).

  • OnSpecialSpawned (string spawnTag) Raised when a special encounter actually spawns. Payload: the rule’s spawnTag, useful for HUD logs or analytics.


Telemetry Injection

public void SetTelemetryProvider(ISpecialsTelemetry provider)
  • Allows injecting a code-based telemetry provider.

  • If not set, telemetryProvider (ScriptableObject) is used.

  • If neither is present and a rule requires telemetry (pressure/HP), the condition fails by default.


Runtime Control API

Start / Stop scheduler

public void SetRunning(bool run)
  • true:

    • Locates a RuntimeSpawner (if not yet bound) and calls Init(spawner) once.

    • Calls Begin() to start evaluation.

  • false:

    • Calls Stop() to cancel the loop and unhook population events.

    • Clears schedule hints:

      • NextSpawnAt = -1

      • NextPlannedTag = null

    • Invokes OnScheduleChanged().

Begin / Pause / Resume / Stop / End

public void Begin()
public void Pause()
public void Resume()
public void Stop()
public void End()
  • Begin() Idempotent. When first called:

    • Verifies RuntimeSpawner, profile, Executor, and Locator are available.

    • Clears per-rule timers and name map.

    • Initializes:

      • _nextEvalAt[rule] = 0 (evaluate immediately on start).

      • _ruleByName for rule lookups by name.

    • Resets timing:

      • _lastSpecialTime = -1

      • LastSpawnAt = -1

      • NextPlannedTag = null

    • Hooks PopulationTracker events:

      • _pop.OnSpawnedWithMeta += OnSpawnedWithMeta

      • _pop.OnDespawned += OnDespawned

    • Starts the evaluation coroutine EvalLoop() and sets _running = true.

    • Recomputes NextSpawnAt and raises OnScheduleChanged.

  • Pause()

    • Stops the evaluation coroutine.

    • Leaves:

      • Rule timers (_nextEvalAt)

      • Alive counts (_alivePerRule)

      • Population hooks intact so Resume() can continue seamlessly.

  • Resume()

    • Restarts EvalLoop() if:

      • Scheduler is not already running.

      • RuntimeSpawner and profile are valid.

      • Executor and locator have been initialized.

    • Recomputes NextSpawnAt and notifies via OnScheduleChanged.

  • Stop()

    • Stops the evaluation loop.

    • Unhooks population events from PopulationTracker.

    • Leaves timers and counts intact.

    • Use Begin() again to fully reinitialize rule state.

  • End()

    • Performs a hard shutdown:

      • Calls Stop().

      • Clears NextSpawnAt and NextPlannedTag.

      • Raises OnScheduleChanged.

    • Called by RuntimeSpawner.StopSpawners() during teardown.


Immediate Requests API

These methods “nudge” rules so they become eligible immediately, without waiting for their scheduled interval.

Request by rule name

public bool RequestImmediateForRule(string ruleName)
  • Ensures RuntimeSpawner is bound and the scheduler is running (calls Init + Begin if needed).

  • If a rule with ruleName exists:

    • Sets _nextEvalAt[rule] = Time.time so it can be evaluated next frame.

    • Recomputes NextSpawnAt and raises OnScheduleChanged.

    • Returns true.

  • Returns false if the profile is missing or no matching rule is found.

Request by spawn tag

public bool RequestImmediateForTag(string spawnTag)
  • Ensures RuntimeSpawner is bound and scheduler is running.

  • For all rules whose spawnTag matches (case-insensitive):

    • Sets _nextEvalAt[rule] = Time.time.

  • If any rule was nudged:

    • Recomputes NextSpawnAt.

    • Raises OnScheduleChanged.

    • Returns true.

  • Returns false if no matches were found.

Request roll for all rules

public void RequestImmediateRoll()
  • Ensures RuntimeSpawner is bound and scheduler is running.

  • Sets _nextEvalAt[rule] = Time.time for all rules in the profile.

  • Recomputes NextSpawnAt and raises OnScheduleChanged.


Wiring to RuntimeSpawner

public void Init(RuntimeSpawner spawner)
  • Binds:

    • _spawner

    • _director (via spawner.GetComponent<SpawnDirector>(), if present)

    • _pop, _exec, _locator, _player from the spawner’s services:

      • PopulationTracker

      • ISpawnExecutor

      • ISpawnLocator

      • PlayerTransform

  • Subscribes to _director.OnStepChanged, if a director exists.

  • Sets _telemetry to:

    • Existing code-injected provider, or

    • telemetryProvider ScriptableObject, if no injected provider is set.

OnDisable() unsubscribes from OnStepChanged and calls Pause().


Intensity Step Gating

private void HandleStepChanged(int _)
  • When the intensity step changes (via SpawnDirector):

    • Forces all rules to become eligible sooner (their _nextEvalAt is set to now or earlier).

    • Recomputes NextSpawnAt.

    • Raises OnScheduleChanged.

private bool StepAllowed(SpecialRule r)
  • If no director is bound, returns true (no gating).

  • Otherwise checks if SpawnDirector.CurrentStep lies within r.stepRange.x..r.stepRange.y.

StepAllowed is used both during evaluation and when computing NextSpawnAt, so HUDs only reflect rules that are allowed at the current intensity.


Population Tracking

The manager tracks how many live instances each rule has produced using SpawnMeta supplied by the spawner’s PopulationTracker.

On spawn

private void OnSpawnedWithMeta(GameObject go, SpawnMeta meta)
  • Only considered if:

    • meta.Source == SpawnSource.Special

    • meta.SourceName is not empty

  • If meta.SourceName matches a known rule name:

    • Increments _alivePerRule[rule].

On despawn

private void OnDespawned(GameObject go, SpawnMeta meta)
  • Same source checks as above.

  • If meta.SourceName matches a known rule:

    • Decrements _alivePerRule[rule], clamped to non-negative.

The EvalLoop enforces:

if (alive >= r.maxAlive) { ... skip & reschedule ... }

per rule.


Evaluation Loop

private IEnumerator EvalLoop()

Runs once per frame while _running is true and profile is valid.

For each frame:

  1. Capture now = Time.time and playerPos from _player (or Vector3.zero).

  2. For each SpecialRule r in profile.rules:

    • Skip if rule or r.entry is null.

    • Skip if now < _nextEvalAt[r] (not time to evaluate).

    • Enforce:

      • Step gating via StepAllowed(r).

      • Global minimum gap since last special (profile.minGapSeconds).

      • Per-rule alive cap (r.maxAlive).

      • Telemetry conditions via ConditionsPass(r):

        • minPressure

        • minAvgPlayerHP

    • If any check fails, bump _nextEvalAt[r] by r.evalEverySeconds using Bump.

  3. If rule is eligible:

    • Try to find an anchor via TryPickAnchor(r, playerPos, out anchor):

      • If an anchor is found:

        • Build SpawnContext with:

          • WavePoint = anchor.transform

          • WaveRange = anchor.GetSpawnpointRange()

          • Source = SpawnSource.Special

          • SourceName = r.name

          • SpawnTag = r.spawnTag

        • Attempt to spawn via _exec.Spawn(r.entry, ctx).

      • If no anchor:

        • Build a fallback global context:

          • IsGlobal = true

          • PlayerPos = playerPos

          • Source = SpawnSource.Special

          • SourceName = r.name

          • SpawnTag = r.spawnTag

        • Spawn via _exec.Spawn(r.entry, ctxFallback).

    • On successful spawn:

      • Set:

        • _lastSpecialTime = now

        • LastSpawnAt = now

        • NextPlannedTag = r.spawnTag

      • Invoke OnSpecialSpawned(r.spawnTag).

      • Set per-rule cooldown via SetCooldown(r, now, r.cooldownSeconds).

    • On failed spawn:

      • Bump eval time via Bump(r, now, r.evalEverySeconds).

  4. If any schedule times were changed during this frame:

    • Call RecomputeNextSpawnAt().

    • Raise OnScheduleChanged().


Anchor Selection

The manager prefers WaveSpawnPoint anchors when they are available and valid for a rule.

Anchor cache

private static WaveSpawnPoint[] _cachedAnchors;
public static void RefreshAnchors() => _cachedAnchors = null;
private WaveSpawnPoint[] GetAnchors() => _cachedAnchors ??= FindObjectsByType<WaveSpawnPoint>(FindObjectsSortMode.None);
  • Anchor list is cached for efficiency.

  • RefreshAnchors() clears the cache; the next call to GetAnchors() rescans the scene.

Picking an anchor

private bool TryPickAnchor(SpecialRule r, Vector3 playerPos, out WaveSpawnPoint anchor)

Logic:

  1. Get all WaveSpawnPoint instances.

  2. Filter candidates by:

    • Tag match (AnchorMatchesTag(anchor, r.spawnTag)):

      • Checks WaveSpawnPoint.AnchorTags and the GameObject’s tag.

    • Distance range:

      • r.distanceRange.x <= distance <= r.distanceRange.y

    • LOS requirement:

      • If r.requireNoLOS is true:

        • Requires that a Physics.Linecast from player to anchor hits something in occluderMask.

  3. If at least one candidate remains:

    • Choose a random one via Random.Range.

    • Return true with the selected anchor.

  4. Otherwise:

    • Return false (caller uses radial fallback).


Typical Usage Patterns

1. Basic setup with automatic scheduling

  1. Add RuntimeSpawner and configure normal/global/wave spawning.

  2. Add SpecialEncounterManager to the same scene.

  3. Assign:

    • profile to a SpecialProfile with one or more SpecialRules.

    • telemetryProvider (optional).

  4. Ensure:

    • RuntimeSpawner.Init() calls SpecialEncounterManager.Init(spawner).

    • RuntimeSpawner.StartSpawners() calls SpecialEncounterManager.Begin() (this is already done in the provided spawner code).

At runtime:

  • The manager evaluates rules every frame at their scheduled times.

  • Specials spawn as conditions are met, respecting gaps and caps.

  • OnSpecialSpawned fires for each encounter.


2. Triggering specials from game events

Use the “immediate request” API to align specials with scripted events:

using UnityEngine;
using MegaCrush.Spawner;

public class BossRoomEvents : MonoBehaviour
{
    [SerializeField] private SpecialEncounterManager specials;

    public void OnBossPhaseStart()
    {
        // Make all specials eligible immediately
        specials.RequestImmediateRoll();
    }

    public void OnBossShieldDown()
    {
        // Nudge a specific rule by name
        specials.RequestImmediateForRule("OrbitalStrike");
    }

    public void OnPlayerEnterArena()
    {
        // Nudge by tag instead of rule name
        specials.RequestImmediateForTag("Ambush");
    }
}

The scheduler still applies all normal checks (gap, caps, telemetry), but evaluation happens sooner.


3. Pausing specials during safe zones or menus

using UnityEngine;
using MegaCrush.Spawner;

public class SpecialsPauseHandler : MonoBehaviour
{
    [SerializeField] private SpecialEncounterManager specials;

    public void OnEnterSafeZone()
    {
        specials.Pause();
    }

    public void OnExitSafeZone()
    {
        specials.Resume();
    }
}

Special evaluation is paused while in a safe zone, then resumes seamlessly on exit.


4. HUD integration for “next special”

The scheduler exposes timing and tags that can be used for a telegraphed HUD:

using UnityEngine;
using MegaCrush.Spawner;
using UnityEngine.UI;

public class SpecialsHUD : MonoBehaviour
{
    [SerializeField] private SpecialEncounterManager specials;
    [SerializeField] private Text nextLabel;
    [SerializeField] private Text timerLabel;

    private void OnEnable()
    {
        if (specials != null)
            specials.OnScheduleChanged += Refresh;
    }

    private void OnDisable()
    {
        if (specials != null)
            specials.OnScheduleChanged -= Refresh;
    }

    private void Update()
    {
        RefreshTimer();
    }

    private void Refresh()
    {
        nextLabel.text = string.IsNullOrEmpty(specials.NextPlannedTag)
            ? "No special planned"
            : $"Next special: {specials.NextPlannedTag}";
        RefreshTimer();
    }

    private void RefreshTimer()
    {
        if (specials.NextSpawnAt < 0f)
        {
            timerLabel.text = "--";
            return;
        }

        float remaining = Mathf.Max(0f, specials.NextSpawnAt - Time.time);
        timerLabel.text = $"{remaining:0.0}s";
    }
}

This allows players (or debug builds) to see when the next special is likely to be evaluated.


5. Updating anchors dynamically

If anchors are added/removed at runtime (e.g. streamed tiles, phase changes):

using UnityEngine;
using MegaCrush.Spawner;

public class AnchorRefreshOnPhase : MonoBehaviour
{
    public void OnPhaseChanged()
    {
        SpecialEncounterManager.RefreshAnchors();
    }
}

The next special evaluation will rescan WaveSpawnPoints and use the updated set.

Last updated