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.
Play in one click
Open the demo scene and press Play: three levels, a shop, results and an autopilot.
Built for crowds
Up to 500 units laid out and moved by Burst jobs, with no physics and no garbage per frame.
Math gates
Add, subtract, multiply and divide, in linked pairs or full width, with an inline-editing spawner.
Crowd battles
Armies engage, charge and trade units one for one on a tick, from contact checks in a job.
Obstacles
Spinning saws, sweeping bars and spike traps, tested against every unit without colliders.
Levels as data
Gates, enemies, obstacles, coins and the finish in one asset, played in a looping sequence.
Coins and upgrades
Rewards, upgrades with growing prices and saved progress, through a swappable store.
Sound and haptics
One Feedback Player gives every event a sound and a vibration, from pooled AudioSources.
Requirements
| Item | Version | Notes |
|---|---|---|
| Unity | 6000.6+ | |
| Universal Render Pipeline | 17.6+ | The unit shader and materials are URP and SRP Batcher compatible |
com.unity.burst | 2.0+ | Installed as a dependency |
com.unity.collections | 6.6+ | Installed as a dependency |
com.unity.mathematics | 1.4+ | Installed as a dependency |
com.unity.inputsystem | 1.20+ | Swipe and keyboard steering |
com.unity.ugui | 2.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
- Import the package Unity installs the package dependencies. If TextMeshPro asks to import its Essentials, accept.
- Play the demo Open
Scenes/Demo/CrowdRunnerDemo.unityand press Play. Press Play on the start screen, or Watch autopilot to let it play itself. - Build your own scene From , add a Player Swarm, a Combat Controller, a Hazard Controller and a Level Loader. Each one comes wired to the default prefabs and settings.
- Make a level , fill in its lists, assign it to the Level Loader and press Play. See Levels.
Add a 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
| Control | Action |
|---|---|
| Play, Space or Enter | Start the run from the start screen |
| Drag / swipe, or A / D | Steer |
| T or Auto | Autopilot: dodges obstacles, picks up coins, steers into the gate giving the most units |
| P / Space or Pause | Stop and resume running (not during battles or on the stairs) |
| + / - or +10 / -10 | Add or remove units through the pool, to try the 500-unit cap and elimination |
| R or Restart | Restart 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,.Combatand 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/andScenes/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.
- Create a pool config . Add each prefab with a prewarm count that covers its peak use.
- Add a Pool Manager , and assign the config.
- Spawn from code Fetch the typed pool once, then spawn and despawn through it.
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
- Add the swarm creates a
SwarmControllerwith the default unit prefab,SwarmSettingsand aSwipeInput. - Tune it In
SwarmSettings: Max Units, Start Count, forward speed, lane width, steering and the formation (spiral or grid, spacing, unit radius). - 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.
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
_CrowdIdlevector the material reads: 0 runs, 1 stands. The demo'sCrowdAnimationDriversets it fromSwarmController.IsRunningand 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
- Add a spawner . It uses
Prefabs/Gates/MathGate. - 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.
- 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 (): 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
.
Combat
- Add an 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).
- Add a 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 () tests every unit against them in a Burst job each frame and removes the hits in one call.
| Obstacle | Behaviour |
|---|---|
| Spinning Saw | A blade that spins and, optionally, slides back and forth across the lane. |
| Sweeping Bar | An arm sweeping around a post. The post is harmless. |
| Spike Trap | Spikes 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.
- Create a 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.
- Add a 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.
- Chain levels List them in a .
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 noUpdateof their own. Listen withCoinRegistry.AnyCollected. - PlayerProgress
- Holds coins, the level reached, upgrade levels and the sound and haptics switches, saved through an
IProgressStore:PlayerPrefsProgressStoreby default (keys start withCrowdRunner.), or your own save system. - Upgrades
- : 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();
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 ()
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 ownIHapticsProvidertoHapticsto use a haptics plugin with light taps. SoundEnabledandHapticsEnabledswitch 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:
| Level | Introduces |
|---|---|
| 1 | Gates and coins, then the finish stairs |
| 2 | A sliding saw, staggered spike traps, an enemy squad and a sweeping bar |
| 3 | Everything, 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
, 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: .
- "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_CrowdIdlevector 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
IProgressStoreand pass it toPlayerProgress. - Do I need the demo?
- No. Delete
Demo/andScenes/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.
Last updated October 30, 2026.