Documentation

Crowd Runner Framework

A mobile-first, zero-GC framework for math runner and crowd battle games in Unity: math gates, crowd battles, obstacles, finish stairs, coins and upgrades, with hundreds of units moved by Burst jobs and levels described as data.

  • Version 1.0.0
  • Unity 6000.6+
  • Platforms Android, iOS, WebGL, Desktop
  • Updated Oct 30, 2026
Crowd Runner Framework

Overview

Crowd Runner Framework gives you the whole "math runner and crowd battle" genre as clean, reusable systems. A swarm grows and shrinks through math gates, enemy armies fight it unit for unit, saws, sweeping bars and spike traps cut it down, and the multiplier staircase at the finish turns what is left into a coin reward. Levels are data assets, so a new level is a list to fill in, not a scene to build.

The framework is a hybrid of GameObjects and Burst: units are ordinary pooled GameObjects, while their layout, movement, battle contacts and hazard hits run in Burst-compiled C# jobs. Nothing is instantiated during play and the gameplay loop allocates no garbage. Every system raises C# events for code and UnityEvents for designers.

Requirements

ItemVersionNotes
Unity6000.6+
Universal Render Pipeline17.6+The unit shader and materials are URP and SRP Batcher compatible
com.unity.burst2.0+Installed as a dependency
com.unity.collections6.6+Installed as a dependency
com.unity.mathematics1.4+Installed as a dependency
com.unity.inputsystem1.20+Swipe and keyboard steering
com.unity.ugui2.6+TextMeshPro, for gate labels, step labels and the demo UI

WebGL works. Burst is not available there, so the jobs run as plain C#, which is still quick enough for a few hundred units.

Quick start

  1. Import the package Unity installs the package dependencies. If TextMeshPro asks to import its Essentials, accept.
  2. Play the demo Open Scenes/Demo/CrowdRunnerDemo.unity and press Play. Press Play on the start screen, or Watch autopilot to let it play itself.
  3. Build your own scene From GameObject › Crowd Runner, add a Player Swarm, a Combat Controller, a Hazard Controller and a Level Loader. Each one comes wired to the default prefabs and settings.
  4. Make a level Create › Crowd Runner › Level, fill in its lists, assign it to the Level Loader and press Play. See Levels.
Tip

Add a Pool Manager (Add Component › Crowd Runner › Pool Manager) with a Pool Config to your scene, so pools prewarm while the scene loads. Without one, a manager is created on first use and logs a warning.

Demo controls

ControlAction
Play, Space or EnterStart the run from the start screen
Drag / swipe, or A / DSteer
T or AutoAutopilot: dodges obstacles, picks up coins, steers into the gate giving the most units
P / Space or PauseStop and resume running (not during battles or on the stairs)
+ / - or +10 / -10Add or remove units through the pool, to try the 500-unit cap and elimination
R or RestartRestart the level straight away, without reloading the scene

What is in the package

Assets/CrowdRunnerFramework/
├── Runtime/             Core, Pooling, Gates, Swarm, Combat, Obstacles, Finish, Economy, Levels, Feedback, Input
├── Editor/              Inspectors, gizmos, patrol handles and the GameObject › Crowd Runner menu
├── Prefabs/             Unit, EnemyUnit, MathGate, UnitPop effect, Coin, SpinningSaw, SweepingBar, SpikeTrap, FinishStairs
├── ScriptableObjects/   SwarmSettings, CombatSettings, gate presets (+10, +25, -10, x2, x3, ÷2), upgrades
├── Materials/           URP materials for units, gates, coins, obstacles and stairs
├── Shaders/             Crowd Runner/Unit: the vertex-animated run cycle
├── Meshes/              StickFigure: the default low-poly unit
├── Demo/                The demo game: scripts, levels, prefabs, art, fonts and sounds
├── Scenes/Demo/         CrowdRunnerDemo, the playable demo scene
└── Documentation/       This guide as a PDF

Assemblies

CrowdRunner.Runtime
Every gameplay system. Namespaces follow the folders: CrowdRunner.Runtime.Swarm, .Gates, .Combat and so on.
CrowdRunner.Editor
Inspectors, gizmos, Scene view handles and menu items. Editor only.
CrowdRunner.Demo
The demo game. It only uses the framework's public API, so you can delete Demo/ and Scenes/Demo/ without affecting the framework.

Object pooling

Units, enemies and effects come from pools that are filled up front. Each group reserves what it needs in Awake, so several groups can share one prefab's pool and nothing is instantiated during play.

  1. Create a pool config Create › Crowd Runner › Pool Config. Add each prefab with a prewarm count that covers its peak use.
  2. Add a Pool Manager Add Component › Crowd Runner › Pool Manager, and assign the config.
  3. Spawn from code Fetch the typed pool once, then spawn and despawn through it.
Spawner.cs
using CrowdRunner.Runtime.Pooling;

public class Spawner : MonoBehaviour
{
    [SerializeField] private Transform _unitPrefab;
    private ObjectPool<Transform> _units;

    private void Start()
    {
        // Fetch once; spawning from the typed pool skips every lookup.
        _units = PoolManager.Instance.GetPool(_unitPrefab);
    }

    public void SpawnUnit(Vector3 position)
    {
        Transform unit = _units.Get(position, Quaternion.identity);
        PoolManager.Instance.DespawnAfter(unit, 2f);
    }
}

Implement IPoolable on a prefab's components to reset state in OnSpawned and OnDespawned. When a pool runs dry, its PoolExhaustionPolicy decides what happens: Expand (instantiates and warns), ReuseOldest or ReturnNull. Select the Pool Manager in play to see live stats for every pool.

Player swarm

  1. Add the swarm GameObject › Crowd Runner › Player Swarm creates a SwarmController with the default unit prefab, SwarmSettings and a SwipeInput.
  2. Tune it In SwarmSettings: Max Units, Start Count, forward speed, lane width, steering and the formation (spiral or grid, spacing, unit radius).
  3. Press Play Drag, or hold A / D, to steer. Gates the leader crosses change the count.

The swarm GameObject is the leader. Units are pooled GameObjects placed by two Burst jobs: FormationLayoutJob assigns formation slots when the count changes, and UnitFollowJob eases units into them every frame. Jobs are scheduled in Update and completed in LateUpdate; call CompleteJobs() before reading unit transforms or UnitPositions earlier in the frame.

swarm.CountChanged += (before, after) => counterText.SetText("{0}", after);
swarm.UnitRemoved += position => PoolManager.Instance.Spawn(deathFx, position, Quaternion.identity);
swarm.Eliminated += ShowGameOver;

Use your own input by implementing ISteeringInput and assigning it to Steering Source, or call Steer() and SetSteering() directly.

Note

Units live at the root of the scene, not under the swarm: Unity only runs transform jobs in parallel across separate hierarchy roots.

Unit visuals

The default units are faceless stick figures with a round head, torso, arms and legs: one mesh of about 620 triangles in Meshes/StickFigure.asset. The Crowd Runner/Unit shader animates the run cycle in the vertex stage, so hundreds of units animate with no Animator, no skinning and no CPU cost, and stay SRP Batcher and GPU instancing compatible.

Cycles Per Second, Limb Swing, Bob Height, Forward Lean
Tune the run cycle on the material.
Phase Spread Per Metre
Offsets each unit's cycle by its position, so a crowd never runs in lockstep.
Color, Shade Color, Rim Color
A toon-like two-tone look with a soft rim.
Animation Group
Which component of the global _CrowdIdle vector the material reads: 0 runs, 1 stands. The demo's CrowdAnimationDriver sets it from SwarmController.IsRunning and enemy charges.

Your own mesh can use the shader: put each vertex's swing weight in UV1.x (the sign is the phase, 0 is rigid) and its limb's pivot height in UV1.y. Any prefab works as a unit, including rigged characters with an Animator, at a CPU cost per Animator at high counts.

Math gates

  1. Add a spawner GameObject › Crowd Runner › Gate Spawner. It uses Prefabs/Gates/MathGate.
  2. Add rows Click Add Row, then pick a preset for the left and right slots. Two presets make a linked pair: taking one closes the other. One preset makes a full-width gate.
  3. Tune them Change the operation and value with the inline sliders. They edit the preset asset, so use Make Unique to give a slot its own copy.

Presets are GateData assets (Create › Crowd Runner › Gate Data): add, subtract, multiply or divide by a value, with colours for good and bad gates. Gates use no colliders. Each frame the swarm asks the registry whether its leader crossed one:

count = GateRegistry.ProcessCrossing(previousLeaderPosition, leaderPosition, count, maxUnits);

Listen with MathGate.Triggered, GateRegistry.AnyGateTriggered or the gate's On Triggered UnityEvent. To link gates placed by hand, select them and use GameObject › Crowd Runner › Link Selected Gates.

Combat

  1. Add an enemy formation GameObject › Crowd Runner › Enemy Formation, placed on the track. Set its unit count, layout, engage distance, charge speed and, optionally, patrol points (drag them in the Scene view).
  2. Add a combat controller GameObject › Crowd Runner › Combat Controller. It finds the Player Swarm and uses the default CombatSettings.

When the gap between the swarm's edge and a formation's edge drops below the formation's Engage Distance, the battle starts: the swarm stops, the enemies charge, and every Tick Interval units in contact die, one player unit for one enemy unit. Contact is a squared-distance check in a Burst job, so no colliders or Rigidbodies are involved. The winner keeps the difference; a tie is a loss. After a win the swarm runs on.

Min Kills Per Tick, Kill Fraction
Pairs that die each tick, plus a share of the smaller side, so big battles stay short.
Stalemate Timeout
Seconds without contact before the nearest units fight anyway, so every battle finishes.
Player / Enemy Death Effect
Optional pooled effects spawned where units die.
combat.BattleStarted += enemy => cameraRig.FocusOn(enemy.transform);
combat.BattleEnded += (enemy, won) => { if (!won) ShowGameOver(); };
combat.Skirmish += (lost, killed) => hitSound.Play();

Gizmos: the red ring is a formation's boundary, the orange ring is where the battle starts, and the yellow line is its patrol path.

Obstacles

Obstacles kill the player's units they touch, without colliders. Each obstacle describes its danger as circles and rotated boxes on the ground; a Hazard Controller (GameObject › Crowd Runner › Hazard Controller) tests every unit against them in a Burst job each frame and removes the hits in one call.

ObstacleBehaviour
Spinning SawA blade that spins and, optionally, slides back and forth across the lane.
Sweeping BarAn arm sweeping around a post. The post is harmless.
Spike TrapSpikes that wobble as a warning, then rise and sink on a timer. Deadly only while up.

Give copies different Time Offsets so they move out of step. Listen with HazardController.UnitsKilled. To make your own, derive from Obstacle, animate it in Update, and describe its danger in WriteShapes and its area in Footprint (the demo's autopilot uses the footprint to steer clear).

public override int WriteShapes(NativeArray<HazardShape> shapes, int start)
{
    if (start >= shapes.Length) return 0;

    // Ground() flattens a world position onto the track plane.
    shapes[start] = HazardShape.Circle(Ground(_blade.position), _radius);
    return 1; // Shapes written.
}

Finish stairs

The genre's ending: after the finish line the crowd climbs steps labelled with multipliers (x1.2 to x5 over 20 steps by default), paying Units Per Step for each. The last step it can pay for sets the reward multiplier. The crowd always keeps at least one unit, so the climb never counts as a loss. Listen with FinishStairs.StepReached and Finished, or their UnityEvents.

Put Prefabs/Levels/FinishStairs (or your own copy) in a level and the Level Loader starts the climb when the swarm crosses the finish, then raises LevelFinished(multiplier). Without stairs it raises x1. The steps are built from the stairs' multiplier list, so its length sets the number of steps, one Step Prefab each: Prefabs/Levels/StairsStep, or any block with an optional TextMeshPro label. They are coloured along the stairs' gradient.

Levels

A level is a data asset, so new levels need no scene work.

  1. Create a level Create › Crowd Runner › Level. Set the finish distance, then add gate rows (two presets make a linked pair, one a full-width gate), enemy formations, obstacles, coin lines and, optionally, a finish stairs prefab. Distances run along the track from the Level Loader; x is across the lane.
  2. Add a loader GameObject › Crowd Runner › Level Loader comes with the gate, enemy unit and coin prefabs assigned. Assign your level, and select the loader to see the level drawn as gizmos.
  3. Chain levels List them in a Create › Crowd Runner › Level Sequence. sequence.Get(levelNumber) loops back to the first level after the last, while the level number keeps counting.
loader.Load(sequence.Get(progress.LevelIndex));
swarm.ResetSwarm(loader.StartPosition, startCount, run: false);
loader.LevelFinished += multiplier => ShowResults(multiplier);

// Restart without rebuilding: reset combat, the level, then the swarm.
combat.ResetCombat();
loader.ResetLevel();
swarm.ResetSwarm(loader.StartPosition, startCount);

Loading a level instantiates its content, which is the one moment that allocates; play itself does not. LevelData.Validate reports rows without presets, missing prefabs, enemies that never charge, and anything placed past the finish or off the lane. The loader logs those warnings in the Editor.

Coins and upgrades

Coins
Collected when the swarm's formation runs over them. The swarm animates and collects every coin through CoinRegistry, so coins have no Update of their own. Listen with CoinRegistry.AnyCollected.
PlayerProgress
Holds coins, the level reached, upgrade levels and the sound and haptics switches, saved through an IProgressStore: PlayerPrefsProgressStore by default (keys start with CrowdRunner.), or your own save system.
Upgrades
Create › Crowd Runner › Upgrade: an id, an effect, a value per level, a base cost, a cost growth and a max level. Built-in effects: Start Units and Income. Two are included in ScriptableObjects/Upgrades.
var progress = new PlayerProgress(new PlayerPrefsProgressStore(), upgrades);

int start = swarmSettings.StartCount + Mathf.RoundToInt(progress.GetEffect(UpgradeEffect.StartUnits));
int reward = PlayerProgress.Reward(collected, multiplier, progress.GetEffect(UpgradeEffect.Income));
progress.AddCoins(reward);

if (progress.TryBuy(upgrade)) RefreshShop();
Warning

An upgrade's Id is its save key. Keep it unchanged once players have bought the upgrade, or their levels are lost.

Sound and haptics

Add a Feedback Player (Add Component › Crowd Runner › Feedback Player) and assign clips to its cues. Gates, coins, battle start and hits, hazard kills and stairs steps play by themselves; call Play(feedback.Win) and similar for game-flow moments.

  • Each cue has a volume, a random pitch range, a minimum interval (so rapid events don't stack into noise) and a haptic strength.
  • Sounds come from a small AudioSource pool and never allocate.
  • The default haptics use Handheld.Vibrate, for heavy cues only. Assign your own IHapticsProvider to Haptics to use a haptics plugin with light taps.
  • SoundEnabled and HapticsEnabled switch each off.
public sealed class PluginHaptics : IHapticsProvider
{
    public void Play(HapticStrength strength)
    {
        // Call your haptics plugin here.
    }
}

feedback.Haptics = new PluginHaptics();

The demo

Scenes/Demo/CrowdRunnerDemo.unity plays three levels from Demo/Levels in a loop through a Level Loader:

LevelIntroduces
1Gates and coins, then the finish stairs
2A sliding saw, staggered spike traps, an enemy squad and a sweeping bar
3Everything, with a patrolling squad and an 80-unit grid "boss"

Each level opens on a start screen with the upgrade shop (More units, More coins), sound and vibration switches, Play, and Watch autopilot for an attract mode. The run shows the level, progress, coins and toasts for gates, hazards and battles, and ends on a results card with the coins earned and the multiplier reached: Next level and Replay, or Try again after a loss. The UI is laid out for portrait phones and expands to fit landscape screens, such as a WebGL page.

The demo's art, the Nunito font (SIL Open Font License) and its placeholder sounds (generated, free to use) live in Demo/. Its scripts are a good reference for wiring the systems together: DemoGameController runs the game flow, DemoAutopilot steers, and CrowdAnimationDriver drives the run cycle.

Measuring performance

The top-left overlay shows FPS, GC memory allocated per frame and the unit count. In the Editor that figure includes Editor-only allocations: the Editor UI, the Input System's Editor hooks, and TextMeshPro, which copies text to a string on every SetText only in the Editor. For the true number, add the demo scene to File › Build Profiles, make a Development Build and run it on a device.

Troubleshooting

Pink materials
The project is not on URP. The unit shader and materials need the Universal Render Pipeline 17.6+.
Gate and step labels are missing
Import TextMeshPro's Essentials: Window › TextMeshPro › Import TMP Essential Resources.
"No PoolManager in the scene"
A manager was created on first use. Add a Pool Manager with a Pool Config to the scene, so pools prewarm while it loads.
"Pool expanded at runtime"
A pool ran out during play. Raise its prewarm count in the Pool Config, or Max Units in the swarm settings.
Enemies stand still and the battle drags on
The formation's Charge Speed is 0. Level validation warns about this in the Console.
Units don't animate
The unit material is not on Crowd Runner/Unit, or the global _CrowdIdle vector holds the material's Animation Group at 1 (standing).
The GC figure is not zero in the Editor
See Measuring performance: the Editor allocates on its own. Measure in a Development Build.

FAQ

Can I use my own characters?
Yes. Any prefab works as a unit. Rigged characters with an Animator work too, at a CPU cost per Animator when there are hundreds of them.
How many units can it handle?
Max Units sets the cap (500 by default). Pools and job buffers are sized from it once, up front, so raise it as far as your target devices allow.
Does it use physics?
No. Gates, battles, hazards and coins use plain maths in jobs, so there are no colliders or Rigidbodies on units.
Can I use my own save system?
Yes. Implement IProgressStore and pass it to PlayerProgress.
Do I need the demo?
No. Delete Demo/ and Scenes/Demo/; the framework does not depend on them.

Release notes

1.0.0

  • Player swarm with Burst formation layout and movement, spiral and grid formations, swipe and keyboard steering.
  • Math gates with presets, linked pairs, full-width gates and an inline-editing Gate Spawner.
  • Crowd battles with enemy formations that patrol and charge, resolved by Burst contact jobs.
  • Obstacles: spinning saw, sweeping bar and spike trap, hit-tested in a Burst job.
  • Finish stairs with reward multipliers.
  • Coins, upgrades and saved progress through a swappable store.
  • Levels as data assets, level sequences and level gizmos.
  • Feedback Player for sound and haptics.
  • Stick figure units with a vertex-animated run cycle.
  • Zero-GC object pooling.
  • A three-level demo game with an upgrade shop, results screen and autopilot.

Need help?

Questions, bug reports or feature requests: I usually reply within a day.

support@hardartcore.com

Last updated October 30, 2026.