---
title: "Add obstacles"
content_type: tutorial
url: "https://learn.unity.com/course/2d-roguelike-tutorial/tutorial/add-obstacles?language=en"
language: en
container_type: course
container_title: "Create a 2D Roguelike Game"
container_url: "https://learn.unity.com/course/2d-roguelike-tutorial.md?language=en&version=6.3"
standalone_markdown_url: "https://learn.unity.com/tutorial/add-obstacles.md?language=en&version=6.3"
previous_title: "Add a food resource"
previous_url: "https://learn.unity.com/course/2d-roguelike-tutorial/tutorial/add-a-food-resource?language=en&version=6.3"
previous_markdown_url: "https://learn.unity.com/course/2d-roguelike-tutorial/tutorial/add-a-food-resource.md?language=en&version=6.3"
next_title: "Add win and lose conditions"
next_url: "https://learn.unity.com/course/2d-roguelike-tutorial/tutorial/add-win-and-lose-conditions?language=en&version=6.3"
next_markdown_url: "https://learn.unity.com/course/2d-roguelike-tutorial/tutorial/add-win-and-lose-conditions.md?language=en&version=6.3"
difficulty: beginner
duration_minutes: 15
unity_version:
  - "6.0"
  - "6.3"
supported_unity_versions:
  - name: "6.0"
    url: "https://learn.unity.com/course/2d-roguelike-tutorial/tutorial/add-obstacles.md?language=en&version=6.0"
  - name: "6.3"
    url: "https://learn.unity.com/course/2d-roguelike-tutorial/tutorial/add-obstacles.md?language=en&version=6.3"
last_updated: "2026-08-04T14:23:22.000Z"
---

# Add obstacles

Add wall prefabs, random wall spawning, collision blocking, and destructible walls to make your game board more challenging.

In order to make your game more challenging, it could use some obstacles to make navigating the game board less straightforward.

By the end of this tutorial, you’ll have done the following:

- Created wall prefabs.
- Coded the functionality to have a random number of Wall GameObjects spawn on the game board when the game starts.
- Coded the functionality that denies the player character access to the tile the Wall GameObject occupies.
- Added the functionality that allows the player character to damage the Wall GameObject, and once destroyed, move to the tile it used to occupy.

## 1. Overview

To make navigating the game board more interesting, this tutorial will guide you through adding walls in random places on the board. Then, you’ll code the functionality to have the player character destroy these walls and remove them from the game board.

## 2. Add obstacles

Let’s build on that CellObject concept by creating an obstacle for the player to overcome. A wall that blocks the player character is a good candidate for this, because you’ve already done something similar with the borders of the board. Let’s also add the functionality that every time the player bumps into it, the wall gets damaged, and after a certain amount of damage, the wall gets destroyed and the cell is now passable.

There are a couple of sprites that you can use for walls:

![](https://connect-mediagw.unity.com/h1/20240927/learn/images/ab7af444-f199-494a-be1b-097cf57798b3_wall-sprites-1.png)


These sprites are full tile sprites, which means they cover the entire cell, like the ground or wall sprites you used before. You can use them as normal sprites and render them on top of the tilemap, and once they get destroyed the base tilemap will be shown. But instead of that, let’s modify which tile the tilemap uses in that particular cell.

In a game this small, the difference between these two techniques is minimal, but on large scale maps this could change a lot: individual sprites are rendered one after another, but tilemaps are rendered in a batch, so tilemaps are a lot more efficient.

You can’t add components (like the **CellObject**) to individual cells. However, this won’t be an issue because this new type of cell object, let’s call them **WallObjects**, will just be an empty GameObject with no sprite. It will still be placed in the cell, but it will only be there to hold the component and change the tilemap under it through a script.

To create and add WallObjects to your game, follow these instructions:

**1.**  In the **Hierarchy** window, create an empty GameObject and name it “Wall”.

Remember, you aren’t using sprites; the CellObject will modify the tilemap.

**2.**  In the **Project** window, right click the **Scripts** folder and select **Create** > **Scripting** > **MonoBehaviour Script**, and name it “WallObject”.

```csharp
public class WallObject : CellObject
{
}
```

**WallObject**, just like **FoodObject**, inherits from CellObject.

**3.**  Add the **WallObject** script as a component to the **Wall** GameObject, click and drag the **Wall** GameObject into your **Prefabs** folder, then delete the **Wall** GameObject from the scene.

**4.**  Add a public variable of type **WallObject** called “WallPrefab” to your **BoardManager** and create a new **GenerateWall** function that will be very similar to your **GenerateFood** function.

The **GenerateWall** function will spawn a certain number of walls in the level.

```csharp
void GenerateWall()
{
   int wallCount = Random.Range(6, 10);
   for (int i = 0; i < wallCount; ++i)
   {
       int randomIndex = Random.Range(0, m_EmptyCellsList.Count);
       Vector2Int coord = m_EmptyCellsList[randomIndex];

       m_EmptyCellsList.RemoveAt(randomIndex);
       CellData data = m_BoardData[coord.x, coord.y];
       WallObject newWall = Instantiate(WallPrefab);

       newWall.transform.position = CellToWorld(coord);

       data.ContainedObject = newWall;
   }
}
```

**5.**  Call the above **GenerateWall** method inside the **Init** method, before the call to the **GenerateFood** function.

```csharp
public void Init()
    {
        ... (all code above)
        GenerateWal() ; // new line
        GenerateFood();
    }
```

This way all the cells that will generate walls will be removed from the empty cell list and food won’t be able to generate there.

In the Editor, assign your WallPrefab to the WallPrefab slot on your BoardManager and if you enter Play mode now, because the **WallObjects** class is empty, they won't change the board yet and you won’t be able to see them, but selecting them in the **Hierarchy** window will show they are at the right place in the level. Finding ways like this to test features even when they aren’t finished allows you to reduce the scope of code you’ll need to debug if there are any problems, so always try to think of a way to test something you’re doing intermittently. Even if the walls aren’t visible, by using the **Scene** view and **Hierarchy** window, you can check that the generating code works before going to the next step.

The **Wall** GameObject will need to do two things that CellObject can’t do for now:

- Change the tile sprite it's placed on to a wall sprite.
- Stop the player character from entering a cell.

Let’s first tackle the tile changing problem. As discussed before, when presented with a problem, it’s a good idea to try and make it more generic so that you can craft a solution that might cover more than one use case.

The goal is for the code to perform an action when the **CellObject** is placed over a cell. This seems like something that you might want to do with other CellObjects, not only walls. So it’ll be beneficial to add a new virtual function to **CellObject** that’s called after the object is placed. This function will also need to know the cell it’s placed on.

We can combine all these requirements into an **Init** virtual function that takes a **Vector2Int** storing the coordinate of the cell it’s placed on.

```csharp
public class CellObject : MonoBehaviour
{
   protected Vector2Int m_Cell;

   public virtual void Init(Vector2Int cell)
   {
       m_Cell = cell;
   }
  
   public virtual void PlayerEntered()
   {
      
   }
}
```

**Note**: **m_Cell** is protected**,** not private. **Private** would make it inaccessible to the subclasses of **CellObject** like **FoodObject** or **WallObject**. Instead, **protected** will let other subclasses like **WallObject** access it, while still hiding it from other unrelated classes.

**6.**  Update the **GenerateWall** function in the **BoardManager** script to the following, calling **Init** just after instantiating the new Wall:

```csharp
void GenerateWall()
{
   int wallCount = Random.Range(6, 10);
   for (int i = 0; i < wallCount; ++i)
   {
       int randomIndex = Random.Range(0, m_EmptyCellsList.Count);
       Vector2Int coord = m_EmptyCellsList[randomIndex];

       m_EmptyCellsList.RemoveAt(randomIndex);
       CellData data = m_BoardData[coord.x, coord.y];
       WallObject newWall = Instantiate(WallPrefab);
      
       //init the wall
       newWall.Init(coord);
      
       newWall.transform.position = CellToWorld(coord);

       data.ContainedObject = newWall;
   }
}
```

To finalize this group of changes, you need to override the **Init** function in the **WallObject** class for it to change the Tilemap tile. However, as you try doing this, you might realize that there is no access to the Tilemap from outside the **BoardManager** class. Like the method you added to change cell index to world position, it makes sense to add this method on the **BoardManager**.

**7.**  Add the following new method to **BoardManager**:

```csharp
public void SetCellTile(Vector2Int cellIndex, Tile tile)
{
   m_Tilemap.SetTile(new Vector3Int(cellIndex.x, cellIndex.y, 0), tile);
}
```

Because this new function is public, you can use it on **WallObject** to finally change the **Tile** in the **Init** function.

**8.**  Add the following code to your script:

```csharp
using UnityEngine;
using UnityEngine.Tilemaps;

public class WallObject : CellObject
{
   public Tile ObstacleTile;
  
   public override void Init(Vector2Int cell)
   {
       base.Init(cell);
       GameManager.Instance.BoardManager.SetCellTile(cell, ObstacleTile);
   }
}
```

**Note**: Make sure to call **base.Init()** first. This will call the **Init** function from the class it inherits from (**CellObject**), and assign the **m_Cell** variable before setting the tile. If you don’t call **base.Init** first, the **m_Cell** variable will be unassigned! As a general rule, always call the base version of an overridden function unless you know that base version does nothing by default or you want to completely override what it does.

**9.**  Save your scripts. Select the Wall prefab and, in the **Inspector** window, assign the tile you want to use to the **Obstacle Tile** slot.

If the sprite you want to use isn’t a tile yet, create it through the TilePalette like you did in [this earlier lesson](https://learn.unity.com/tutorial/66f53d37edbc2a0e85ce171c?uv=6&courseId=66fa6d33edbc2a141af15aae#66f69920edbc2a05bcd61787))

If you enter Play mode now, you should finally see the wall tiles being added to the game board randomly.

![](https://connect-mediagw.unity.com/h1/20240927/learn/images/7823f650-9045-4a1f-b388-bfe40646d3ae_game-view-walls.png)

However, the player character can still move on top of the walls. This is because right now the walls just change the tilemap tile when initialized. To stop the player character from moving on top of the walls, you can change the **Passable** property of the **CellData** to **false** in the **Init** code of the **WallObject**.

This works well on its own, but in the game, you also want the player character to be able to destroy the wall by bumping into it a certain number of times. But your player character movement code doesn't even check if a cell has anything in it if it isn’t passable.

As always, when presented with a problem, it’s helpful to describe it in abstract to figure out what you need to do to solve it. In this case:

- Detect if the player wants to move to a certain cell.
- Stop the player from moving there if the cell has a special condition (in this case, has a wall).

To add this functionality, the player character code needs to ask the **CellObject** code if the player character can enter the cell, and if the **CellObject** code will allow it or not.

A new virtual **PlayerWantsToEnter** method in **CellObject** can serve this purpose. The player character movement code calls this method on the object in a cell when the player character tries to move there, and this method either returns true if the player character can enter the cell (like for **FoodObject**) or cannot (like for **WallObject**):

```csharp
public virtual bool PlayerWantsToEnter()
{
   return true;
}
```

Then you can use that new method by modifying the code in the **if(haseMoved)** condition from the **Update** function of the **PlayerController**:

```csharp
if(cellData != null && cellData.Passable)
{
   GameManager.Instance.TurnManager.Tick();

   if (cellData.ContainedObject == null)
   {
       MoveTo(newCellTarget);
   }
   else if(cellData.ContainedObject.PlayerWantsToEnter())
   {
       MoveTo(newCellTarget);
       //Call PlayerEntered AFTER moving the player! Otherwise not in cell yet
       cellData.ContainedObject.PlayerEntered();
   }
}
```

Let’s take a closer look at the lines of code above:

- If a cell is passable the game ticks once (bumping into an object counts as a tick, but not being able to enter a cell like an outer wall or non destructible wall doesn’t).
- Then, a conditional block will run a couple of checks:
  - If the cell the player is trying to move to doesn’t have a cell object, the player character can move to that cell.
  - If the cell has a cell object, it will call **PlayerWantsToEnter** on that cell object and if that returns **true**, then the player character can enter.

Right now, the base version of the **PlayerWantsToEnter** function returns **true**, so the player character can always move to the cells, but let’s override the function inside **WallObject** to be able to also return **false** when the **CellObject** is a wall:er character can enter.

```csharp
public class WallObject : CellObject
{
   public Tile ObstacleTile;
  
   public override void Init(Vector2Int cell)
   {
       base.Init(cell);
       GameManager.Instance.BoardManager.SetCellTile(cell, ObstacleTile);
   }

   public override bool PlayerWantsToEnter()
   {
       return false;
   }
}
```

Now if you enter Play mode, your newly placed wall should stop the player character from moving to the cell!

**Tip**: Remember to check in your changes to UVCS!

## 3. Refactoring

Before you continue, take some time to review what you’ve done so far and consider if there is an area where you can clean up the code or simplify it to better future proof it.

If you look for code duplication, you might notice that in **BoardManager**, there’s similar code used to place the food and the wall: both add a **CellObject** to a cell. But right now, your food placement doesn’t call **Init** on the new **FoodObject**. This isn’t a problem here as the **FoodObject** doesn’t override **Init** and doesn’t use the **m_Cell** property that the **Init** function initialized, but this could be a source of an error if you forget to initialize other CellObjects you write.

Therefore, now’s a good opportunity to consolidate this part of the code and create an **AddObject** function that will add a given **CellObject** into a given cell, and take care of placing it in the right place and calling **Init** on it:

```csharp
void AddObject(CellObject obj, Vector2Int coord)
{
   CellData data = m_BoardData[coord.x, coord.y];
   obj.transform.position = CellToWorld(coord);
   data.ContainedObject = obj;
   obj.Init(coord);
}
```

Now let’s use this new function to add a **FoodObject** and **WallObject** to the cell instead of manually doing it; the new **GenerateWall** and **GenerateFood** functions now look like the following:

```csharp
void GenerateWall()
{
   int wallCount = Random.Range(6, 10);
   for (int i = 0; i < wallCount; ++i)
   {
       int randomIndex = Random.Range(0, m_EmptyCellsList.Count);
       Vector2Int coord = m_EmptyCellsList[randomIndex];

       m_EmptyCellsList.RemoveAt(randomIndex);
       WallObject newWall = Instantiate(WallPrefab);
       AddObject(newWall, coord);
   }
}

void GenerateFood()
{
   int foodCount = 5;
   for (int i = 0; i < foodCount; ++i)
   {
       int randomIndex = Random.Range(0, m_EmptyCellsList.Count);
       Vector2Int coord = m_EmptyCellsList[randomIndex];

       m_EmptyCellsList.RemoveAt(randomIndex);
       FoodObject newFood = Instantiate(FoodPrefab);
       AddObject(newFood, coord);
   }
}
```

This way, if you use the **AddObject** function to add new kinds of objects to your board later, everything will be set properly.

Don’t forget to try the game in Play mode after all those changes to make sure you haven’t broken anything before you continue!

## 4. Damaging walls

Currently when the player bumps into a wall cell, they won't be able to move, but the food counter will decrease as bumping is taken as a  turn. So, let’s add the functionality of damaging walls. You should be able to do this by yourself with everything you have learned so far. But first, let's write down the what this functionality needs to do:

- Create a new counter variable in **WallObject** that stores the hitpoint count of that wall.
- Save the original tile in that cell during **Init** before it’s replaced with the wall tile (so you can set it back after the wall is destroyed). This will require you to add a new function in **BoardManager** just like we did for **SetCellTile**!

**Note**: The **Tilemap** class contains a function called **GetTile** (similar to **SetTile**) that allows you to retrieve the tile of a given cell. But if you look at the documentation, you’ll see that this function returns a **BaseTile**, not a tile. Don’t worry, you can easily solve this by using the generic version of the function **GetTile\<Tile>()** to retrieve the actual tile.

- When the **PlayerWantToEnter** function is called in **WallObject**, reduce the hit point counter by 1, and when the hit points reach 0, the **WallObject** is destroyed, and the wall tile is removed and is set back to the original tile.

## WallObject

```csharp
public class WallObject : CellObject
{
   public Tile ObstacleTile;
   public int MaxHealth = 3;

   private int m_HealthPoint;
   private Tile m_OriginalTile;
  
   public override void Init(Vector2Int cell)
   {
       base.Init(cell);

       m_HealthPoint = MaxHealth;
      
       m_OriginalTile = GameManager.Instance.BoardManager.GetCellTile(cell);
       GameManager.Instance.BoardManager.SetCellTile(cell, ObstacleTile);
   }

   public override bool PlayerWantsToEnter()
   {
       m_HealthPoint -= 1;

       if (m_HealthPoint > 0)
       {
           return false;
       }

       GameManager.Instance.BoardManager.SetCellTile(m_Cell, m_OriginalTile);
       Destroy(gameObject);
       return true;
   }
}
```

##  BoardManager

```csharp
public Tile GetCellTile(Vector2Int cellIndex)
    {
        return m_Tilemap.GetTile<Tile>(new Vector3Int(cellIndex.x,     cellIndex.y, 0));
    }
```

Above are our solutions; yours might be a bit different! Note that we declare a public property **MaxHealth** so we can change how much health a **WallObject** has in the **Inspector** window. That way we can potentially have walls that require more or less damage to be destroyed.

Enter Play mode and see how the player can now destroy the walls after a certain number of hits. Congratulations, you’ve added a lot of complexity to your gameplay! Don’t forget to save your changes to UVCS!

## 5. Challenges

Below are a couple of small challenges for you to add to the game!

## Easy: Multiple walls

There are different tiles sprites that you can use for walls. Create a prefab of each one and then modify your **BoardManager** to pick one randomly when generating walls.

![](https://connect-mediagw.unity.com/h1/20240927/learn/images/f2f82e3e-247c-49bb-a9d5-68af45579d20_wall-sprites-1__1_.png)

##  Medium: Change wall tiles based on how damaged they are

There is a second set of sprites for each of the wall sprites we showed in the previous challenge, but these sprites look half destroyed.

![](https://connect-mediagw.unity.com/h1/20240927/learn/images/4098ff03-5013-4d84-a95e-a280e7d957d7_wall-sprites-2.png)

Improve your code so the wall tiles are changed to these damaged ones when they have only 1 HP left to let your player know the wall is nearly destroyed!

## 6. Next steps

Your game now has obstacles to avoid that make the game much more challenging and fun! In the next tutorial, you’ll bring everything you’ve done previously together by adding win and lose conditions!
