Simple Maze
Mazes for Unity that are generated, solved exactly and rated, on square, hex, triangle and circular grids.
Simple Maze builds a maze from your settings, then proves it can be solved and works out its par, the fewest moves it takes. It can add keys and doors, switches and gates, gems, traps, one-way sides and portals, stack up to 8 floors joined by stairs, put up to 4 players on the board, and make the players roll like on ice instead of stepping. Every maze is rated 1 to 5 stars and can be saved as a ScriptableObject, exported as a PNG or SVG picture, or played in the included 3D demo.
What it is
Simple Maze is a maze generator with a solver built in. You choose the grid, the size and what goes in the maze, and it carves one for you. Then it solves the maze exactly, so you know for certain that it can be finished, how many moves the best solution takes and how many different best solutions there are.
Everything is deterministic. The same settings and the same seed always give the same maze, and every maze keeps the settings and seed it was made from. When the generator can't give you exactly what you asked for, it says so in the maze's notes instead of quietly handing you something else.
The four grids
Square
Classic square cells with four sides, from 2 by 2 up to 64 by 64. The only grid with weave crossings, where a passage tunnels under a corridor.
Hex
Pointy-top hexagons with six ways out of each cell, up to 64 by 64.
Triangle
Alternating up and down triangles with three sides each, up to 64 by 64.
Circular
A polar maze: a centre cell inside 2 to 40 rings that split into more cells as they grow, so the cells stay roughly square from the centre to the rim.
Any grid can be stacked into 2 to 8 floors joined by 1 to 4 stairs between each pair of floors, and any grid can be cut to a shape: a circle, ring, heart, star, diamond, cross, a word in the built-in font, or your own picture.
Features
Proven par
The solver searches every position the players can reach, so the par is the true minimum and not an estimate.
Soft-lock check
Every maze reports whether a player can get stuck somewhere they can no longer finish from, and NoSoftLocks only accepts mazes where that can't happen.
Difficulty stars
A 1 to 5 star rating worked out from the par, the real decisions on the way and the gimmicks, calibrated so each star covers about a fifth of everyday mazes. Ask for a star count and the generator keeps trying until it gets one.
Gimmicks
Up to 6 keys and doors, 4 switches with up to 3 gates each, 8 gems, 24 one-way sides, 4 portal pairs and traps, all placed so the maze stays solvable and proven together.
Players and movement
Up to 4 players, either racing to one goal from starts the same distance away or each heading to a goal of its own. Players step one cell at a time, or roll in a straight line until something stops them (Square and Hex).
Six carving algorithms
Recursive Backtracker, Prim, Kruskal, Wilson, Hunt-and-Kill and Growing Tree, each with its own look, plus braid, extra loops, open rooms and holes.
Shapes from pictures
Drop an image into the generator window and its dark, light or opaque areas become the maze.
Printable export
Save any maze as a PNG or SVG: a black on white puzzle sheet, an answer key with the solution drawn in, or the full colour view with every gimmick.
ScriptableObjects
Save single mazes or ordered packs, with inspectors that preview, re-check and regenerate them.
Play rules included
MazeRules tells your game what a move does, using the same rules the solver counted with, so the game and the par always agree. It also gives hints from any position.
Editor tooling
One Maze Generator window with 19 presets, an Auto button that fixes settings that fight each other, and generation on a background thread with a progress bar and Cancel.
A 3D demo
Five game modes with endless mazes and 50 ready-made levels, built entirely in code. It works in URP, HDRP and the Built-in pipeline.
How it works
Configure
Pick a grid, its size and what goes in the maze, in the Maze Generator window or with a MazeSettings object in code.
Build
The board is cut to its shape, carved, and given its rooms, loops and crossings. Then the start and goal are placed, the extra players are added and the gimmicks are set out along the way.
Solve and rate
The solver explores every reachable position, counting keys held, gems taken and switches flipped. It records the par, how many best solutions exist, one of them, whether anyone can get stuck, and the star rating.
Use it
Save it as an asset, export a picture of it, play it in the demo, or read the MazeData in your own scripts.
Why "verified"
Lots of generators make mazes that are probably fine. Simple Maze proves the things you design around:
- The par is real. It is the shortest solution that exists, counting every key, door, gate and portal, so a 40-move maze really takes 40 moves.
- Gimmicks are part of the proof. Doors, gates and gems are solved together with the maze, and by default every door and every switch's first gate stands in the way, so its key or switch is really needed.
- Getting stuck is detected. One-way sides, traps and rolling can leave a player somewhere they can't finish from. Every maze reports it, and you can ask for mazes where it never happens.
- Misses are reported. When a target star count, a coverage or a gimmick can't be met, the maze's notes say what happened and what you got instead.
One honest limit: the solver has a state budget (500,000 positions by default). A very large maze with many players and gimmicks can run past it, and then the par is reported as not proven rather than guessed. Raise StateBudget or make the maze smaller.
Getting started
Open Window > Living Failure > Simple Maze > Maze Generator, pick a preset, press Generate and look at the analysis on the right. Then open the demo scene and play. The Quick Start walks through the whole loop in about five minutes.
Quick Start
From import to a maze you can play, in about five minutes.
1. Install
Import the package. Everything lives under Assets/SimpleMaze/, and nothing is added to your scenes until you ask for it.
The core (the generator, the solver, the play rules and the maze data) has no package dependencies. The demo uses the Input System and Unity UI packages. It reads the keyboard through the Input System, so set Project Settings > Player > Active Input Handling to Input System Package or Both before you play it.
Render pipelines: URP is recommended, HDRP is supported and the Built-in pipeline still works. The demo builds its materials from the active pipeline's default shader, and on HDRP it adds a small volume so it looks the same as on the others (turn off HdrpLook on the MazeDemo component to use your own lighting instead). Simple Maze was built and tested with Unity 6000.6.
2. Generate a maze
Open Window > Living Failure > Simple Maze > Maze Generator. You can also get there from Assets > Create > Simple Maze > Maze Generator.
Take a preset
Press Presets and pick Classic/Circular 10 rings. A preset replaces every setting with a ready-made recipe.
Generate
Press Generate. Mazes are built on a background thread, so the window stays responsive, and a long batch shows a progress bar with a Cancel button. Raise Batch count to make up to 20 at a time.
Try a few more
Gimmicks/Keys and doors locks the way with doors whose keys hide before them. Roll/Ice board (square) makes the player slide until a wall stops it. Big/Tower of 3 floors stacks three floors joined by stairs, and Shapes/Word: MAZE carves the maze into the letters of a word.
If you change settings by hand and they fight each other (Roll on a triangle grid, say), press Auto. It fixes them and tells you what it changed.
3. Read the analysis
The pane on the right shows the selected maze. From the top:
- The verdict: the par in steps or rolls, whether the best solution is unique or how many there are, and the difficulty out of 5. A warning shows when a player can get stuck.
- Board: a picture of the maze with a legend, tabs for each floor, and a zoom. Hover a cell to see what is in it.
- Best solution: a slider that steps through the solution, with the players moving, keys and gems disappearing as they're taken and gates flipping. Below it is the move list and a Copy solution button.
- Facts and Generation report: the numbers (dead ends, loops, decisions, coverage and more) and every note the generator left.
- Export: save the maze as a PNG or SVG, as a printable sheet, an answer key or the colour view.
Dragging the solution slider is the quickest way to judge a maze. You can see at a glance whether the route wanders the whole board or runs straight to the goal.
4. Save it
Press Save and choose:
- Current saves the selected maze as a MazePuzzleAsset.
- Pack (all) saves every maze in the set as one ordered MazePuzzlePack.
- Pack (unique only) saves just the mazes with exactly one best solution.
Both asset types have inspectors with the same analysis pane, plus Verify, which solves the maze again and refreshes its stored verdict, and Re-roll, which makes a new maze from the same settings with a fresh seed. A pack also has Export all, which writes every maze as a printable picture and its answer key into a folder you pick.
5. Play the demo
Open Assets/SimpleMaze/Scenes/SimpleMazeDemo.unity and press Play. The demo opens on a welcome screen. Press Let's play, pick a mode, then choose Endless for a new maze to your order or Level Pack for the ready-made levels.
The controls are also listed at the bottom of the screen while you play:
- Arrows or WASD move or roll, and Q, E, Z and C take the diagonal sides on hex, triangle and circular mazes.
- Space or Enter takes the stairs (or Page Up and Page Down).
- U (or Backspace, or Ctrl+Z) undoes a move, R restarts and H shows a hint.
- F lets the solver play the rest of the maze from where you are, and F or R stops it.
- Tab or 1 to 4 switches player when several players each have a goal of their own.
- [ and ] or a right-drag turn the view, Home and End tilt it, the mouse wheel or + and - zoom, and V resets it.
- M mutes the sound and Esc goes back to the menu.
6. Use it in your game
Make a maze in code, check what the solver found, and read the rest straight off the MazeData:
using SimpleMaze;
var settings = new MazeSettings
{
Grid = TopologySpec.Circle(10),
Endpoints = Endpoints.OutsideIn, // start on the rim, goal in the centre
Keys = 2,
TargetDifficulty = 3, // keep trying until it rates 3 stars
};
MazeData maze = MazeGenerator.GenerateVerified(settings);
Debug.Log($"Par {maze.Par} steps, {maze.Difficulty} stars, solvable: {maze.Solvable}");
To run your own game on it, keep a MazePlay and ask MazeRules what each move does. These are the same rules the solver used, so your game always agrees with the par:
MazePlay play = MazeRules.NewGame(maze);
int here = play.Positions[0];
int next = maze.Topology.Neighbor(here, 0); // the cell through side 0
if (MazeRules.TryStep(maze, play, 0, next, out string reason))
{
if (MazeRules.IsSolved(maze, play)) Debug.Log("Solved!");
}
else Debug.Log(reason); // why it can't go: a wall, a locked door...
Or let the demo's 3D view draw and play it for you. MazeGame adds a MazeView3D to the same object and builds the whole maze from primitives:
using SimpleMaze.Demo;
var game = new GameObject("Maze").AddComponent<MazeGame>();
game.Begin(maze);
game.View.FrameCamera(Camera.main, -1); // -1 frames every floor
A saved maze works the same way: give your script a MazePuzzleAsset field and use its Maze, or a MazePuzzlePack and its Get(index). See the API Reference for the rest.
Where to next
- Generating explains every setting and what it does.
- Demos shows how the demo scene is put together and how to change it.
- Troubleshooting covers notes, fallbacks and limits.
Generating
Every setting, what it changes, and how the generator keeps its promises.
The Maze Generator window
Open Window > Living Failure > Simple Maze > Maze Generator. The settings are on the left in six sections (Board, Carving, Shape, Players and movement, Gimmicks and Quality), and the mazes you've made are on the right with the analysis pane. Fields only appear when they apply, so Rings shows for a circular board and Width and Height for the others.
Every maze the window makes goes through verified generation: if a maze misses what you asked for, the generator tries the next seed, and if none of its tries succeed it gives you the closest one with a note saying why. Generation runs on a background thread with a progress bar and a Cancel button, and your settings and the unsaved mazes survive a recompile.
Each table below gives the label in the window, the matching field on MazeSettings for code, and the range it accepts. Values outside the range are clamped, so code can pass anything.
Board
| Setting | Field | Range | What it does |
|---|---|---|---|
| Grid | Grid.Shape | Square, Hex, Triangle, Circular | The cell shape. Square cells have 4 sides, Hex 6 (pointy-top), Triangle 3 (alternating up and down), and Circular is a centre cell inside rings. |
| Width, Height | Grid.Width, Grid.Height | 2 to 64 | Cells across and rows (on Triangle, Width counts triangles per row). |
| Rings | Grid.Rings | 2 to 40 | Circular only: the rings around the centre cell, which is the board's radius. Rings split into more cells as they grow, so cells stay roughly square from the centre to the rim. |
| First ring cells | Grid.FirstRing | 0, or 3 to 12 | Circular only: the cells in the first ring. 0 means automatic, which is 6. |
| Floors | Grid.Floors | 1 to 8 | Stack the board into floors joined by stairs. Every floor has the same outline and holes, and each is carved as its own maze. |
| Stairs per floor | Stairs | 1 to 4 | Stairs between each pair of floors. The first goes where that floor's maze is farthest from the stairs you arrived by, so every floor gets crossed, and each extra one adds a loop. |
In code, TopologySpec.Grid(shape, width, height) and TopologySpec.Circle(rings) build the board, and you set Floors on the result.
Carving
| Setting | Field | Range | What it does |
|---|---|---|---|
| Algorithm | Algorithm | 6 choices | How the maze is carved. Each one has its own look: Recursive Backtracker makes long winding corridors, Prim short and bushy ones, Kruskal an even, patchy maze, Wilson a perfectly uniform one, Hunt and Kill long corridors with fewer dead ends, and Growing Tree anything in between. |
| Branchiness | Branchiness | 0 to 1 | Growing Tree only: 0 gives long winding corridors and 1 short, bushy ones. |
| Braid | Braid | 0 to 1 | Dead-end removal. 0 is a perfect maze, with exactly one route between any two cells, and 1 leaves no dead ends at all. Rolling mazes need some braid, because rolling needs loops. |
| Extra loops | Loops | 0 to 200 in the window, up to 999 in code | Random openings, each adding a second way round. |
| Rooms | Rooms | 0 to 12 | Open rooms carved into the maze, spread out. |
| Room size | RoomSize | 1 to 4 | How far each room reaches from its centre, in cells. |
| Holes | Holes | 0 to 0.35 | The fraction of cells removed as holes. The board always stays in one piece. |
| Weave crossings | Weave | 0 to 40 | Square boards only: places where a passage tunnels under a corridor, like the bridges in a printed weave maze. The maze keeps its routes and just gains crossings. |
Shape
| Setting | Field | Range | What it does |
|---|---|---|---|
| Shape | Shape | None, Circle, Ring, Heart, Star, Diamond, Cross, Text, Custom | Cuts the board to an outline. It works on every grid. |
| Text | ShapeText | A to Z, 0 to 9, space, - ! ? . + and
<3 for a heart | Shape Text: the word, in the built-in 5 by 7 font. Give it a wide board, about 6 cells across per letter and at least 9 rows. |
| Image, or Mask rows | ShapeMask | any picture, or rows of
# and . | Shape Custom: drop in any picture and choose whether its Dark, Light or Opaque areas are inside the maze, where the line falls, and how much Detail to keep (24 to 160 mask pixels across). The picture needs no import settings. Leave the image empty to type the shape as rows instead, top row first. |
| Fill | ShapeFill | 0.3 to 1 | How much of the board the shape fills. |
| Connect pieces | ConnectPieces | on or off | When the shape falls apart into pieces, like the letters of a word, join them with the shortest bridges. Off keeps only the largest piece. |
Players and movement
| Setting | Field | Range | What it does |
|---|---|---|---|
| Movement | Movement | Step, Roll | Step moves one cell at a time on every grid. Roll slides in a straight line until something stops it, on one-floor Square and Hex boards only. Anywhere else a Roll maze uses Step, with a note. |
| Start / goal | Endpoints | Farthest, Center Out, Outside In, Fixed | Farthest puts them at the two cells farthest apart along the maze. Center Out starts in the middle with the goal on the rim, and Outside In is the other way round. Fixed uses the two cells you give. |
| Start cell, Goal cell | Start, Goal | cell indices | Fixed only. If they aren't two different playable cells, the farthest pair is used instead, with a note. |
| Open border | OpenBorder | on or off | Cuts an entrance and an exit in the outer wall where the start and goal touch it, for printed mazes. With Farthest, both are then picked on the border. |
| Players | Players | 1 to 4 | How many players are on the board. |
| Shared goal | SharedGoal | on or off | With several players: on gives everyone one goal (the centre, or the Fixed goal) from fair starts, which makes it a race. Off gives each player a goal of its own. |
Fair races. On Step, a race's starts are picked so that every player's par is exactly the same. With gimmicks, each player has to fetch its own keys and gems, so the other players' starts are chosen after the gimmicks are placed, using each cell's real par. When a board has no fair starts, a "Players:" note says so, MazeData.Fair is false, and verified generation tries the next seed.
How players move
The solver, the generator and the play rules your game uses all follow the same rules, so a move your game allows is exactly a move the solver counted.
Step. A player moves one cell through an open side. A door needs its key, a shut gate blocks, a one-way side only lets you out its own way, a tunnel side leads under the corridor to the cell beyond, and stairs lead up or down a floor. A trap is never a legal step. Stepping into a portal puts you on its twin. Keys and gems are picked up where you land, and landing on a switch flips every gate of its colour. You are home when you land on your goal holding every gem. With several players, each one moves on its own and keeps its own keys, gems and switch flips, and players can pass through each other.
Roll. Every player rolls the chosen way at once, front-most first, until a wall, a locked door, a shut gate, a one-way side, the border or another player stops it. Tunnels are rolled straight through. Rolling onto a trap fails the whole roll, so nothing changes. A player picks up whatever it rolls over, and the players share keys and gems, which makes several rolling players a cooperative puzzle. A player is home the moment it rolls over its goal holding every gem, a portal throws it out of its twin still rolling, and a player that comes to rest on a switch flips its gates once the roll is over.
Gimmicks
Gimmicks are placed one type at a time in a fixed order (portals, keys and doors, switches and gates, gems, one-way sides, then traps), and the maze is solved again after each type. A type that can't be made to work is dropped on its own, with a note, and never drags the others down.
| Setting | Field | Range | What it does |
|---|---|---|---|
| Keys and doors | Keys | 0 to 6 | Each door locks a side on the way to the goal, and its key waits somewhere you can reach before that door, off the way and as far from it as possible. |
| Switches | Switches | 0 to 4 | Each switch has its own colour, and landing on it flips every gate of that colour between open and shut. Its first gate is shut across the way, and the switch waits before it. |
| Gates per switch | GatesPerSwitch | 1 to 3 | The extra gates are decoys off the way. Some start open, and the switch shuts them. |
| Shortcut doors | ShortcutDoors | on or off | Off (the default) puts every door and every switch's first gate truly in the way, so its key or switch is always needed. A maze with lots of loops may then get fewer doors, with a note. On lets doors and gates go where a player could walk around them, so the key opens a shortcut instead of the only way. |
| Gems | Gems | 0 to 8 | Every gem must be collected before a goal lets a player in. They go in dead ends first, spread out. |
| Traps | Traps | 0 to 0.25 | The fraction of free cells off the way that become traps. They sit beside the way as decoys, never on the way to anything you need. |
| One-way sides | OneWays | 0 to 24 | About half go along the way, so there's no going back, and the rest are decoys. |
| Portal pairs | Portals | 0 to 4 | Pairs the best solution actually uses, so each one shortens the way, with the two ends far apart. |
| No soft-locks | NoSoftLocks | on or off | Only accept mazes where no player can ever reach a position it can't finish from. It's off by default, because rolling mazes very often have such positions (like ice puzzles, give players a reset). Every maze reports it in CanGetStuck either way. On Roll boards it works best with Braid near 1. |
When fewer doors, gates or portals fit than you ask for, you get fewer, with a note. As a rule of thumb, about one key or switch fits per 40 cells, and Auto lowers the count to that for you.
Quality
| Setting | Field | Range | What it does |
|---|---|---|---|
| Target difficulty | TargetDifficulty | 0 to 5 | Keep trying mazes until one rates exactly this many stars. 0 accepts any rating. A miss returns the closest maze and says so in its notes. |
| Min coverage | MinCoverage | 0 to 0.9 | The best solution must pass through at least this much of the maze. Gimmicks are also placed to make the player roam as much as they can. |
| Max attempts | MaxAttempts | 1 to 500 | Under Advanced: how many mazes to try for each result before falling back to the closest one. |
| State budget | StateBudget | 1,000 to 5,000,000 | Under Advanced: how many positions the solver may explore before it reports the maze as not verified. Several rolling players need more. |
How the stars work
The rating comes from a score. On Step the score is the longest player's par, plus 5 for every real decision on the way (a fork whose wrong way runs 3 or more cells deep, so you can't see the dead end at a glance), plus 6 per key, 2 per gem or portal pair and 1 per one-way side. On Roll it is the par in rolls, plus 2 per key and 1 per gem.
| Stars | Step score | Roll score |
|---|---|---|
| 1 | up to 50 | up to 3 |
| 2 | 51 to 85 | 4 to 7 |
| 3 | 86 to 126 | 8 to 11 |
| 4 | 127 to 179 | 12 to 22 |
| 5 | above 179 | above 22 |
The bands were measured on a sweep of everyday mazes (576 Step and 288 Roll boards, on every grid, sizes 6 to 24, all six algorithms, with and without braid and holes), so each star covers about a fifth of them. A maze that isn't proven solvable gets 0 stars. Bigger boards usually rate higher, so to hit a target, pick a board size that can reach it.
Auto and presets
Auto fixes settings that fight each other and tells you what it changed. In code it is MazePresets.Tune. It:
- switches Roll to Step on a grid that can't roll (Triangle, Circular or stacked floors);
- raises Braid to 0.6 on a Roll maze below 0.5, or to 0.9 when No soft-locks is on;
- turns Weave off on a board that isn't Square;
- lowers keys and switches to about one per 40 cells;
- raises the State budget to 2,000,000 for several rolling players;
- raises Max attempts to 150 for 4 or 5 star targets, coverage targets or No soft-locks.
Presets replace every setting with a ready-made recipe. In code they are MazePresets.All, and each one's Make() gives a fresh MazeSettings.
| Preset | What you get |
|---|---|
| Classic/Square 15×15 | A plain perfect maze. |
| Classic/Circular 10 rings | The polar maze: start on the rim, finish in the centre. |
| Classic/Hex 13×13 | Honeycomb cells, six ways out of each. |
| Classic/Triangle 21×10 | Alternating triangles, three sides each. |
| Classic/Printable 30×30 | Big, with an entrance and an exit cut in the outer wall. |
| Shapes/Heart (circular) | A circular board cut to a heart. |
| Shapes/Star (hex) | A hex board cut to a star. |
| Shapes/Word: MAZE | The board is the word, and its letters are joined by bridges. |
| Shapes/Ring with rooms | A circular ring with open rooms to cross. |
| Gimmicks/Keys and doors | Three locked doors on the way, each with its key hidden before it, and gems to collect. |
| Gimmicks/Switches and gates | Shut gates on the way, opened by switches, and decoy gates the switches shut. |
| Gimmicks/Traps and one-ways (hex) | Traps beside the way, and one-way sides you can't go back through. |
| Gimmicks/Portal hop (circular) | Portal pairs and gems on a circular board. It asks for 3 pairs, and often fewer fit on this board, which its note tells you. |
| Roll/Ice board (square) | Roll until a wall stops you, rated 3 stars. |
| Roll/Co-op pair (hex) | Two players roll together, each to its own goal. |
| Players/Race of 4 (hex) | Four players and one goal in the middle, with exactly equal routes. |
| Players/Duel (circular) | Two players race to the centre. |
| Big/Weave 20×20 | Corridors crossing over and under each other. |
| Big/Tower of 3 floors | Three floors joined by stairs, with the goal on the top floor and a key and a switch on the way. |
Seeds, batches and saving
The same settings and the same seed always give the same maze. In the window, Random uses a
fresh seed for every Generate and shows the one it used. Turn Random off to type a seed and keep it. Each
maze gets its own seed, the batch's seed followed by a dash and the maze's number in the set
(-0, -1 and so on), and every maze stores its own seed, which the analysis pane can
copy.
Verified generation tries the seed itself first, then the same seed with #1, #2 and
so on added, until a maze meets the request. A maze's stored seed is the one that worked, such as
level-7-0#15, so it always reproduces that exact maze.
Batch count makes 1 to 20 mazes per click and adds them to the set. Clear on generate empties the set first, so each click replaces the batch. Save then writes Current as a MazePuzzleAsset, or Pack (all) or Pack (unique only) as a MazePuzzlePack, using the name in Save name as the suggestion.
Limits and notes
The generator never quietly gives you less than you asked for. Whenever it adjusts something, the maze's Notes say what happened, and the Generation report in the analysis pane lists them. Each note starts with the setting it is about, for example "Rooms:", "Players:" or "Target difficulty:". Common ones:
- Not enough room: rooms, loops, crossings, players, portal pairs or doors that wouldn't fit on the board.
- A gimmick dropped: no layout with it could be solved (without a soft-lock, if you asked for that) in 8 tries, so it was left out.
- A setting that doesn't apply: Roll on a board that can't roll, or Weave off Square.
- A shape that's too small for the board, so the whole board was used.
- A missed target: no maze in the attempts rated the stars you asked for, and you got the closest.
- Not verified: the solver's state budget ran out before every player got home.
Use MazeAudit.MeetsRequest(maze) to check whether a maze got everything its settings asked for. A pack's inspector can re-roll every maze in the pack that didn't (Re-roll > Fallbacks).
Generating from code
Everything the window does is available from code through MazeGenerator. Your settings object is copied and clamped first, so it is never changed.
using SimpleMaze;
var s = new MazeSettings
{
Grid = TopologySpec.Grid(GridShape.Hex, 13, 13),
Players = 2,
SharedGoal = true,
Keys = 1,
Gems = 2,
TargetDifficulty = 4,
Seed = "level-7", // empty = a fresh random seed
};
MazeData one = MazeGenerator.Generate(s); // one maze from this exact seed, solved and rated
MazeData good = MazeGenerator.GenerateVerified(s); // tries seeds until the maze meets the request
List<MazeData> pack = MazeGenerator.GenerateBatch(s, 10); // seeds level-7-0 to level-7-9, each verified
Each one has an Async version that runs on a background thread and takes a CancellationToken. In Unity, await carries on on the main thread afterwards. GenerateBatchAsync also reports progress:
var progress = new Progress<int>(n => Debug.Log($"{n} of 10 done"));
List<MazeData> mazes = await MazeGenerator.GenerateBatchAsync(s, 10, progress, cancel.Token);
A custom shape comes from ShapeMask.FromRows, ShapeMask.FromText or ShapeMask.FromPixels, which reads raw RGBA bytes, so set Shape = ShapeKind.Custom and ShapeMask on the settings:
s.Shape = ShapeKind.Custom;
s.ShapeMask = ShapeMask.FromRows(
"..##...##..",
".####.####.",
"###########",
".#########.",
"..#######..",
"....###....");
See the API Reference for everything on MazeData.
API Reference
The public surface: generating, the maze data, playing, solving, pictures, assets and the demo.
Assemblies
| Assembly and namespace | What's in it |
|---|---|
SimpleMaze | The core: generation, the maze data, the play rules, the solver, pictures and the ScriptableObjects. It references no packages, and everything except the two asset types is plain C# that also runs outside Unity. |
SimpleMaze.Demo | The 3D demo: the view, the game, the HUD, the hub, sounds and effects. It uses the Input System and Unity UI packages, and HDRP when that package is installed. |
SimpleMaze.Editor | The Maze Generator window, the inspectors and the documentation menu. Editor only. |
Everything below is in the SimpleMaze namespace unless it says SimpleMaze.Demo.
MazeGenerator
The entry point. Every method copies and clamps the settings first, so your object is never changed, and the same settings with the same seed always give the same maze. A null settings object means the defaults, and an empty seed means a fresh random one.
| Method | What it does |
|---|---|
MazeData Generate(MazeSettings settings) | One maze from this exact seed, solved and rated. |
MazeData GenerateVerified(MazeSettings settings, CancellationToken cancel = default) |
Tries the seed, then the seed with #1, #2 and so on, up to
MaxAttempts, until a maze is sound: solvable, a fair race, no soft-lock and enough coverage when
asked for, and the target stars. On a miss it returns the closest maze with a note. If it is cancelled before
the first maze, it throws OperationCanceledException. |
List<MazeData> GenerateBatch(MazeSettings settings, int count, CancellationToken cancel = default) |
A pack of verified mazes with seeds root-0, root-1 and so on. |
Task<MazeData> GenerateAsync(...), GenerateVerifiedAsync(...) | The same on a background thread. |
Task<List<MazeData>> GenerateBatchAsync(MazeSettings settings, int count, IProgress<int> progress = null, CancellationToken cancel = default) |
A batch on a background thread, reporting how many are done (1 to count). |
MazeSettings and enums
MazeSettings holds every input to the generator. Each field, its range and what it does is on the Generating page. It also has:
| Member | What it does |
|---|---|
MazeSettings Clamp() | Puts every field into its valid range, in place, and returns the same object. |
MazeSettings Clone() | A copy. |
bool HasGimmicks | True when any gimmick is asked for. |
| Enum | Values |
|---|---|
GridShape | Square, Hex, Triangle, Circular |
CarveAlgorithm | RecursiveBacktracker, Prim, Kruskal, Wilson, HuntAndKill, GrowingTree |
Endpoints | Farthest, CenterOut, OutsideIn, Fixed |
Movement | Step, Roll |
ShapeKind | None, Circle, Ring, Heart, Star, Diamond, Cross, Text, Custom |
ShapeSource | Dark, Light, Opaque (which pixels of a picture are inside) |
TopologySpec describes the board: Shape, Width,
Height, Rings, FirstRing and Floors. Build one with
TopologySpec.Grid(shape, width, height) or TopologySpec.Circle(rings, firstRing = 0),
and Clamp() returns a copy with every field in range.
MazeData
A finished maze: the board, the passages, the players and gimmicks, and the solver's verdict stamped on it. It is serializable, so it stores inside a ScriptableObject, and plain C#, so it works outside Unity too. Cells are numbered as described in Cells and slots.
| Board | |
|---|---|
TopologySpec Grid | The board this maze is carved on. |
string Seed | The seed that made it. With Settings it rebuilds this exact
maze. |
CarveAlgorithm Algorithm | How it was carved. |
int[] Masks | Each cell's open sides: bit s set means slot s is open. |
List<int> Holes | Cells removed from the board, including the ones a shape cut away. |
ShapeKind Shape, List<int> Outside | The shape it was cut to and the cells the shape cut away. |
List<MazeRoom> Rooms | The open rooms, each a list of
Cells. |
List<Tunnel> Tunnels | Weave crossings: each runs under the corridor cell
Over, joining A and C on either side of it. |
| Players | |
|---|---|
List<int> Starts, List<int> Goals | One start per player. One goal per player, or a single goal everyone heads to. |
int Start, int Goal | Player 1's start and goal. |
int PlayerCount | How many players. |
int GoalOf(int player) | The goal that player heads to. |
bool SharedGoal | Every player heads to Goals[0]. |
Movement Movement | How the players move. When Roll can't be used (a board that can't roll, or no start that rolls into the goal), the maze is stored as Step, with a note. |
| Gimmicks | |
|---|---|
List<int> Keys | Keys[k] is the cell holding key k. Taking
it opens every door of colour k for good. |
List<Door> Doors | Locked sides: Cell, Slot and the
Key colour that opens it. A door blocks both ways. |
List<Switch> Switches, List<Gate> Gates | A switch has a
Cell and a Channel (0 to 3). A gate has a Cell, a Slot,
the Channel that flips it, and StartsOpen for a decoy the switch shuts. |
List<int> Gems | Every gem must be collected before a goal lets a player in. |
List<int> Traps | Trap cells. Stepping or rolling onto one fails. |
List<OneWay> OneWays | A side you can leave Cell through
Slot, but never come back. |
List<Portal> Portals | Pairs of cells A and B.
Entering either puts you on the other. |
| The verdict | |
|---|---|
bool Solvable | The solver proved every player can reach its goal. |
int Par | The proven minimum: steps, or rolls. With several stepping players it is the total of their routes. -1 means unsolvable or not verified. |
List<int> PlayerPars | Step: each player's own par. |
bool Fair | False only for a race (Step, several players, one goal) whose players' pars differ. |
long OptimalCount, bool CountComplete | How many different best solutions
there are. When CountComplete is false the count was cut short and is a lower bound. |
bool Unique | Exactly one best solution, proven. |
bool CanGetStuck | Some reachable position can no longer reach the goal (a soft-lock). |
int Difficulty, int Score | The 1 to 5 star rating (0 when unsolvable) and the raw score it comes from. |
List<int> Solution | Player 1's positions along a best solution, start first. Step lists every cell, Roll lists where the player comes to rest after each roll. |
List<PlayerRoute> Routes | Step with several players: each player's best route
as Cells. |
List<int> Moves | Roll: the directions of the best solution. |
| Facts and notes | |
|---|---|
int DeadEnds, int Junctions, int Loops | Cells with one way in or out, cells where three or more ways meet, and independent loops (0 is a perfect maze). |
int SolutionLength, int Decisions, int RealDecisions | Steps on player 1's shortest route through the bare maze, the forks on it, and the forks whose wrong way runs 3 or more cells deep. |
float Coverage | The fraction of the maze the best solution passes through (0 to 1). |
int Shortcuts | Doors and first gates a player can walk around. |
MazeSettings Settings | The settings it was made with. |
List<string> Notes | What the generator adjusted or couldn't do. |
| Helpers | |
|---|---|
Topology Topology, int CellCount | The grid, built from
Grid on first use. |
bool IsOpen(int cell, int slot) | Is this side open? (Doors and one-way sides are open sides with a rule on them.) |
bool IsHole(int cell) | Is this cell removed from the board? |
Passages ToPassages() | An editable copy of the passages. |
Cells and slots
A cell is an index from 0 to CellCount - 1, and each side of a cell is a slot. Use
Topology.Neighbor(cell, slot) to find the cell across a side and SlotTo(cell, other)
for the side between two neighbours.
| Grid | Cell index | Slots |
|---|---|---|
| Square | y * Width + x, row 0 at the bottom | 0 N, 1 E, 2 S, 3 W |
| Hex | y * Width + x, row 0 at the bottom, odd rows shifted half a cell right |
0 E, 1 NE, 2 NW, 3 W, 4 SW, 5 SE |
| Triangle | y * Width + x, row 0 at the bottom; cell (x, y) points up when x + y is
even | 0 left, 1 right, 2 the flat side |
| Circular | 0 is the centre, then each ring counter-clockwise from angle 0 | 0 counter-clockwise, 1 clockwise, 2 inward, then one outward slot per cell in the next ring (the centre has one slot per first-ring cell) |
| Floors | floor * cells per floor + base cell | The base grid's slots, then Up and Down (the ground floor has no Down slot and the top floor no Up slot) |
Roll directions use the same numbers as the slots on Square and Hex.
Playing: MazeRules
Your game keeps a MazePlay and asks MazeRules what each move does. These are the same rules the solver counted with, so the game and the par always agree.
| MazePlay | |
|---|---|
int[] Positions | Each player's cell (-1 once home). |
bool[] Home | Which players are home. |
int[] Keys | Keys held, one bit per colour, per player (on Roll every entry is the
same, because keys are shared). Bits 16 to 19 are the switch channels flipped an odd number of times; read a
gate with MazeRules.IsGateOpen. |
int[] Gems | Gems collected, one bit per gem, per player. |
MazePlay Clone() | A copy, for undo. |
| MazeRules | What it does |
|---|---|
MazePlay NewGame(MazeData m) | Every player on its start, nothing held. |
bool TryStep(MazeData m, MazePlay play, int player, int toCell, out string reason) |
Moves one player one cell: a neighbour, the far end of a tunnel, or up or down the stairs. On success the
position is updated in place, including portals, pickups, switches and getting home. Otherwise it returns
false with the reason, such as a wall, a locked door, a shut gate, a one-way side or a trap. There is also an
overload without reason. |
RollResult Roll(MazeData m, MazePlay play, int dir, List<int>[] trails = null) |
Rolls every player in direction dir. play is not changed. The result has
After (the new position), Moved (false for a wasted roll) and
Trapped (someone rolled onto a trap, so the roll fails). The optional trails, one
list per player, receive every cell each player passes, for animation. |
bool Hint(MazeData m, MazePlay play, int player, out int move, int stateBudget = 500_000) |
The first move of a best way on from here. On Step move is the cell to step to, and on Roll
the direction. False with -1 when it's solved, that player is home, there is no way on (a soft-lock) or the
search ran out of budget. |
bool IsSolved(MazeData m, MazePlay play) | Every player is home. |
bool IsGateOpen(MazePlay play, int player, Gate gate) | Is this gate open for that player now? |
bool CanRoll(GridShape shape), bool CanRoll(TopologySpec grid) | Roll is defined on Square and Hex, on one floor. |
int DirectionCount(GridShape shape) | Roll directions: 4 on Square, 6 on Hex, 0 elsewhere. |
Vec2 Direction(Topology t, int dir) | The way a roll direction points, in board units. |
bool IsLegalStep(MazeData m, int from, int to, out string reason) | Can a player step between two cells of the bare maze, ignoring gimmicks? |
bool Validate(MazeData m, out string error) | Is the data well-formed? Both sides of every opening agree, holes are sealed, every start, goal and gimmick sits on a playable cell, nothing overlaps, every gate has its switch, every tunnel runs straight under a corridor, and the stored route walks. |
const int SwitchShift = 16 | Where the switch flips sit in
MazePlay.Keys. |
Solving and rating
| MazeSolver | What it does |
|---|---|
MazeVerdict Solve(MazeData m, int stateBudget = 500_000) | Solves the maze as it stands
and returns a MazeVerdict: Solvable, Par, PlayerPars,
Count, CountComplete, Unique, CanGetStuck,
Explored, Path, Routes and Moves. |
MazeVerdict SolveFrom(MazeData m, MazePlay play, int player = 0, int stateBudget = 500_000) |
Solves on from a position in play. Par is the moves still needed. On Step it solves for that
player alone, and Moves holds the slot of each step. |
int[] StepParsFromEveryCell(MazeData m, int player = 0, int stateBudget = 500_000) |
On Step, that player's par to its goal from every cell, holding nothing, with all the gimmicks (-1 for a hole or no way home). Null when the budget ran out. |
const long CountCap | The best-solution count stops at 1,000,000,000. |
| Rating and checks | What it does |
|---|---|
MazeDifficulty.Score(MazeData m), MazeDifficulty.Stars(MazeData m) | The raw score and the 1 to 5 stars. See How the stars work. |
MazeAudit.Verify(MazeData m, out string before, out string after) | Solves a stored maze again and restamps its verdict. True when anything changed. |
MazeAudit.Reroll(MazeData m) | A fresh maze from the same settings and a new seed, or null for a maze with no settings. |
MazeAudit.MeetsRequest(MazeData m) | Did the maze get everything its settings asked for: solvable, the target stars, the coverage, no soft-lock when asked, and a fair race? |
MazeAudit.Short(MazeData m) | One line, such as "par 63 steps, 3 of 5 stars, unique". |
Presets and Auto
| Member | What it does |
|---|---|
MazePresets.All | Every preset, in menu order. Each MazePreset has a
Name (its menu path), a Description, and Make(), which returns a fresh
MazeSettings. |
MazePresets.Tune(MazeSettings s, List<string> changes) | What the Auto button does: a copy with settings that fight each other fixed, and a line per change. |
Pictures
MazeExport draws a maze in plain C#, so it works in a build, in the editor or on a server.
| Member | What it does |
|---|---|
byte[] Png(MazeData m, ExportOptions options = null) | A PNG file's bytes. |
string Svg(MazeData m, ExportOptions options = null) | An SVG document. |
byte[] Pixels(MazeData m, ExportOptions options, out int width, out int height) | Raw RGBA bytes, top row first. |
Png and Pixels with a MazeSketch | A picture of a given moment, for example part way through the solution. |
int FitSize(MazeData m, MazeSketch sketch, ExportOptions options, int maxWidth, int maxHeight) |
The Size that fits a box. |
| ExportOptions | Default | What it does |
|---|---|---|
Style | Printable | Printable (black on white with the start and goal), AnswerKey (with the solution drawn in), or AsShown (colour, every gimmick, the solution and a legend). |
Size | 2048 | The maze's longer side in pixels (64 to 8192). |
WallWidth | 0.12 | Wall thickness as a fraction of a cell (0.02 to 0.4). |
Margin | 0.8 | The white border, in cells (0 to 4). |
Legend | true | AsShown: draw the legend under the maze. |
Floor | -1 | Stacked boards: -1 draws every floor side by side, and 0 or more just that floor. |
MazeSketch is the drawing model behind the pictures. MazeSketch.Build(maze, play,
focus, trails) sketches a position, MazeSketch.Frames(maze) steps through the stored solution
move by move, and MazeSketch.Describe(maze, cell, play) says what is in a cell in plain words.
Shapes
A ShapeMask is a black and white bitmap (Width, Height,
Pixels, row 0 at the top). Set it on MazeSettings.ShapeMask with
Shape = ShapeKind.Custom.
| Method | What it does |
|---|---|
ShapeMask.FromRows(params string[] rows) | From rows of text: # (or any
character other than a space or .) is inside. |
ShapeMask.FromText(string text) | Text in the built-in 5 by 7 font. |
ShapeMask.FromPixels(byte[] rgba, int width, int height, bool bottomRowFirst, ShapeSource source, float threshold = 0.5f, int maxSide = 128) |
From a picture's RGBA bytes. Pass bottomRowFirst true for Unity's
GetPixels32. The result is cropped to the shape and shrunk to at most maxSide
across, and is null when nothing counts as inside. |
Topology
A Topology is a grid as a graph of cells and slots. Get one from
MazeData.Topology or Topology.Create(spec).
| Member | What it does |
|---|---|
int CellCount, int SlotCount(int cell) | How many cells, and how many sides a cell has. |
int Neighbor(int cell, int slot) | The cell across a side, or -1 at the border. |
int OppositeSlot(int cell, int slot), int SlotTo(int cell, int other) | The neighbour's side that faces back, and the side between two cells (-1 when they don't touch). |
Vec2 Center(int cell) | A cell's centre in board units (x right, y up). |
Edge EdgeOf(int cell, int slot) | The wall on that side: a segment from
A to B, or an arc when Arc is true. |
int CellAt(Vec2 p) | The cell under a point, or -1. |
void Bounds(out Vec2 min, out Vec2 max) | The board's bounds, for framing. |
(int x, int y) Coord(int cell) | Column and row, or index in ring and ring on a circular board. |
A board with floors is a LayeredTopology, which adds Base,
Floors, FloorOf(cell), BaseCell(cell), At(floor, baseCell),
UpSlot(cell), DownSlot(cell) and IsStairSlot(cell, slot).
ScriptableObjects
| Type | What it holds |
|---|---|
MazePuzzleAsset | One maze: Maze and Label, plus
shortcuts to its verdict (Shape, Floors, Movement,
PlayerCount, Solvable, IsUnique, OptimalCount,
Par, Difficulty, Seed) and Set(data, label). |
MazePuzzlePack | An ordered list of mazes: PackName,
Mazes, Count, Get(index) (null when out of range),
Set, Add, Replace(index, data) and Clear(). |
PuzzleAssets | CreatePuzzle(data, label) and
CreatePack(mazes, packName) make new instances in memory, which you can save with
AssetDatabase.CreateAsset in the editor or use at runtime. |
IPuzzleProvider | Both asset types implement it: AppendTo(List<MazeData>
levels) adds their mazes in order, so one list in the inspector can take either. |
Both are created by the Maze Generator's Save menu, not by an empty right-click Create.
Demo components
These are in SimpleMaze.Demo. The Demos page explains how they fit together.
| Type | What it does |
|---|---|
MazeDemo | The one component the demo scene needs. It builds the view, game, HUD,
hub, sounds and effects in code and runs the flow. Fields: Camera, Pitch,
HdrpLook, UiThroughCamera and a level pack per mode. Methods include
Setup(), PlayEndless(mode, grid, stars), PlayLevel(mode, index),
Generate(mode, settings), Begin(maze), NextGame() and
BackToHub(). |
MazeGame | Plays a maze on a MazeView3D: input, movement, every gimmick's
animation, moves against par, undo, restart, hints, auto-solve, players and races, stairs and the camera.
Start a game with Begin(maze), drive it with Push(Vector2 screen) or the keyboard,
and listen to Won, Cue, Moment and Message. |
MazeView3D | Builds a maze in 3D from primitives and meshes, on any grid, with no
prefabs: Build(maze), CellPosition, StandPosition,
FocusFloor, FrameCamera, and lists of the objects it made (keys, doors, gates,
players and so on). |
MazeHud | The in-game HUD and win panel, in uGUI with the built-in font. |
MazeHub | The front screen: the welcome panel, the mode cards, and each mode's Endless and Level Pack setup. |
MazeMode, MazeModes | The five demo modes, their names, colours, sample mazes and the Endless recipe for each mode, grid and star count. |
MazeProgress | Each pack level's best moves, kept in PlayerPrefs:
Best, Record, Clear and NameOf. |
MazeBeeper | Sounds made in code for every MazeCue, with no audio
files. |
MazeEffects | Small effects (sparkles, dust, confetti) from a pool of primitives, for
every MazeMoment. |
DemoLook, DemoHdrp | The demo's colours and materials for the active render pipeline, and the HDRP volume that gives it the same look. |
Building blocks
The pieces the generator is made of are public too, for anyone who wants to build their own pipeline. Most projects never need them.
| Type | What it does |
|---|---|
MazeBuilder | Build(settings, seed) makes one maze from clamped settings
and an exact seed, which is what every MazeGenerator method calls. |
Passages | The open sides of every cell over a topology, plus holes, with
Open, Close, IsOpen, Degree and
OnRim. |
Carver | Carves a perfect maze through every playable cell with a
CarveAlgorithm. |
HolePlacer, BoardShape, Weave, GimmickPlacer | Holes, shapes, crossings and gimmicks. |
GraphSearch | Breadth-first search, shortest paths and connectivity over passages. |
Rng | The small deterministic random generator, seeded from a string. |
Demos
What the 3D demo does, how it is put together, and how to use its pieces in your own game.
The demo scene
Open Assets/SimpleMaze/Scenes/SimpleMazeDemo.unity and press Play. The scene holds a camera, a sun, and one object called Simple Maze Demo with the MazeDemo component and the five level packs assigned. Everything else, from the maze to the menus, is built in code when the scene starts, so there are no prefabs, models, textures or audio files to look after.
The demo reads the keyboard through the Input System package. If nothing happens when you press keys, set Project Settings > Player > Active Input Handling to Input System Package or Both.
One component: MazeDemo
Add MazeDemo to any object in a scene with a camera and press Play. When it starts, it builds:
- a Maze child with MazeView3D, which draws the maze, and MazeGame, which plays it;
- the HUD (MazeHud) and the hub (MazeHub), both in uGUI with Unity's built-in font;
- MazeBeeper for the sounds and MazeEffects for the effects.
It also sets the camera up for the maze, giving it a plain background colour and a 40 degree field of view, and adds a sun if the scene has no light.
| Field | What it does |
|---|---|
Camera | The camera that shows the maze. Empty uses the main camera, and if there is none, one is made. |
UiThroughCamera | Draw the HUD and the hub through the camera instead of over the screen, so the camera's own picture includes them. |
Pitch | How steeply the camera looks down at the maze, in degrees (58 by default). |
HdrpLook | HDRP only: give the demo the same look as on the other pipelines. Turn it off to use your own volumes and lights. |
ClassicPack, GimmicksPack, RollPack, TowerPack,
RacePack | The level pack for each mode. Any can be empty, and that mode's Level Pack tab
then says so. GimmicksPack is the Dungeon mode's pack. |
The flow
Welcome
The first screen says what Simple Maze is and where to make your own mazes, with the publisher's contact at the bottom. Let's play goes on, and About on the mode screen brings it back.
Pick a mode
Five cards, each with a picture of a small maze of that mode drawn without its solution.
Endless or Level Pack
Endless lets you pick a grid and a difficulty from 1 to 5 stars, then builds a new maze to that order on a background thread. Level Pack shows the mode's levels as numbered buttons: grey still to do, the mode's colour solved, and gold solved on par. Play starts the first level you haven't solved. The demo remembers your choices for each mode while it runs.
Play
The HUD shows the maze, its stars, your moves against par, and whose turn it is. A button bar has Undo, Restart, Hint, Solve, Stairs (when you stand on stairs) and Menu.
Win
After a short celebration a panel shows your moves, the par and a rating, with Replay, Menu and Next (or press Enter). In Endless, Next builds a new maze to the same order, and in a pack it goes to the next level, then back to the pack after the last one. Menu or Esc returns to the mode cards at any time.
The five modes
| Mode | Grids | Endless mazes |
|---|---|---|
| Classic | all four | Plain mazes. The board grows with the stars. |
Dungeon (MazeMode.Gimmicks in code) | all four | Keys and doors from 1 star, gems from 2, a switch and traps from 3, a portal pair and one-way sides from 4, and a second switch at 5. |
| Roll | Square and Hex | The player rolls until something stops it. Braid is set so rolling has loops to use. |
| Tower | all four | 2 floors joined by stairs, or 3 floors from 3 stars, from the rim to the centre. |
| Race | all four | Two players take turns heading for one goal from fair starts, and the first one home wins. |
On a circular board, Endless mazes run from the rim to the centre. The recipes are in MazeModes.Endless, and each one goes through MazePresets.Tune and verified generation, so the maze you get rates the stars you picked whenever the board can reach them.
The level packs
The demo ships with 50 levels in five packs in Assets/SimpleMaze/Generated. Each was made by the generator from a fixed seed, and each was played through to par to check it. They are ordinary MazePuzzlePack assets, so you can open them in the inspector, read their analysis, or export them as printable pictures.
| Pack | Levels | What's in it |
|---|---|---|
| Classic | 12 | Every grid, from a 6 by 6 square up to an 18 by 18 square, including a hex star and a circular heart. Rated 1 to 5 stars. |
| Dungeon | 12 | One new idea per level for the first seven (a key, gems, a switch, a portal, one-way sides, traps and weave crossings), then mixes of them, ending with a 14 by 14 maze that has keys, gems, traps, a switch, a portal and one-way sides. |
| Roll | 10 | Square and hex boards from 6 by 6 to 10 by 10, the last one with gems and a portal. |
| Tower | 8 | 2 to 4 floors on every grid, some with a key or gems. |
| Race | 8 | 2 to 4 players on every grid, each race with equal routes for everyone. |
Your own packs: make mazes in the Maze Generator, choose Save > Pack (all), and drag the pack into a slot on the MazeDemo component.
Progress: each level's best number of moves is saved in PlayerPrefs under
SimpleMaze.Demo.Best.<pack name>.<level>, so it stays on that device. Races are not
recorded, and neither is a level the auto-solver helped with. The win panel says "Level complete!", "New best"
or "Your best is" accordingly.
Playing
The keys are listed at the bottom of the screen while you play, and in the Quick Start. A few details:
- Moving: a key moves you toward the side whose direction on screen is closest to it, so the controls feel right from any camera angle and on any grid. Holding a key keeps walking, and a key pressed during a move is kept for the next one.
- Moves count steps, or rolls that moved someone. Bumping a wall, a roll that goes nowhere and a roll onto a trap don't count.
- Undo takes back one move at a time and Restart puts the whole maze back to the start.
- Hint lays a gold arrow toward the next move of a best way on, worked out from where you are.
- Solve (or F) lets the solver play the rest from where you are. Press it again, or F or R, to stop.
- Several players: in a race the turn passes after every step, and the board shows the maze as the player whose turn it is sees it (their keys, doors and gates). When each player has its own goal, Tab or 1 to 4 switches player. On Roll every roll moves everyone.
- The view: the camera frames the board in the space the HUD leaves free. You can turn it, tilt it and zoom, and when zoomed in it follows the player. On a maze with floors it follows the player's floor and hides the floors above.
- The rating on the win panel is 3 pips on par, 2 a little over (up to 2 moves, or a quarter of the par on bigger mazes) and 1 beyond that. Races show who won instead.
Sounds and effects
MazeBeeper makes every sound in code when the demo starts: steps (three variants, so walking doesn't sound robotic), a bump, a roll, the stairs, a key, a gem, the goal opening, a door, a switch with its gates, a portal, spikes, a "no way" buzz, a hint, undo and restart, the win, and a button blip. M mutes it.
MazeEffects adds small effects made of a pool of tiny shapes: sparkles for a key or a gem in its colour, a gold ring when the goal opens, dust at a bump, a purple swirl at both ends of a portal, sparks off spikes, and confetti for the win. Nothing is created or destroyed while you play, and because the effects use the demo's ordinary materials, they look the same in every render pipeline.
Render pipelines
The demo works in URP (recommended), HDRP and the Built-in pipeline without any changes. Its materials use the active pipeline's default lit shader.
HDRP lights in physical units, sets the exposure on its own and takes its ambient light from a sky, so the demo would come out dark and tinted. With HdrpLook on, DemoHdrp adds a global volume with a fixed exposure, no tone mapping or bloom, and a gradient sky in the demo's colours, clears the camera to the demo's background colour, and sets the sun in lux. That code only compiles when the HDRP package is installed.
Using the pieces yourself
The demo components work on their own too. The smallest setup is one MazeGame, which adds its MazeView3D automatically:
using SimpleMaze;
using SimpleMaze.Demo;
var game = new GameObject("Maze").AddComponent<MazeGame>();
game.Begin(maze); // build it and start a game
game.View.FrameCamera(Camera.main, -1); // -1 frames every floor
game.Won += g => Debug.Log($"Solved in {g.Moves}, par {g.Par}");
- Input: MazeGame reads the keyboard itself. For touch or a gamepad,
call
Push(Vector2 screen)with the direction on screen, andUndo(),Restart(),ShowHint(),StartAutoSolve()andTakeStairs(way)for the rest. - The camera is yours unless you turn on
CameraControls. OnlyFollowFloormoves it, on mazes with floors. - Events:
Wonfires once when the game is won,Cuefor every moment worth a sound,Momentfor the same moments with a position and colour for effects, andMessagefor short notes such as "No stairs here". - A HUD: add a MazeHud and set its
Game, or readMoves,Par,SolvedandPlayerinto your own UI. - The look: the colours, materials and shapes are in DemoLook.cs, and
the timing is on MazeGame (
StepTime,RollTime,PortalTime,HopandSolvePause). - Without the 3D view: keep a MazePlay and use MazeRules directly, as in the Quick Start.
Troubleshooting
What a note means, why a maze came out the way it did, and what to change.
Reading the notes
Whenever the generator adjusts something, it writes a note on the maze instead of quietly giving you less. The notes are listed under Generation report in the analysis pane, and in MazeData.Notes from code. Each one starts with what it is about, such as "Keys:", "Players:" or "Target difficulty:", and the sections below explain the common ones. MazeAudit.MeetsRequest(maze) tells you whether a maze got everything its settings asked for, and a pack's Re-roll > Fallbacks replaces every maze that didn't.
"Target difficulty: 4 stars not found in 60 tries; closest was 3."
The stars come mostly from the par, so the board size sets what a maze can rate. A small board can't reach 5 stars and a huge one rarely rates 1. To hit a target:
- make the board bigger for more stars, or smaller for fewer;
- try another algorithm: long corridors make long routes, so on a 15 by 15 square board Recursive Backtracker mazes average 4.5 stars, Hunt and Kill 4.0, and Prim 2.9;
- raise Max attempts under Advanced, or press Auto, which raises it to 150 for 4 and 5 star targets.
The rules behind the stars are on the Generating page.
Fewer gimmicks than I asked for
Gimmicks are only placed where they do their job, and the generator says when it couldn't fit them all:
- "Keys: the maze's loops (or portals) leave room for 1 of 3 doors that can't be walked around." A door only counts when it's truly in the way, and loops give the player ways round it. Lower Braid and Extra loops, or turn on Shortcut doors to allow doors that can be walked around. The same goes for switches and their gates.
- "Keys: the way to the goal is too short for 4 doors." Use a bigger board, or Farthest endpoints for a longer way.
- "Portals: room for 1 of 3 pairs the solution would actually use." Each portal must shorten the way, so it needs a long way with loops. A bigger board or more loops allows more.
- "Gems: room for 2 of 5." Gems go off the way, in dead ends first, and each one must be collectable together with the others. A small board has few such spots, and on a rolling board a gem also has to be reachable without getting stuck. Use a bigger board.
- "Gimmicks: no layout with traps was solvable in 8 tries; dropped the traps." That type couldn't be made to work with everything else on this board, so it was left out and the rest kept.
As a rule of thumb about one key or switch fits per 40 cells, and Auto lowers the counts to that.
"Can get stuck"
The analysis pane warns when a player can reach a position it can no longer finish from. One-way sides, switches and rolling can all cause it. Rolling mazes have it very often, just like ice puzzles, so it is reported rather than forbidden by default. You can:
- turn on No soft-locks so only mazes without it are accepted (on Roll, raise Braid close to 1 as well, or press Auto);
- or keep the maze and give players a Restart and Undo, as the demo does.
"Not verified" or "the state budget ran out"
The solver explores every position the players can reach, counting keys, gems and switch flips. Several rolling players, many keys and gems, or a very large board can need more positions than the budget allows, and then the maze is marked not verified instead of guessed. Raise State budget under Advanced (up to 5,000,000), or use a smaller board, fewer players or fewer gimmicks. Several rolling players need the most, and Auto raises the budget to 2,000,000 for them.
The race isn't fair
"Players: the gimmicks leave no other start exactly as far from the goal as player 1's (pars 36, 32), so the race isn't fair." On Step, every player in a race needs the same par, and with gimmicks the other players' starts are picked after the gimmicks are placed, using each cell's real par. On some boards no such cells exist. Verified generation, which the window always uses, skips these mazes and tries the next seed, so you only see this note from MazeGenerator.Generate or after every attempt missed. A bigger board or fewer gimmicks helps.
Roll or weave was ignored
- "Movement: Roll needs a Square or Hex board" or "Roll keeps to one floor": rolling needs straight lines, which Triangle and Circular boards and stacked floors don't have, so those mazes use Step.
- "Movement: no start could roll into the goal on this maze": rolling needs loops. Raise Braid (Auto sets at least 0.6).
- "Weave: crossings need a Square board": weave crossings only exist on Square boards.
My shape looks wrong
- "Shape: Heart leaves only 20 cells on this board, which is too few, so the whole board was used." Use a bigger board or a higher Fill.
- "Shape: 4 separate pieces; joined them with 3 bridges." The shape fell apart, for example into the letters of a word, and was joined up. Turn off Connect pieces to keep only the largest piece instead.
- Text: give it a wide board, about 6 cells across per letter and at least 9 rows. On a Square board the font's diagonal strokes only touch at their corners, so a letter like M can lose its diagonals and read as H. Check the preview, and try another word or grid if it matters.
- "Nothing in this image counts as inside." Switch Inside is between Dark, Light and Opaque, or move the threshold slider.
Generation feels slow
Plain mazes are fast at any size. Measured on a desktop PC outside Unity (your machine and Unity's runtime will differ):
| Maze | Time |
|---|---|
| Square 64 by 64, or circular with 40 rings | under 10 ms |
| Square 64 by 64 with 8 floors | about 30 ms |
| Hex 13 by 13, a race of 4 with a key and gems, verified | under 10 ms |
| Square 30 by 30 with 6 keys, 8 gems, 4 switches and 4 portals | 0.5 to 1.4 s |
| Square 14 by 14, 4 rolling players with gems | 1.3 to 4.7 s |
The costly part is solving, which grows with every key, gem and switch (each one multiplies the positions to search) and most of all with several rolling players, who are solved together. Every gimmick type is solved again after it is placed, up to 8 times. Generation runs on a background thread in the window and with the Async methods, so it never freezes the editor or your game, and it can be cancelled.
The demo ignores the keyboard or the mouse
The demo uses the Input System package. Set Project Settings > Player > Active Input Handling to Input System Package or Both. The hub and the HUD make an EventSystem with the Input System's UI module when the scene has none. If your scene already has an EventSystem, its input module has to match your input handling.
HDRP looks dark, or ignores my volumes
Out of the box HDRP uses physical light units, automatic exposure and sky lighting, which would make the demo dark and tinted. MazeDemo.HdrpLook (on by default) adds a global volume at priority 10 with a fixed exposure and a gradient sky, and sets the sun in lux, so the demo looks the same as on the other pipelines. If you want your own volumes and lights to take over, turn HdrpLook off.
Resetting the demo's progress
The demo saves each pack level's best moves in PlayerPrefs, under
SimpleMaze.Demo.Best.<pack name>.<level>. Call
MazeProgress.Clear(packName, levelCount) to forget a pack, or delete those keys. A pack's
progress is filed under its pack name, so renaming a pack starts it fresh.
Seeds and versions
The same settings and seed always give the same maze within a version of Simple Maze. A future version that
improves the generator may change what a seed produces. Saved assets store the maze itself, not just its seed,
so your saved mazes and packs never change. Verified generation stores the seed that actually worked, such as
level-7#3, so copying a maze's own seed always rebuilds that exact maze.
Console messages
- "[Simple Maze] Documentation page not found": the documentation menu looks for these pages
in a folder whose path contains
SimpleMazeandDocumentation. Keep the Documentation folder inside the Simple Maze folder. - "[Simple Maze] Generation failed": something went wrong on the generation thread. Please send the message with your settings and seed.
Still stuck
Email livingfailuregames@gmail.com with your Unity version and, for a maze-specific question, its seed and settings. Copy seed and Copy solution in the analysis pane make that easy, and a seed rebuilds the maze exactly.
About
What this is, who made it, and how to get help.
About Simple Maze
Simple Maze is a maze generator for Unity, made by Living Failure. It builds mazes on square, hex, triangle and circular grids, with keys and doors, switches and gates, gems, traps, one-way sides, portals, rolling players and up to 8 floors. Then it solves each maze exactly, stamps it with its proven par and a 1 to 5 star rating, and stores it as a ScriptableObject or exports it as a picture. A complete 3D demo, built entirely in code, is included.
Version 1.0, made and tested with Unity 6000.6.
What's in the package
| Folder | What it holds |
|---|---|
Runtime | The core (generation, the maze data, the play rules, the solver, pictures and
the ScriptableObjects) and, in Runtime/Demo, the 3D demo. |
Editor | The Maze Generator window, the inspectors and the documentation menu. |
Scenes | The demo scene. |
Generated | The demo's five level packs. |
Documentation | These pages, which open offline from Window > Living Failure > Simple Maze > Documentation. |
Design principles
- Solve exactly. The par and the number of best solutions come from searching every position the players can reach, not from an estimate.
- Prove the gimmicks together. Keys, gates, gems, portals and the rest are solved with the maze, so a door that looks necessary really is.
- Be fair. Race starts are picked so every player's par is the same, gimmicks included.
- Be honest. When a request can't be met, the maze's notes say so. Nothing is quietly downgraded, and the store page claims nothing the solver can't back up.
- One set of rules. The generator, the solver and the play rules your game uses all move players the same way, so your game and the par always agree.
- No bloat. The core has no package dependencies, and the demo and the editor tools live in their own assemblies. The demo needs no prefabs, models, textures or audio files.
More from Living Failure
Simple Klotski: sliding-block puzzles in five modes (Gridlock, Klotski, Clear, Clear 3D and Clear Snakes), each board solved exactly and stamped with its proven par.
Simple States: switch-and-state logic puzzles in four modes: Classic combinations, Ordered chains with traps, Lights Out with paintable pattern goals, and the three-state Trilight grid.
Simple Connect: a connection-puzzle toolkit where you draw the pairs (square and hex), rotate the pipes, or rotate a whole network back together.
Simple Hamiltonian: a cover-all, single-path puzzle generator (square and hex), where the solution is one unbroken line that visits every tile once.
All on the Unity Asset Store.
Support
Questions, bugs and ideas are welcome at livingfailuregames@gmail.com. Please include your Unity version and, for anything about a particular maze, its seed and settings, since a seed rebuilds the maze exactly. Corrections to these pages are welcome too.
License
Distributed under the Unity Asset Store End User License Agreement. Use it in your games and tools, but don't resell or redistribute the source as an asset.