Building a Procedural Maze Generator in UE5 Blueprints — Part 1
Creating the Grid and Valid Neighbor System
If you have ever tried following an older Unreal maze tutorial, you probably noticed the same thing I did: the core idea is good, but the Blueprint setup can get messy fast. There are extra arrays, overcomplicated macros, and a lot of wiring that makes the logic harder to understand than it needs to be.
This version rebuilds the system cleanly from scratch in Unreal Engine 5 Blueprints.
In Part 1, we are going to build the foundation:
- a grid of maze cells
- a helper function to convert X/Y into an array index
- a function that finds valid neighboring cells
This is the part that makes the maze “think.”
What We Are Building
The maze will use a grid of cells. Each cell stores:
- its X position
- its Y position
- whether it has been visited
- whether it is currently a wall
Later, the generator will carve paths by moving through this grid two cells at a time.
Before You Start
Create a new Actor Blueprint called:
BP_MazeGenerator
Also create a Blueprint Structure called:
MazeCell
Inside MazeCell, add these variables:
X— IntegerY— IntegerVisited— BooleanIsWall— Boolean
Screenshot Placeholder
[Screenshot: MazeCell struct with X, Y, Visited, IsWall]
Step 1 — Add the Main Variables
Open BP_MazeGenerator and add these variables:
Grid Settings
GridWidth— IntegerGridHeight— IntegerTileSize— Float
Set these to Instance Editable so you can change them in the Details panel later.
Grid Data
Grid— MazeCell Array
Make sure Grid is an array of MazeCell, not a single struct.
Screenshot Placeholder
[Screenshot: BP_MazeGenerator variables panel showing GridWidth, GridHeight, TileSize, and Grid as MazeCell Array]
Step 2 — Create the InitializeGrid Function
This function creates every cell in the maze and stores it in the Grid array.
Create a new function called:
InitializeGrid
Step 2.1 — Clear the Grid
Inside InitializeGrid:
- drag the
Gridvariable into the graph as Get - drag off it and add the node:
Clear
This ensures the array starts empty each time the grid is rebuilt.
Screenshot Placeholder
[Screenshot: InitializeGrid → Grid → Clear]
Step 2.2 — Add Nested For Loops
We need one loop for rows and one loop for columns.
Outer loop
Add a ForLoop for Y.
- First Index =
0 - Last Index =
GridHeight - 1
Inner loop
From the outer loop’s Loop Body, add another ForLoop for X.
- First Index =
0 - Last Index =
GridWidth - 1
This gives us a full 2D grid.
Screenshot Placeholder
[Screenshot: Nested ForLoop setup inside InitializeGrid]
Step 2.3 — Create a MazeCell
Inside the inner loop:
- right-click and add:
Make MazeCell
Set it like this:
X = inner loop IndexY = outer loop IndexVisited = falseIsWall = true
That means every cell starts as an unvisited wall.
Screenshot Placeholder
[Screenshot: Make MazeCell with X, Y, Visited false, IsWall true]
Step 2.4 — Add the Cell to the Grid Array
Still inside the inner loop:
- drag
Gridinto the graph as Get - drag off it and add:
Add
Connect:
Target Array = GridItem = Make MazeCell
At the end of this function, the Grid array will contain every cell in the maze.
Screenshot Placeholder
[Screenshot: Make MazeCell connected into Add node targeting Grid]
Step 3 — Create the GetIndex Function
Because the maze is stored in a one-dimensional array, we need a way to convert grid coordinates into an array index.
Create a new function called:
GetIndex
Inputs:
X— IntegerY— Integer
Output:
Index— Integer
Step 3.1 — Build the Formula
Use this formula:
Index = X + (Y * GridWidth)
In Blueprint:
- multiply
Y * GridWidth - add
X - connect the result to the Return Node
Screenshot Placeholder
[Screenshot: GetIndex function using X + (Y * GridWidth)]
Why This Matters
If your grid width is 21:
(0,0)becomes 0(1,0)becomes 1(0,1)becomes 21(1,1)becomes 22
This is how we look up the correct cell later.
Step 4 — Create GetValidNeighbors
This is the most important function in this part of the tutorial.
Its job is simple:
- take the current cell
- check the four directions
- return only the valid neighboring cells
Create a new function called:
GetValidNeighbors
Input:
CurrentIndex— Integer
Output:
Neighbors— Integer Array
Step 4.1 — Add Local Variables
Inside the function, create these Local Variables:
CurrentX— IntegerCurrentY— IntegerTestX— IntegerTestY— IntegerTestIndex— IntegerLocalNeighbors— Integer Array
Use local variables here. Do not make these regular Blueprint variables.
Screenshot Placeholder
[Screenshot: Local variables inside GetValidNeighbors]
Step 4.2 — Read the Current Cell
We need to get the current MazeCell from the Grid array.
Do this:
- drag
Gridinto the graph as Get - drag off it and choose:
Get (a copy) - plug
CurrentIndexinto the Index pin - drag off the result and add:
Break MazeCell
Now store the values:
X → Set CurrentXY → Set CurrentY
Screenshot Placeholder
[Screenshot: Grid → Get (a copy) → Break MazeCell → Set CurrentX / Set CurrentY]
Step 4.3 — Add a Sequence Node
After Set CurrentY, add a Sequence node.
Click Add pin until it has:
- Then 0
- Then 1
- Then 2
- Then 3
We will use those for:
- Left
- Right
- Up
- Down
Screenshot Placeholder
[Screenshot: Sequence node with Then 0 through Then 3]
Step 5 — Build the Left Direction
For each direction, we:
- calculate test coordinates
- check bounds
- convert to index
- check if visited
- add to LocalNeighbors if valid
Start with LEFT.
Step 5.1 — Set Test Coordinates for LEFT
For LEFT:
TestX = CurrentX - 2TestY = CurrentY
That means:
- drag in
CurrentX - subtract
2 - connect into
Set TestX
Then:
- drag in
CurrentY - connect into
Set TestY
The white exec flow should be:
Sequence Then 0 → Set TestX → Set TestY
Screenshot Placeholder
[Screenshot: LEFT setup using CurrentX - 2 and CurrentY]
Step 5.2 — Add the Bounds Check
We only want cells inside the valid maze area.
Build these four checks:
TestX > 0TestX < GridWidth - 1TestY > 0TestY < GridHeight - 1
Combine them using AND nodes, then plug the final result into a Branch.
Screenshot Placeholder
[Screenshot: Bounds check using four comparisons and AND nodes]
Step 5.3 — Convert to Index
On the True pin of that Branch:
- call
GetIndex - connect
X = TestX - connect
Y = TestY
Then store the result in:
Set TestIndex
Screenshot Placeholder
[Screenshot: Branch True → GetIndex → Set TestIndex]
Step 5.4 — Check If the Tile Has Been Visited
Now test the actual cell:
- drag
Gridinto the graph as Get - drag off and choose:
Get (a copy) - use
TestIndexas the Index - add
Break MazeCell
Then:
- take
Visited - pass it through a
NOTnode - plug that into another
Branch
If the Branch is True, the tile is a valid neighbor.
Screenshot Placeholder
[Screenshot: Grid → Get (a copy) using TestIndex → Break MazeCell → NOT Visited → Branch]
Step 5.5 — Add the Valid Neighbor
On the True pin of that second Branch:
- drag
LocalNeighborsinto the graph as Get - drag off it and add:
Add
Connect:
Target Array = LocalNeighborsItem = TestIndex
Screenshot Placeholder
[Screenshot: Add node using LocalNeighbors and TestIndex]
Step 6 — Duplicate for the Other Three Directions
Once LEFT is working, duplicate the whole direction block three times and change only the coordinate math.
RIGHT
TestX = CurrentX + 2TestY = CurrentY
Connect:
Sequence Then 1 → RIGHT Set TestX
Screenshot Placeholder
[Screenshot: RIGHT direction block]
UP
TestX = CurrentXTestY = CurrentY - 2
Connect:
Sequence Then 2 → UP Set TestX
Screenshot Placeholder
[Screenshot: UP direction block]
DOWN
TestX = CurrentXTestY = CurrentY + 2
Connect:
Sequence Then 3 → DOWN Set TestX
Screenshot Placeholder
[Screenshot: DOWN direction block]
Step 7 — Add the Return Node
If your Return Node is missing, right-click in the function graph and add:
Return Node
Now connect:
LocalNeighborsinto theNeighborspin on the Return Node
This means the function returns every valid neighbor it found.
Screenshot Placeholder
[Screenshot: Return Node with LocalNeighbors connected to Neighbors]
Final Execution Flow for GetValidNeighbors
Your function should now flow like this:
GetValidNeighbors
→ Set CurrentX
→ Set CurrentY
→ Sequence
Then 0 → LEFT
Then 1 → RIGHT
Then 2 → UP
Then 3 → DOWN
→ Return Node
Screenshot Placeholder
[Screenshot: Full GetValidNeighbors function overview]
What You Have Built So Far
At this point, your maze system can now:
- create a full grid of wall cells
- convert X/Y into array positions
- check all four directions
- return only valid, unvisited neighboring cells
That is the full foundation for procedural maze generation.
In the next part, we will use this system to generate the actual maze path using stack-based backtracking.
Beginner Notes
If you are new to Blueprints, here are a few things worth remembering:
“Get” vs “Get (a copy)”
When working with arrays, Unreal often uses the node name:
Get (a copy)
That simply means:
- get one element from an array at a specific index
So when you see instructions like:
- “Get the cell from Grid using CurrentIndex”
what you actually do in Unreal is:
- drag
Gridinto the graph as Get - drag off it and choose Get (a copy)
- plug in the index
Local Variables
Use Local Variables inside functions for temporary values like:
- CurrentX
- CurrentY
- TestX
- TestY
- TestIndex
- LocalNeighbors
These are cleaner and safer than turning everything into Blueprint-wide variables.
Sequence Node
The Sequence node is a simple way to run:
- Left
- Right
- Up
- Down
without needing messy chains of wires between all the direction blocks.
Suggested Defaults
If you want a simple starting point, try:
GridWidth = 21GridHeight = 21TileSize = 100
Odd grid sizes work best for this type of maze setup.
Up Next
Part 2 will build the actual maze generation loop:
- choosing a start tile
- using valid neighbors
- carving paths
- backtracking when stuck