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
A
SpecialProfileScriptableObject 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.
Optional field. Accepts a
SpecialsTelemetryProviderScriptableObject.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
SpecialRuleobjects.
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:
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
WaveSpawnPointanchors (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
RuntimeSpawnerand its collaborators:ISpawnExecutorISpawnLocatorPopulationTracker
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
WaveSpawnPointanchors.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
RuntimeSpawnerin the scene.A
SpecialProfileasset with one or moreSpecialRuleentries.Optionally:
A
SpawnDirectoron the same GameObject asRuntimeSpawner, to gate rules by intensity step.A
SpecialsTelemetryProvider(or customISpecialsTelemetry) to drive pressure/HP conditions.WaveSpawnPointanchors 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; }profiledefines:Rules (
SpecialRulelist)Global minimum gap between specials (
minGapSeconds)Other rule properties (distance ranges, cooldowns, etc.).
Placement / LOS
[Header("Placement / LOS")]
[SerializeField] private LayerMask occluderMask = ~0;occluderMaskLayers treated as occluders for “require no line-of-sight” rules. Used when a rule hasrequireNoLOS = true.
Telemetry
[Header("Telemetry")]
[SerializeField] private SpecialsTelemetryProvider telemetryProvider;telemetryProviderOptional ScriptableObject implementingISpecialsTelemetry. 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;NextSpawnAtTime.timewhen the next eligible evaluation is planned across all rules;-1if nothing is scheduled.LastSpawnAtTime.timewhen the last special successfully spawned;-1if none have spawned yet.NextPlannedTagDebug-only hint for the next chosenspawnTag(best effort).IsRunningtruewhile the evaluation loop is active.
Events
public event Action OnScheduleChanged;
public event Action<string> OnSpecialSpawned;OnScheduleChangedRaised 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’sspawnTag, 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 callsInit(spawner)once.Calls
Begin()to start evaluation.
false:Calls
Stop()to cancel the loop and unhook population events.Clears schedule hints:
NextSpawnAt = -1NextPlannedTag = 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, andLocatorare available.Clears per-rule timers and name map.
Initializes:
_nextEvalAt[rule] = 0(evaluate immediately on start)._ruleByNamefor rule lookups by name.
Resets timing:
_lastSpecialTime = -1LastSpawnAt = -1NextPlannedTag = null
Hooks
PopulationTrackerevents:_pop.OnSpawnedWithMeta += OnSpawnedWithMeta_pop.OnDespawned += OnDespawned
Starts the evaluation coroutine
EvalLoop()and sets_running = true.Recomputes
NextSpawnAtand raisesOnScheduleChanged.
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.
RuntimeSpawnerandprofileare valid.Executor and locator have been initialized.
Recomputes
NextSpawnAtand notifies viaOnScheduleChanged.
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
NextSpawnAtandNextPlannedTag.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
RuntimeSpawneris bound and the scheduler is running (callsInit+Beginif needed).If a rule with
ruleNameexists:Sets
_nextEvalAt[rule] = Time.timeso it can be evaluated next frame.Recomputes
NextSpawnAtand raisesOnScheduleChanged.Returns
true.
Returns
falseif the profile is missing or no matching rule is found.
Request by spawn tag
public bool RequestImmediateForTag(string spawnTag)Ensures
RuntimeSpawneris bound and scheduler is running.For all rules whose
spawnTagmatches (case-insensitive):Sets
_nextEvalAt[rule] = Time.time.
If any rule was nudged:
Recomputes
NextSpawnAt.Raises
OnScheduleChanged.Returns
true.
Returns
falseif no matches were found.
Request roll for all rules
public void RequestImmediateRoll()Ensures
RuntimeSpawneris bound and scheduler is running.Sets
_nextEvalAt[rule] = Time.timefor all rules in the profile.Recomputes
NextSpawnAtand raisesOnScheduleChanged.
Wiring to RuntimeSpawner
public void Init(RuntimeSpawner spawner)Binds:
_spawner_director(viaspawner.GetComponent<SpawnDirector>(), if present)_pop,_exec,_locator,_playerfrom the spawner’s services:PopulationTrackerISpawnExecutorISpawnLocatorPlayerTransform
Subscribes to
_director.OnStepChanged, if a director exists.Sets
_telemetryto:Existing code-injected provider, or
telemetryProviderScriptableObject, 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
_nextEvalAtis 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.CurrentSteplies withinr.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.Specialmeta.SourceNameis not empty
If
meta.SourceNamematches a known rule name:Increments
_alivePerRule[rule].
On despawn
private void OnDespawned(GameObject go, SpawnMeta meta)Same source checks as above.
If
meta.SourceNamematches 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:
Capture
now = Time.timeandplayerPosfrom_player(orVector3.zero).For each
SpecialRule rinprofile.rules:Skip if rule or
r.entryis 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):minPressureminAvgPlayerHP
If any check fails, bump
_nextEvalAt[r]byr.evalEverySecondsusingBump.
If rule is eligible:
Try to find an anchor via
TryPickAnchor(r, playerPos, out anchor):If an anchor is found:
Build
SpawnContextwith:WavePoint = anchor.transformWaveRange = anchor.GetSpawnpointRange()Source = SpawnSource.SpecialSourceName = r.nameSpawnTag = r.spawnTag
Attempt to spawn via
_exec.Spawn(r.entry, ctx).
If no anchor:
Build a fallback global context:
IsGlobal = truePlayerPos = playerPosSource = SpawnSource.SpecialSourceName = r.nameSpawnTag = r.spawnTag
Spawn via
_exec.Spawn(r.entry, ctxFallback).
On successful spawn:
Set:
_lastSpecialTime = nowLastSpawnAt = nowNextPlannedTag = 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).
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 toGetAnchors()rescans the scene.
Picking an anchor
private bool TryPickAnchor(SpecialRule r, Vector3 playerPos, out WaveSpawnPoint anchor)Logic:
Get all
WaveSpawnPointinstances.Filter candidates by:
Tag match (
AnchorMatchesTag(anchor, r.spawnTag)):Checks
WaveSpawnPoint.AnchorTagsand the GameObject’s tag.
Distance range:
r.distanceRange.x <= distance <= r.distanceRange.y
LOS requirement:
If
r.requireNoLOSis true:Requires that a
Physics.Linecastfrom player to anchor hits something inoccluderMask.
If at least one candidate remains:
Choose a random one via
Random.Range.Return
truewith the selectedanchor.
Otherwise:
Return
false(caller uses radial fallback).
Typical Usage Patterns
1. Basic setup with automatic scheduling
Add
RuntimeSpawnerand configure normal/global/wave spawning.Add
SpecialEncounterManagerto the same scene.Assign:
profileto aSpecialProfilewith one or moreSpecialRules.telemetryProvider(optional).
Ensure:
RuntimeSpawner.Init()callsSpecialEncounterManager.Init(spawner).RuntimeSpawner.StartSpawners()callsSpecialEncounterManager.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.
OnSpecialSpawnedfires 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