Unity3D: Dungeon Crawler Movement with Collisions

Strolling through the 11th level of Eye of the Beholder (1991).

It’s been almost two years since I wrote “First Person Grid Movement with Unity3D“, in which I showed how to use Unity3D to replicate the grid-based movement typical of “blobbers” like Eye of the Beholder. The most common question I’ve had about that article is how to implement collisions. So that’s what I’m going to show you in this article, using Unity Editor 2021.3.25f1 LTS.

Creating a Project

Creating a new Unity3D project.

Open the Unity Hub and create a new project. Make sure you select the 3D template, and call it whatever you want.

Adding the Game Script

Attach the new Game script to the Main Camera object.
  1. In the Assets pane, right-click and select Create -> C# Script. You can call it whatever you want, but I’ll call it Game.
  2. Drag it onto your Main Camera object.
  3. Double-click the script to open it in your IDE (e.g. Visual Studio Code).
  4. In your Update() function, paste the following code which is where we left off in “First Person Grid Movement with Unity3D“:
void Update()
{
    if (Input.GetKeyDown(KeyCode.W))
        this.transform.position += this.transform.rotation * Vector3.forward;
    else if (Input.GetKeyDown(KeyCode.S))
        this.transform.position += this.transform.rotation * Vector3.back;
    else if (Input.GetKeyDown(KeyCode.A))
        this.transform.position += this.transform.rotation * Vector3.left;
    else if (Input.GetKeyDown(KeyCode.D))
        this.transform.position += this.transform.rotation * Vector3.right;
    else if (Input.GetKeyDown(KeyCode.Q))
        this.transform.rotation *= Quaternion.Euler(0, -90, 0);
    else if (Input.GetKeyDown(KeyCode.E))
        this.transform.rotation *= Quaternion.Euler(0, 90, 0);
}

Adding a Map

So now we can move the camera, but we don’t have collisions yet. In fact, we don’t even have anything to collide with yet.

Unity3D comes with its own set of colliders you can use, but since we’re assuming movement in a 2D grid, then it’s much easier to just store a simple map in a 2D array and check what’s in the destination cell before moving into it. We’ll use this same 2D array to generate walls in the scene rather than putting cubes manually into the scene as we did in the earlier article. The following should do:

    private string[] map = new[] {
        "XXXXXXXXXXXX",
        "X          X",
        "XX XX  XX XX",
        "XX XX XXX XX",
        "XX  XXX    X",
        "XXX X X XX X",
        "X     X  X X",
        "X XXXXXXXX X",
        "X      X   X",
        "XXX XX    XX",
        "X    X XX  X",
        "XXXXXXXXXXXX",
    };

Since a string behaves like an array of characters, it’s good enough to use an array of strings instead of a 2D array of characters.

Generating Walls

After the map, let’s also add a field that we can use to set the prefab we’ll use as a wall:

    [SerializeField]
    GameObject wallPrefab;
Use the Wall prefab as the input GameObject to the Game script.

Back in the Unity Editor:

  1. Create a cube (GameObject menu -> 3D Object -> Cube) in the scene.
  2. Drag it from the Hierarchy pane into your Assets pane to create a prefab.
  3. Rename the Cube prefab in your Assets pane to Wall.
  4. Delete the cube in the scene.
  5. Select the Main Camera, then drag the Wall prefab into the Wall Prefab slot of the Game script as shown above.

Now that we have the Wall prefab set up, let’s go back into the script and add some code that will create the walls on startup:

    void Start()
    {
        for (int z = 0; z < map.Length; z++)
        {
            for (int x = 0; x < map[z].Length; x++)
            {
                if (map[z][x] == 'X')
                {
                    Vector3 position = new Vector3(x, 0, z);
                    Instantiate(wallPrefab, position, Quaternion.identity);
                }
            }
        }
    }

Because the Y-axis points upwards and we’re dealing with a flat 2D grid, it’s useful to note that Y is always zero and we’re dealing with the X- and Z-axes when moving.

Positioning the Camera

Press Play and we can get a first peek at the generated walls:

An initial view of the generated walls.

It’s clear that the camera is a little off, but we can already see that we’ve successfully generated some sort of maze. Thanks to the movement script we copied earlier, you can move around (WASD for forward, backward, left and right movement, Q to turn left, and E to turn right) and find a good starting point for the camera, e.g. (X, Y, Z) = (5, 0, 1):

The view from (X, Y, Z) = (5, 0, 1).

Implementing Collision Detection

So now we come to the original problem: we’re able to walk through the walls. Now that we have a map, all we need to do to detect collisions is to check what’s in the square we’re moving to, before we move into it. For this, we’ll add a little helper function:

    private void updatePositionIfNoCollision(Vector3 newPosition)
    {
        var x = System.Convert.ToInt32(newPosition.x);
        var z = System.Convert.ToInt32(newPosition.z);

        if (map[z][x] == ' ')
            this.transform.position = newPosition;
    }

This function takes the position of the square we’re about to move into as an input, then if that square is clear on the map, it updates the camera’s position. Conversely, if that square contains an ‘X’, then nothing will happen.

So in the Update() function, instead of updating the position directly when we handle the movement keys, we instead call this helper function to move only on condition that the target square is clear:

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.W))
            updatePositionIfNoCollision(this.transform.position + this.transform.rotation * Vector3.forward);
        else if (Input.GetKeyDown(KeyCode.S))
            updatePositionIfNoCollision(this.transform.position + this.transform.rotation * Vector3.back);
        else if (Input.GetKeyDown(KeyCode.A))
            updatePositionIfNoCollision(this.transform.position + this.transform.rotation * Vector3.left);
        else if (Input.GetKeyDown(KeyCode.D))
            updatePositionIfNoCollision(this.transform.position + this.transform.rotation * Vector3.right);
        else if (Input.GetKeyDown(KeyCode.Q))
            this.transform.rotation *= Quaternion.Euler(0, -90, 0);
        else if (Input.GetKeyDown(KeyCode.E))
            this.transform.rotation *= Quaternion.Euler(0, 90, 0);
    }

That’s all. If you press Play now, you’ll find that you can roam freely around the map, but you can’t move into walls.

Conclusion

Collision detection on a 2D grid is pretty easy. You just need to keep track of where you are and what’s in the square you’re moving into.

4 thoughts on “Unity3D: Dungeon Crawler Movement with Collisions”

  1. This is really good. I do have a question if you still check this…Using a string like this you would be limited to a specific number of possible tiles right? IE 26 letters and 10 numbers for a total of 36 possible wall or other obstacle tiles. Is there a way to improve this to have hundreds of possibilities?

    1. Why do you think they’re limited to letters and numbers? A C# string is composed of 16-bit Unicode characters, so you have something over a million different options as it is. The example already in fact uses spaces which are outside the set you mention.

      Of course, this article demonstrates a trivial example, and for something more serious you’d probably store binary data in a file using an encoding that fits your particular needs.

Leave a Reply

Your email address will not be published. Required fields are marked *