Implementing Recursive Backtracking in Unity's C#

Hello everyone,

I like experimenting with Unity in my freetime, but I don’t do that professionally. A few days ago, I learned about creating a random maze using the Recursive Backtracking algorithm. I tried to “translate” tthat into C# and came to the following script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MazeCreator : MonoBehaviour
{

    public GameObject MazeGenerator;
    public static int MazeSize = 20;    // Number of colums and rows
    public static int CellSize = 10;    // Size of a cell in World Units
    private int x = 0;                  // x position in the grid
    private int y = 0;                  // y position in the grid
                                        // These variables are changed based on the cell.
    private int Borders = MazeSize * CellSize / 2; // should be the number of cells from the world middle to the edge
    private int r = 0;                  // variable for the random number
    private string d = "";              // direction to go
    private List<string> directions = new List<string>();   // will be filled with the possible moves
    private List<int> VisitedX = new List<int>();           // x-coordinates of all visited cells
    private List<int> VisitedY = new List<int>();           // and the y-coordinates
    private List<int> StackX = new List<int>();
    private List<int> StackY = new List<int>();             // used for backtracking
    

    private void CheckNeighbours()      // Check possible moves
    {
        directions.Clear();
        if ((x + 1 <= 0) & (VisitedX.IndexOf(x + 1) != -1))
        {
            directions.Add("right");
        }
        if ((x - 1 >= 0) & (VisitedX.IndexOf(x - 1) != -1))
        {
            directions.Add("left");
        }
        if ((y + 1 <= MazeSize - 1) & (VisitedY.IndexOf(y + 1) != -1))
        {
            directions.Add("up");
        }
        if ((y - 1 <= MazeSize - 1) & (VisitedY.IndexOf(y - 1) != -1))
        {
            directions.Add("down");
        }
    }


    void Start()
    {
        // Vorbereitung
        x = 0;
        y = 0;              // The lower left corner is my starting point and has the grid coordinates (0|0)
        StackX.Add(x);
        StackY.Add(y);
        VisitedX.Add(x);
        VisitedY.Add(y);

        // Path positioning
        transform.position = new Vector3(-1 * Borders, 1, -1 * Borders);
        transform.rotation = Quaternion.Euler(Vector3.zero);
        Instantiate(MazeGenerator);             // an object representing the path from the last cell to this one
        print("Maze construction begins.");
        print(Borders);
        print(StackX.Count);
        while (StackX.Count > 0)
        {
            print(StackX.Count);
            CheckNeighbours();
            if (directions.Count > 0)
            {
                // Random number
                r = (int)Mathf.Round(Random.Range(0, directions.Count));
                d = directions[r];

                // Move to selected direction
                if (d == "right")
                {
                    x++;
                    transform.Translate(Vector3.right * CellSize);
                    transform.rotation = Quaternion.Euler(0, 0, 0);
                    Instantiate(MazeGenerator);
                }
                if (d == "left")
                {
                    x--;
                    transform.Translate(Vector3.left * CellSize);
                    transform.rotation = Quaternion.Euler(0, 180, 0);
                    Instantiate(MazeGenerator);
                }
                if (d == "up")
                {
                    y++;
                    transform.Translate(Vector3.forward * CellSize);
                    transform.rotation = Quaternion.Euler(0, 90, 0);
                    Instantiate(MazeGenerator);
                }
                if (d == "right")
                {
                    y--;
                    transform.Translate(Vector3.back * CellSize);
                    transform.rotation = Quaternion.Euler(0, -90, 0);
                    Instantiate(MazeGenerator);
                }

                // Update lists
                StackX.Add(x);
                StackY.Add(y);
                VisitedX.Add(x);
                VisitedY.Add(y);
            } else
            {

                // Backtrack path
                x = StackX[StackX.Count - 1];
                y = StackY[StackY.Count - 1];
                transform.rotation = Quaternion.Euler(90, 0, 0);
                transform.position = new Vector3(-1 * Borders + x * CellSize, 1,
                                  -1 * Borders + y * CellSize);
                StackX.Remove(StackX.Count - 1);
                StackY.Remove(StackY.Count - 1);
            }
        }
        print("Maze finished.");
    }
    
}

If I start the scene, the GameObject I attached that script to goes to the middle of the screen, but does nothing. That’s why I have the following questions:

  • The while-loop doesn’t seem to be executed as it should, because the StackX.Count is just printed once. But it prints the right length of 1.
  • The first two variables should be shown by the Inspector. They are public, but if they’re not static, the names are no more accepted below in the script. Why is that the case?
  • And why aren’t they shown in the Inspector? I even tried [SerializeField], but the Inspector still hides them.

Furthermore I’d be really happy if someone would upload a corrected version of that script, or even make a better script using that algorithm. The cell size and the number of cells in columns and rows should be settible in the Inspector. And it would be even better if that script would create walls (based on scaled cubes) instead of a “path”.

It is because you are using those variables as a field initializer for your Borders variable.

It appears you could make a lot of your variables local to Start() and initialize them there, perhaps with the exception of those used by the other function.

Also, just so you know, recursion like this is a difficult problem to reason about, especially when you recurse (I presume) by Instantiate-ing Unity objects. The timing can be exquisitely tricky to reason about. You will always do better with a simple iterative loop approach to fabricating a maze.

Or even better you could learn to debug your script! Here’s how… it’s easy!

You must find a way to get the information you need in order to reason about what the problem is.

Once you understand what the problem is, you may begin to reason about a solution to the problem.

What is often happening in these cases is one of the following:

  • the code you think is executing is not actually executing at all
  • the code is executing far EARLIER or LATER than you think
  • the code is executing far LESS OFTEN than you think
  • the code is executing far MORE OFTEN than you think
  • the code is executing on another GameObject than you think it is
  • you’re getting an error or warning and you haven’t noticed it in the console window

To help gain more insight into your problem, I recommend liberally sprinkling Debug.Log() statements through your code to display information in realtime.

Doing this should help you answer these types of questions:

  • is this code even running? which parts are running? how often does it run? what order does it run in?
  • what are the values of the variables involved? Are they initialized? Are the values reasonable?
  • are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

Knowing this information will help you reason about the behavior you are seeing.

You can also supply a second argument to Debug.Log() and when you click the message, it will highlight the object in scene, such as Debug.Log("Problem!",this);

If your problem would benefit from in-scene or in-game visualization, Debug.DrawRay() or Debug.DrawLine() can help you visualize things like rays (used in raycasting) or distances.

You can also call Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene manually, looking for all the parts, where they are, what scripts are on them, etc.

You can also call GameObject.CreatePrimitive() to emplace debug-marker-ish objects in the scene at runtime.

You could also just display various important quantities in UI Text elements to watch them change as you play the game.

If you are running a mobile device you can also view the console output. Google for how on your particular mobile target, such as this answer or iOS: https://discussions.unity.com/t/700551 or this answer for Android: https://discussions.unity.com/t/699654

If you are working in VR, it might be useful to make your on onscreen log output, or integrate one from the asset store, so you can see what is happening as you operate your software.

Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

Here’s an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

https://discussions.unity.com/t/839300/3

When in doubt, print it out!™

Note: the print() function is an alias for Debug.Log() provided by the MonoBehaviour class.

Thank you, @Kurt-Dekker !! Thanks for all the many tips you gave me to help solving the problems. It is indeed even better as just uploading a solution. I’ll try ALL of them out!

Just glancing (recursion is tricky) but it appears to me that your source is junk. First, and this doesn’t prove anything, using written-out words for the directions (“right”, “up”, “left”, “down”) is such a known novice thing that no one would ever use it in an example they want people to read. Also not proving anything, but odd, is using parallel arrays for x and y. I suppose there are excellent FORTRAN examples from 1979 that do that, but anyone else would naturally create an x,y struct (which is a Vector2Int in Unity, but making that isn’t a Unity thing). Third funny thing, why it is moving (transform.translate)? These things mentally walk through the maze, and can give you a list of the moves. They don’t actually move (unless Turtle math?)

It appears to be a non-recursive rewrite of a recursive solution using a stack (StackX, StackY, DirectionX, DirectionY). It substitutes for recursion by remembering “the thing I tried before this was going up from (4,7), and the thing before that was going left from 3,7” and so on. That’s a real thing, and the best solutions often do it, but it’s generally a step more difficult than recursion. Bigger, and the thing that has me stuck, where’s the 2D maze? I mean, how does this tell if something is a wall? CheckNeighbors looks at the Visitors lists, which start empty, so no walls there.

To sum up, possibly you’ve done the worst job copying ever, or possibly the person who write the original did. Maybe you missed one important thing, but I can’t guess at it. Or maybe it was written long ago for a funny environment and I’m not seeing the one thing that makes it work. I mean, if you told me this started out as terrific code, then run back and forth through wonky translation programs, I could believe that. You could look for a different one to start with.

Pretty abusive language, there, @Owen-Reynolds . Was the attitude necessary?

1 Like

The main thing I would do is to, as Kurt suggested, is not try to instantiate stuff as you go. Build a model of the maze in one go, then instantiate everything afterwards.

Hey OP I found this from Catlike Coding:

It appears to be a VERY thorough and step-by-step approach to making a maze.

It includes ALL the parts at the bottom in downloadable form, AND it includes a huge list of questions and well-written answers.

And… get this: it freakin’ WORKED IMMEDIATELY from downloading the final package! As a long-time sufferer of broken example projects, what Catlike Coding has done here is nothing short of legendary.

And here it is one second into generation… it’s pretty awesome!

2 Likes

CatlikeCoding has been a very useful resource for learning shader code. Nice to see that the blog covers other topics equally well :slight_smile:

1 Like

Oh, creation! I saw “recursive” and assumed it was solving a maze, since that has a nice recursive solution. I made a bunch of maze creation stuff a few years ago and it only really worked by adding a bunch of extra stuff: figuring out which directions were towards the exit and using weights to choose, adding “try to go straight for a while” rules, then making the false paths in a completely different way (using a maximum length sometimes), and so on.

But I rewrote a straight recursive version. Most mazes have walls between squares, but the spirit of the code way above seemed to be using spaces as big walls, so I did that. Unity crashed twice due to infinite loops – recursion is tricky. It’s a little long but you should be able to see where the stack of positions and directions is, and the backtracking. Oh, the results are as ugly as I remember. It gives these random-looking long zig-zag paths. I remember way back then I found maze-makers that I didn’t really like, but they gave better results than this:

public class mazeMake : MonoBehaviour {

    // 2D maze board variables:
    public int mazeHT=15, mazeWD=20;

    public Transform squarePF; // a 1x1 square, along xz

    class BSpace_t { // 1 board space
        public Transform t; // the visible square (null==wall)

        public void setAsFloor() { t.gameObject.SetActive(true); }
        public bool isFloor() { return t.gameObject.activeSelf==true; }
        public void resetToWall() { t.gameObject.SetActive(false); }
    }

    BSpace_t[,] Board;

    void initializeMaze() {
        Board=new BSpace_t[mazeWD,mazeHT];
        for(int x=0;x<mazeWD;x++)
            for(int y=0;y<mazeHT;y++) {
                Board[x,y]=new BSpace_t();
                Transform sq=Instantiate(squarePF);
                sq.position=new Vector3(x, 0, y);
                Board[x,y].t=sq;
            }
    }

    void resetBoard() { for(int x=0;x<mazeWD;x++) for(int y=0;y<mazeHT;y++) Board[x,y].resetToWall(); }

    // algorithm variables:
    class PathSquare_t {
        public Vector2Int p; // coordinates
        // directions in order to try them:
        public char[] D = new char[4];
        public int curDirIndex=0;

        public PathSquare_t(int x, int y) {
            p.x=x; p.y=y;
            // randomize order of directions:
            D=new char[]{'N','E','S','W'};
            for(int i=0;i<3;i++) {
                int i2=Random.Range(i,4);
                char tmp=D[i]; D[i]=D[i2]; D[i2]=tmp;
            }
        }

        public char getCurDirection() { return curDirIndex<4?D[curDirIndex]:'X'; }
        public bool isDone() { return curDirIndex>=4; }
        public void gotoNextDirection() { curDirIndex++; }
    }

    Vector2Int dirCharToDir(char dChar) {
        Vector2Int val=Vector2Int.zero;
        if(dChar=='N') val.y=+1; else if(dChar=='S') val.y=-1;
        else if(dChar=='E') val.x=+1; else val.x=-1;
        return val;
    }

    List<PathSquare_t> BuildPath;

    bool isOffEdge(Vector2Int p) { return p.x<0 || p.y<0 || p.x>=mazeWD || p.y>=mazeHT; }

    // used to avoid running one path into an older one:
    int connectedFloors(Vector2Int p) {
        int count=0;
        for(int i=0;i<4;i++) {
            char dChar="NESW"[i];
            Vector2Int p2=p+dirCharToDir(dChar);
            if(!isOffEdge(p2) && Board[p2.x,p2.y].isFloor()) count++;
        }
        return count;
    }

    public int delayFramesEachSquare=2; // to let us watch it being made
    int currentDelayFrames;

    bool waitingForSpace=true;

  void Start() {
        initializeMaze();
        resetBoard();
        waitingForSpace=true;

        BuildPath=new List<PathSquare_t>();
  }

    void Update() {
        if(waitingForSpace) {
            if(Input.GetKeyDown(KeyCode.Space)) {
                restartMazeMake();
                waitingForSpace=false;
            }
            else return;
        }

        // the delay between making each new floor square:
        currentDelayFrames--;
        if(currentDelayFrames>0) return;
        currentDelayFrames=delayFramesEachSquare;

        bool done=BuildPath.Count==0;
        if(done) { waitingForSpace=true; return; }

        makeOneNewFloorSpace();
    }

    void restartMazeMake() {
        resetBoard();

        BuildPath.Clear();
        // arbitrarily start in the middle of the bottom row:
        int x=mazeWD/2, y=0;
        BuildPath.Add(new PathSquare_t(x,y));
        Board[x,y].setAsFloor();
    }

    void makeOneNewFloorSpace() {
        bool createdNewFloor=false;

        while(BuildPath.Count>0 && !createdNewFloor) {
            // first look for a legal direction from the current space:
            PathSquare_t curSpot=BuildPath[BuildPath.Count-1];
            while(!createdNewFloor && !curSpot.isDone()) {
                char dirChar=curSpot.getCurDirection();
                Vector2Int moveAmt=dirCharToDir(dirChar);
                Vector2Int newSq=curSpot.p+moveAmt;

                bool isLegalSpace=!isOffEdge(newSq) && connectedFloors(newSq)==1;
                if(isLegalSpace) {
                    createdNewFloor=true;
                    Board[newSq.x, newSq.y].setAsFloor();
                    curSpot.gotoNextDirection(); // for when we come back
                    BuildPath.Add(new PathSquare_t(newSq.x, newSq.y)); // next time start from here
                    break;
                }
                else curSpot.gotoNextDirection();
            }

            // nowhere to go from current space. Back up and try from there:
            if(!createdNewFloor) BuildPath.RemoveAt(BuildPath.Count-1);
        }
    }
}

Not only that… to understand recursion you must first understand recursion.