24 April 2025

How to detect obstacles in Unity 2D

In games it is quite common to need to determine if a point on the stage is free of obstacles in order to place a character or another game element. Think, for example, of an RTS: when building a building, you have to choose a free section of land, but how can your code know if a site already has a building or some other type of object?

In a 3D game, the most common solution is to project a ray from the camera's viewpoint, passing through the point where the mouse cursor is located on the screen plane, until it hits a collider. If the collider is the ground, that point is free, and if not, there is an obstacle.

Of course, if the object we want to place is larger than a point, projecting a simple ray falls short. Imagine we want to place a rectangular building, and the point where its center would go is free, but the corner area is not. Fortunately, for those cases, Unity allows us to project complex shapes beyond a mere point. For example, the SphereCast methods allow an invisible sphere to be moved along a line, returning the first collider it hits. Another method, BoxCast, would solve the problem of the rectangular building by projecting a rectangular base box along a line. We would only have to make that projection along a vertical line to the ground position we want to check.

In 2D, there are also projection methods, BoxCast and CircleCast, but they only work when the projection takes place in the XY plane (the screen plane). That is, they are equivalent to moving a box or a circle in a straight line along the screen to see if they touch a collider. Of course, that has its utility. Imagine you are making a top-down game and want to check if the character will be able to pass through an opening in a wall. In that case, you would only need to do a CircleCast of a circle, with a diameter like the width of our character's shoulders, projecting through the opening to see if the circle touches the wall's colliders.

A CircleCast, projecting a circle along a vector.
A CircleCast, projecting a circle along a vector.

But what happens when you have to project on the Z-axis in a 2D game? For example, for a 2D case equivalent to the 3D example we mentioned earlier. In that case, neither BoxCast nor CircleCast would work because those methods define the projection vector using a Vector2 parameter, limited to the XY plane. In those cases, a different family of methods is used: the "Overlap" methods.

The Overlap methods place a geometric shape at a specific point in 2D space and, if the shape overlaps with any collider, they return it. Like projections, there are methods specialized in different geometric shapes: OverlapBox, OverlapCapsule, and OverlapCircle, among others.

Let's suppose a case like the following figure. We want to know if a shape the size of the red circle would touch any obstacle (in black) if placed at the point marked in the figure.

Example of using OverlapCircle.
Example of using OverlapCircle.

In that case, we would use OverlapCircle to "draw" an invisible circle at that point (the circle seen in the figure is just a gizmo) and check if the method returns any collider. If not, it would mean that the chosen site is free of obstacles.

A method calling OverlapCircle could be as simple as the following:

Call to OverlapCircle Call to OverlapCircle
Call to OverlapCircle Call to OverlapCircle

The method in the figure returns true if there is no collider within a radius (MinimumCleanRadius) of the candidateHidingPoint position. If there is any collider, the method returns false. For that, the IsCleanHidingPoint method simply calls OverlapCircle, passing the following parameters:

  • candidateHidingPoint (line 224): A Vector2 with the position of the center of the circle to be drawn. 
  • MinimumCleanRadius (line 225): A float with the circle's radius. 
  • NotEmpyGroundLayers (line 226): A LayerMask with the layers of the colliders we want to detect. It serves to filter out colliders we don't want to detect. OverlapCircle will discard a collider that is not in one of the layers we passed in the LayerMask. 

If the area is free of colliders, OverlapCircle will return null. If there are any, it will return the first collider it finds. If you are interested in getting a list of all the colliders that might be in the area, you could use the OverlapCircleAll variant, which returns a list of all of them.

We could end here, but I don't want to do so without warning you about a headache you will undoubtedly encounter in 2D. Fortunately, it can be easily solved if you are warned.

The problem can occur if you use tilemaps. These are very common for shaping 2D scenarios. The issue is that to form the colliders of a tilemap, it is normal to use a "Tilemap Collider 2D" component, and it is also quite common to add a "Composite Collider 2D" component to sum all the individual colliders of each tile into one to improve performance. The problem is that by default, the "Composite Collider 2D" component generates a hollow collider, only defined by its outline. I suppose it does this for performance reasons. This happens when the "Geometry Type" parameter has the value Outlines.

Possible values of the Geometry Type parameter.
Possible values of the Geometry Type parameter.

Why is it a problem that the collider is hollow? Because in that case, the call to OverlapCircle will only detect the collider if the circle it draws intersects with the collider's edge. If, on the other hand, the circle fits neatly inside the collider without touching any of its edges, then OverlapCircle will not return any collider, and we would mistakenly assume that the area is clear. The solution is simple once it has been explained to you. You need to change the default value of "Geometry Type" to Polygons. This value makes the generated collider "solid," so OverlapCollider will detect it even if the drawn circle fits inside without touching its edges.

It seems silly because it is, but it was a silly thing that took me a couple of hours to solve until I managed to find the key. I hope this article helps you avoid the same issue.

19 February 2025

Assigning data to tiles in Unity

Tilemaps are very often used to create 2D games. Their simplicity makes them ideal for creating a retro-style setting. 

However, at first glance, Unity's entire implementation of Tilemaps seems to be limited to its aesthetic aspect. That's why it's quite common to find people on the forums asking how to associate data with the different Tiles used in a Tilemap.

Why might we need to associate data with a tile? For a variety of reasons. For example, let's say we're using a Tilemap to map a top-down game. In that case, we'll probably want to add a "drag" value to the different tiles, so that our character moves slower on tiles that represent a swamp, faster on tiles that represent a path, and can't cross tiles that show impassable stones.

For our examples, let's assume a scenario like the one in the capture:

Scenario of our examples
Scenario of our examples


It represents an enclosed area that includes three obstacles inside, one on the left (with a single tile), one in the center (with four tiles) and one on the right (with three). The origin of the stage's coordinates is at its center; I have indicated it with the crosshair of an empty GameObject.

The problem we want to solve in our example is how to make a script analyze the scenario, identify the black tiles and take note of their positions.

As with many other cases, there is no single solution to this problem. We have an option that is quick to implement and offers more possibilities, but can put too much overhead on the game. On the other hand, we have another option that is more difficult to implement and is more limited, but will put less overhead on the game. Let's analyze both.

Associate a GameObject to a tile

Generally, when we want to identify at once the GameObjects that belong to the same category, the easiest way would be to mark them with a tag and search for them in the scenario with the static method  GameObject.FindGameObjectsWithTag() . The problem is that tiles are ScriptableObjects, so they cannot be marked with tags.

ScriptableObjects for tiles are created when we drag sprites onto the Tile Palette tab. At that point, the editor lets us choose the name and location of the asset with the ScriptableObject we want to create, associated with the tile. From that point on, if we click on the asset of that ScriptableObject we can edit its parameters through the inspector. For example, for the tile I used for the perimeter walls, the parameters are:

Setting up a Tile
Setting up a Tile


The fields that can be configured are:

  • Sprite: This is the sprite with the visual appearance of the tile. Once the sprite is set, we can press the "Sprite Editor" button below to configure both the pivot point and the collider associated with the sprite.
  • Color: Allows you to color the sprite with the color you set here. The neutral color is white; if you use it, Unity will understand that you do not want to force the sprite's color.
  • Collider Type: Defines whether we want to associate a Collider to the tile. If we choose "None" it will mean that we do not want the Tile to have an associated Collider; if we set "Sprite", the collider will be the one we have defined through the Sprite Editor; finally, if the chosen value is "Grid", the collider will have the shape of the Tilemap cells.
  • GameObject to Instantiate: This is the parameter we are interested in. We will explain this in a moment.
  • Flags: These are used to modify how a tile behaves when placed on a Tilemap. For our purposes, you can simply leave it at its default value.

As I was saying, the parameter that interests us for our purpose is "GameObject to instantiate" if we drag a prefab to this field, the Tilemap will be in charge of creating an instance of that prefab in each location where that Tile appears.

For example, to be able to easily locate the black tiles, those of the obstacles, I have associated a prefab to that parameter of their Tile that I have called ObstacleTileData.

Setting up the Obstacle Tile
Setting up the Obstacle Tile

Since all I want is to be able to associate a tag with the tiles, in order to locate them with  FindGameObjectsWithTag() , it was enough for me to make ObstacleTileData a simple transform with the tag I was interested in. In the screenshot you can see that I used the InnerObstacle tag .

ObstacleTileData with label InnerObstacle
ObstacleTileData with label InnerObstacle

Once this is done, and once the tiles we want to locate are deployed on the stage, we only need the following code to make an inventory of the tiles with the InnerObstacle tag .

Code to locate the tiles that we have marked with the InnerObstacle tag

We just need to place the above script on any GameObject located next to the stage's Tilemap. For example, I have it hanging from the same transform as the Grid component of the Stage's Tilemaps.

When the level starts, the Tilemap will create an instance of the ObstacleTileData prefab at each position on the stage where a black obstacle tile appears. Since the ObstacleTileData prefab has no visual component, its instances will be invisible to the player, but not to our scripts. Since these instances are marked with the "InnerObstacle" tag, our script can locate them by calling  FindGameObjectsWithTag() , on line 16 of the code. 

To demonstrate that the code correctly locates the obstacle tile locations, I've set a breakpoint on line 17, so that we can analyze the contents of the "obstacles" variable after calling  FindGameObjectsWithTag() . When running the game in debug mode, the contents of that variable are as follows:

Obstacle tile positions
Obstacle tile positions

If we compare the positions of the GameObjects with those of the tiles, we can see that obstacles[7] is the obstacle on the left, with a single tile. The GameObjects obstacle[2], [3], [5] and [6] correspond to the four tiles of the central obstacle. The three remaining GameObjects ([0], [1] and [4]) are the tiles of the obstacle on the right, the elbow-shaped one.

In this way, we have achieved a quick and easy inventory of all the tiles of a certain type.

However, pulling labels isn't the only way to locate instances of the GameObjects associated with each Tile. Tilemap objects offer the GetInstantiatedObject() method , which is passed a position within the Tilemap and in return returns the instantiated GameObject for that tile's tile. Using this method is less direct than locating objects by label, since it forces you to examine the Tilemap positions one by one, but there will be situations where you have no other choice.

Finally, before we leave this section of the article, you should be aware that there may be situations where instantiating a GameObject per tile can weigh down the performance of the game. In the example case, we are talking about a few tiles, but in much larger scenarios we may be talking about hundreds of tiles, so instantiating hundreds of GameObjects may be something to think twice about.

Extending the Tile class

By default, I would use the above strategy; but there may be situations where you don't want to instantiate a large number of GameObjects. In that case, you may want to use the approach I'm going to explain now.

Tiles are a class that inherits from ScriptableObject. We can extend the Tile class to add any parameters we want. For example, we could create a specialized Tile with a boolean to define whether the tile is an obstacle or not.

Tile with a specialized parameter
Tile with a specialized parameter

This tile can be instantiated like any ScriptableObject to create an asset. When we do this, we will see that the specialized parameter will appear and we can configure it through the inspector.

Setting the tile with the specialized parameter
Setting the tile with the specialized parameter

The key is that the assets we create this way can be dragged to the Tile Palette so they can be drawn on the stage.

Once that is done, we could use the Tilemap.GetTile() method to retrieve the tiles for each position, cast them to our custom tile type (in our case CourtyardTile) and then analyze the value of the custom parameter.

The drawback of this method is that we cannot use labels or layers to search for data associated with tiles, which forces us to go through the tilemap cell by cell to find them, but it has the advantage of freeing our game from the burden of creating a GameObject per tile.

Conclusion

Whether by creating a GameObject per tile or by extending the Tile class, you now have the resources necessary to associate data with each of the tiles. This will allow you to provide the tiles with essential semantics for a multitude of algorithms, such as pathfinding algorithms.