Balls Out

A Colour-Match Conveyor Puzzle — Unity Template

Version 1.0 | Unity 6+ | URP

🌟 This asset was made with the HyperCasual Game Engine!   Check it out — mobile puzzle templates & more. Click here to learn more ➔

Important: Template / Starting Point

This asset is designed as a template and starting point for your own game development. It provides the core systems, architecture, and tools needed to build a colour-match conveyor puzzle. You are expected to customise, extend, and modify this template to create your unique game. This is not a complete, ready-to-publish game — it's a foundation for developers to build upon.

Overview

Balls Out is a colour-match conveyor puzzle. Players tap any unblocked coloured box on the grid to send it flying onto a dock; rotating conveyors then funnel matching coloured beads into the docked boxes until each one fills to 100% and explodes in a particle burst. The game features a fully-rotating bead conveyor, hidden grid boxes revealed only when their neighbours complete, three in-game boosters with first-use tutorial popups, a built-in level editor, an automatic level generator, and post-processing polish out of the box.

Tap-to-Send Conveyor Puzzles

Intuitive one-tap controls — tap any unblocked box to send it to the dock, then watch the rotating bead conveyor feed it. Hidden boxes reveal as their neighbours clear.

Level Editor

Full Unity Editor window for designing levels — a colour-swatch palette, a drag-to-paint grid (Y-flipped so you see the player's orientation), live conveyor colour strips, and per-level dock & booster budgets.

Auto Level Generator

Batch-generate hundreds of playable levels with a concave difficulty curve, configurable colour count, grid width & height ranges, dock count, hidden-box probability, and conveyor channel length. Cancellable mid-batch with a progress bar.

Booster System

Three in-game boosters — Shuffle Beads, Shuffle Baskets, and Picker — with first-use tutorial popups and per-level unlocks. Counts are level-driven via the LevelData schema.

500 Levels Included

Designed to ship with 500 pre-generated levels on a progressive difficulty curve. Hand-tuned starter levels (1-5) ramp the player into the mechanic; the generator handles the rest. Generate more at any time.

Production Settings Out-of-the-Box

Portrait-locked, 1080×1920 reference canvas, Linear colour space, both input handlers enabled. WebGL canvas pre-set to 900×1800 with compression disabled for itch.io / Asset Store hosting compatibility.

Getting Started

Requirements

Installation

  1. Import the Balls Out package into your Unity project.
  2. Ensure the project is set to use the Universal Render Pipeline (check Edit > Project Settings > Graphics).
  3. Open the game scene at Assets/Balls Out/Scenes/GameNew.unity — the HUD, win/lose panels, booster popups, managers, and post-processing are already built and wired into it.
  4. (Optional) Generate a fresh batch of levels via Tools > Balls Out > Auto Level Generator, or hand-author them in Tools > Balls Out > Level Editor.
  5. Press Play to test Level 1.

Quick Test

Once in Play mode, tap any unblocked box on the grid — it flies up and lands in an empty dock. The rotating conveyor then funnels matching-colour beads into it until the box completes at 100%, vanishing in a particle burst. Boxes neighbouring a cleared box become tappable (or have their hidden colour revealed). Clear every box on the grid to win and the next level loads automatically.

Folder Structure

The package ships as a single top-level Balls Out/ folder. All game-specific assets, shared UI prefabs, scripts, and editor tooling live under this one folder so the Asset Store single-folder publishing rule is honoured.

Balls Out/ │ ├── Art/ │ └── Textures/ — 27 .png booster icons + radial shine decorations (subsumed from the shared UI package) │ ├── Audio/ — All sound effects (.wav): Blocked, Pop, Success, Switch, Fail, LevelClear, LevelFail, SnakeExit │ ├── Datas/ — Shared ScriptableObject assets (ColorData, fallback LevelData) │ ├── Documentation/ │ ├── Documentation.html — This file │ └── NewestDevUI-README.md — Archived notes on the shared UI scripts │ ├── Editor/ — Editor-only scripts (namespaces: BallsOut.EditorTools, HypercasualGameEngine) │ ├── LevelEditorWindow.cs — Visual level designer (colour palette, drag-paint grid, conveyor strips) │ ├── AutoLevelGeneratorWindow.cs — Append-mode batch generator with concave difficulty curve │ ├── BallsOutWelcomePopup.cs — Welcome popup on first project open │ └── InputSystemCheck.cs — Warns and fixes Active Input Handling if New-only │ ├── PostProcessing/ │ └── HC-PostProcessing-Volume.asset — Shared HC URP VolumeProfile (Bloom, Tonemapping, Colour Adjustments, Vignette) │ ├── Prefabs/ — Gameplay prefabs + shared UI prefabs │ ├── Bead.prefab / BeadRow.prefab / Box.prefab / Dock.prefab / MatchFx.prefab — Gameplay │ ├── GameHUD_Panel.prefab / LevelClearedPanel.prefab / GameOverPanel.prefab — HUD + win/lose │ ├── Popup_Powerup_Picker.prefab / DeveloperModeButton.prefab — Booster popups, dev panel │ └── BoosterManager.prefab / LoseWinPanelManager.prefab — Manager root GameObjects │ ├── Resources/ │ └── Levels/ — 500 level JSON files (Level_1.json ... Level_500.json) once the generator runs │ ├── Samples/ — Optional LevelManager.cs / SoundManager.cs stubs (rename .cs.txt → .cs to use) │ ├── Scenes/ │ └── GameNew.unity — Main game scene (single playable scene) │ └── Scripts/ — All runtime scripts (namespaces: BallsOut.Scripts, HypercasualGameEngine) ├── BallsOutGameManager.cs — Top-level orchestrator, arrow-key nav, win/lose choke point ├── GameLevel.cs / InputManager.cs — Gameplay loop + tap input ├── Bead.cs / BeadRow.cs / Box.cs / Dock.cs — Component data carriers ├── ColorData.cs / LevelData.cs — ScriptableObject palette + level schema ├── LevelManager.cs — Static JSON level loader; namespaced PlayerPrefs key ├── SoundManager.cs — Singleton audio facade (pools + booster + level-clear/fail) ├── LoseWinPanelManager.cs / BoosterManager.cs — Win/lose orchestration, 3-booster state machine ├── DeveloperModeButton.cs / DeveloperSettingsController.cs — Dev panel + Shift+U/W/L/R hotkeys ├── RotateClockwiseZ.cs / BoosterPopupIconMirror.cs — Win-screen rotation + popup-icon mirroring └── BoosterFeedback.cs — Lazy-instantiates a centred TMP label overlay for the Picker booster's PICK! activation feedback

Gameplay

Core Loop

Each level lays out a vertical grid of coloured boxes. Only boxes on the bottom row (y=0) start tappable. Tap any tappable box and it launches off the grid in a smooth arc onto an empty dock at the bottom of the screen. Once docked, the box waits for the rotating conveyor of beads to feed it — whenever the lead bead of the main conveyor row matches the docked box's colour, beads stream into the box and the fill counter ticks up. When a box fills to 100% it explodes in a particle burst, freeing its dock and revealing (or releasing) the four neighbour boxes around its original grid position. Clear every box on the grid to win.

Hidden Boxes

Boxes marked isHidden = true in the level data are spawned with a placeholder dark material and a muted UI tint; their real colour stays hidden until a neighbour box clears — or a booster reaches them — at which point the box plays a gradual reveal: a scale “pop” plus a colour fade-in from black to its true colour. This creates a satisfying chain-reveal as the player works upward through the grid.

Win & Lose

Win condition: every box on the grid has completed. The Game Manager fires SoundManager.Instance.PlayLevelClear() exactly once and the Level Cleared Panel appears with the level number. Lose condition: there is intentionally no lose condition by default in the shipping template — the player cannot fail Level 1 by misclick, only by running out of conveyor beads, which the generator's channel-length padding prevents. Wire BallsOutGameManager.Instance.TriggerLose() from your gameplay layer if you want a move-limit or time-limit variant.

Scripts Reference

Core Gameplay (Balls Out/Scripts/)

ScriptResponsibility
BallsOutGameManagerTop-level orchestrator. State machine (Playing / Won / Lost). [DefaultExecutionOrder(-100)] so its Awake populates the HUD CurrentLevelText before any other MonoBehaviour. Owns arrow-key level navigation; routes win/lose through itself before forwarding to LoseWinPanelManager so PlayLevelClear / PlayLevelFail fires exactly once.
GameLevelSpawns the dock slots (per-level count), the box grid, and the rotating bead conveyors; runs LoopConveyor() every durationConveyor seconds to advance beads and dispense matched colours into docked boxes; animates fill / complete / release transitions and the booster effects.
InputManagerNew Input System tap / touch raycast onto boxes. Calls SoundManager.PlayTap() on a valid tap, PlayBlocked() on a tap that did nothing.
Box / Bead / BeadRow / DockLightweight data-holding MonoBehaviours; no behaviour beyond serialised Inspector references.

Engine / Shared (Balls Out/Scripts/, namespace HypercasualGameEngine)

ScriptResponsibility
LevelManagerStatic JSON loader for Resources/Levels/Level_<N>.json. CurrentLevelIndex persists via PlayerPrefs key "Balls Out_CurrentLevel". GetCurrentLevel() returns a runtime ScriptableObject built via JsonUtility.FromJsonOverwrite and flagged HideFlags.HideAndDontSave so it survives domain reloads without leaking into project assets.
SoundManagerSingle-source-of-truth singleton audio facade. One AudioSource, [Range(0,1)] masterVolume, one Play<Event>() per event. Random pools for Pop, Success, Switch, Fail clips. Booster SFX methods (PlayBlocked, PlayBoosterShuffle, PlayBoosterUnlock) called from the BoosterManager.
LevelDataRuntime ScriptableObject schema: bead colour lists for the three conveyor channels, the grid layout & width, the three booster int counts, and the dock count.
ColorDataScriptableObject palette mapping TypeColor → Material & UI Color.

UI / Boosters / Dev Mode (NewestDevUI — shared package)

ScriptResponsibility
LoseWinPanelManagerWin / Lose UI orchestration. Auto-finds winLevelText, writes "LEVEL N" using the level the player just cleared (1-based), re-wires Continue / Retry buttons on every show. LoadNextLevel calls LevelManager.AdvanceLevel() before reloading the scene (pitfall #2 fix is built in).
BoosterManager3-booster state machine, level-gated. First-use tutorial popups fire BEFORE the can-effect-run gate (pitfall #31 fix). Self-heals if hooks are lost. Counts are read from LevelData.shuffleBoosterCount / bombBoosterCount / unlockBoosterCount.
DeveloperSettingsControllerShift+U / Shift+W / Shift+L / Shift+R hotkeys for unlock-and-refill boosters, trigger win, trigger lose, reload level. Wired to dev-panel buttons. Ad debug controls compile-guarded by RAGENDOM_ADS.
DeveloperModeButtonToggles the developer settings panel. Auto-finds child named DeveloperSettingsPanel if not assigned.
RotateClockwiseZRotates the win-screen radial shine sprites around their local Z. Uses unscaledDeltaTime so the animation stays platform-equal regardless of gameplay Time.timeScale.
BoosterPopupIconMirrorOn enable, mirrors the source Booster-Button-N's sprite onto the popup's TopIcon so the tutorial icon always matches the booster the player just tapped.
BoosterFeedbackLazy-instantiates a centred TMP label overlay (own sub-Canvas at sortingOrder = 50) and animates a scale-in / hold / fade-out. Only the Picker booster uses it (the green PICK! label); Shuffle Beads and Shuffle Baskets rely purely on their in-world animations — the bead recolour sweep and the boxes physically swirling between cells.

Editor Tooling (Balls Out/Editor/)

ScriptResponsibility
LevelEditorWindowTwo-pane visual editor: level list with Add / Duplicate / Delete(confirm) / ▲ / ▼ / Save All / Reload on the left; on the right a level-settings box (grid width, dock count, booster counts), a colour palette + paint-mode toggles, a drag-paintable grid canvas (Y flipped visually), and the three conveyor channels rendered as live, click-editable colour strips. Save All renumbers and re-writes every level so deletions never leave gaps.
AutoLevelGeneratorWindowAppend-mode batch generator. Concave difficulty curve reaches max by level ~12 regardless of total count. Cancellable progress bar. GC.Collect() every ~50 levels for memory hygiene. Red Delete-All button with explicit confirm step.
BallsOutWelcomePopup[InitializeOnLoad] first-open popup. Buttons open the documentation, the GameNew scene, the Level Editor, and the Auto Level Generator; includes an Asset Store review link.
InputSystemCheckVerbatim from the shared HC Engine repo. Blocks Play mode if Active Input Handling is set to New only, offers a one-click Fix & Restart.

Level Editor

Open via Tools > Balls Out > Level Editor. The window has two panes:

Left pane — Level list

Right pane — Level details

Auto Level Generator

Open via Tools > Balls Out > Auto Level Generator. Append-mode — existing levels on disk are never overwritten; new levels begin at Level_<existing + 1>.json.

Settings

SettingDefaultPurpose
Count500How many levels to append in this batch.
Seed12345Deterministic seeding. Re-run with the same seed + same existing-count and you get byte-identical levels.
Min / Max Grid Width3 — 4Board width scales with difficulty across the batch.
Min / Max Grid Height1 — 5Grid height scales with difficulty across the batch.
Min / Max Colours1 — 6Palette size scales with difficulty.
Bead Row Count24Matches the scene's conveyor row count. Channel lists are padded to at least this length.
Channel Length80Entries per left / right / main conveyor list. Each entry feeds 5 beads; 100 beads complete a box, so 20 entries per box per channel is the rough requirement, padded for safety.
Hidden Chance Min / Max0 — 0.45Probability a non-bottom box is hidden, scaled with difficulty.
Default Shuffle Beads / Shuffle Baskets / Picker2 / 2 / 2Per-level booster budget floor. The generated value is the higher of this and the difficulty ramp.
Dock Count Min / Max3 — 5Number of dock slots. Level 1 is always the minimum; later levels skew toward the max, and level 6+ never uses the minimum.

Difficulty Curve

Concave ramp reaching max difficulty by level ~12 regardless of total batch size. Early levels are tiny single-colour grids the player can clear in a few seconds, so the early game never feels punishing. Difficulty has ±7% jitter so the curve doesn't feel sterile.

Danger zone

The red Delete ALL existing levels button requires a two-step confirm. It removes every Level_<N>.json on disk along with its .meta pair, then refreshes the AssetDatabase.

Boosters

Three boosters, level-gated, each with a one-time tutorial popup keyed by PlayerPrefs. The shared BoosterManager handles counts, unlocks, SFX, and popups; the actual effects are game-specific methods on GameLevel.

IndexNameDefault unlock level (0-based)Effect
1Shuffle Beads0Instant. Randomises the colour of every bead currently on the three conveyor rings. Each bead briefly scale-pulses so the player sees the recolouring sweep across the conveyor. Implemented in GameLevel.ShuffleConveyorBeads().
2Shuffle Baskets1Instant. Fisher-Yates permutes every box that is not yet docked across each other's grid cells — each box visibly slides along a curved, arced, staggered path to its new cell (no snap-swap). Tappability follows the cell; a hidden box that lands in a clickable cell is revealed on the spot. Implemented in GameLevel.ShuffleBoxes().
3Picker2Two-step targeted booster: the first click arms targeting (the Picker button's Bg pulses green) and every locked grid box starts flashing; tapping one makes it skip the neighbour-release queue and fly straight to a dock (revealing it if hidden). A second Picker-button click before tapping a box cancels targeting. Implemented in GameLevel.PickerFillBox().

On-screen feedback

The Picker booster shows a centred green "PICK!" TMP label via BoosterFeedback.cs — lazily spawned under the active Canvas with overrideSorting = true, sortingOrder = 50 so it renders above the gameplay HUD and popups; it scales in, holds, and fades out over ~0.7 s. Shuffle Beads and Shuffle Baskets show no label — their in-world animations (the conveyor recolour sweep and the boxes physically swirling between cells) carry all the feedback the player needs.

Tutorial popups

Each booster's first click pops the Booster-Popup-N overlay with the booster's icon. The popup auto-dismisses when the player taps Continue. The popup fires before the can-effect-run gate, so a booster always shows its tutorial on the first tap even if its effect would be a no-op. Reset all popups with the developer-mode Reset Popup Prefs button.

PlayerPrefs keys (per-game prefixed — pitfall #16)

KeyPurpose
Balls Out_CurrentLevelThe 0-based level index the player is on. Stamped by LevelManager.AdvanceLevel().
BoosterPopupShown_ShuffleTutorial popup gate for Shuffle Beads. 0 = not shown yet.
BoosterPopupShown_ShuffleBasketsTutorial popup gate for Shuffle Baskets.
BoosterPopupShown_PickerTutorial popup gate for Picker.

Customization

Add a new colour

  1. Extend the TypeColor enum in Assets/Balls Out/Scripts/LevelData.cs with a new entry at the END (appending preserves serialised enum int values in existing level JSONs).
  2. Open the ColorData ScriptableObject under Assets/Balls Out/Datas/ColorData.asset and add a new ColorDataItem entry mapping the new TypeColor to a Material and a UI Color.
  3. Re-import the project; existing levels keep working, new levels can now use the added colour.

Tune the conveyor pacing

On the GameLevel component in the scene: durationConveyor controls how often the main conveyor row advances (scene-default 0.1111 s — the script-default 0.5 s divided by the 1.8× speed-up the production tune dialled in); durationFillBeadToBox controls the bead-to-box flight time; durationMoveToDestroy controls the box-completion animation; durationFillBoxToDock controls the box-to-dock launch arc.

Replace booster effects

The boosters' effects are game-specific methods on GameLevel: ShuffleConveyorBeads() (Shuffle Beads), ShuffleBoxes() (Shuffle Baskets), and PickerFillBox(Box) (Picker). BoosterManager owns the count / SFX / popup bookkeeping and the Picker's targeting flag; it locates the scene's GameLevel and forwards to those methods. To change a booster's behaviour, edit the corresponding GameLevel method directly. To add a new booster, follow the OnBoosterNClicked / PopupKeyN pattern in BoosterManager.cs and add a 4th Booster-Button-N child + Booster-Popup-N in the HUD prefab.

Adjust the per-level booster ramp

Each generated level's booster budget is max(default, ramp) — the difficulty ramp adds +1 from level 10 and +2 from level 50, and the Default Shuffle Beads / Shuffle Baskets / Picker fields (2 / 2 / 2 out of the box) set the floor. With the default 2s, every level gets at least 2 of each. The logic lives in AutoLevelGeneratorWindow.BuildLevel. To override a single level, use the Level Editor → right-pane Booster Counts fields and click Save All.

Audio palette

SoundManager's clip arrays are picked round-robin / random. Drop additional Pop<N>.wav / Success<N>.wav / Switch<N>.wav / Fail<N>.wav files into Assets/Balls Out/Audio/ and assign them to the matching array on the SoundManager component in the scene.

Re-skin the HUD

Every HUD widget is a prefab in Balls Out/Prefabs/. Edit the prefab assets directly — the changes propagate to the prefab instances already placed in the GameNew scene.

Support

Common Issues

SymptomLikely Cause & Fix
Level 1 plays correctly, but Level 2 silently doesn't load on Continue.The cached LevelData ScriptableObject got destroyed by the scene reload — check that LevelData is created with HideFlags.HideAndDontSave (pitfall #1). LevelManager's GetCurrentLevel self-heals when the cached instance is null.
Pressing Play crashes with an Input System error.Active Input Handling is set to "New only". Open Edit > Project Settings > Player > Active Input Handling and set it to Both, then restart Unity. InputSystemCheck warns and offers a one-click fix on project load.
Booster button shows the right count but tapping does nothing.Either the scene is missing the BoosterManager prefab, or it is present but its Init(LevelData) wasn't called — check BallsOutGameManager runs at DefaultExecutionOrder(-100) and calls BoosterManager.Instance.Init in Start, not Awake (BoosterManager's Awake hasn't run yet at order -100).
Picker button click does nothing visible.Picker is a two-step booster — the first click arms targeting (the button's Bg pulses green and the locked grid boxes flash). The booster is only consumed when you tap a locked box afterwards. A second Picker-button click before tapping a box cancels targeting without consuming.
Boxes complete but the win panel never appears.Confirm there is exactly one BallsOutGameManager in the scene and that countBox on GameLevel matches levelData.beadGrids.Count. Win is routed via BallsOutGameManager.TriggerWin — calling LoseWinPanelManager.ShowWin directly bypasses PlayLevelClear.
Levels have no boosters, or the wrong dock count.Level JSONs generated before these fields existed won't carry them. Re-generate via Tools > Balls Out > Auto Level Generator, or set the values per-level in the Level Editor (Booster Counts + Docks) and click Save All.

Resetting Progress

The dev panel's Reset Popup Prefs button clears the tutorial popup gates. To reset the level index, call HypercasualGameEngine.LevelManager.ResetProgress() from a debug button or PlayerPrefs.DeleteAll() from the Unity console while in Play mode.

Contact

Questions, bug reports, or feature requests? Get in touch — ragendom@gmail.com.