2D Destructable Terrain in Unity using QuadTrees

Introduction

Kajetan Radulski
17 min readFeb 27, 2024

In this tutorial we will create somewhat optimized destructible 2D terrain that will seem familiar to anyone whose childhood remotly resembled mine.

For this tutorial you will need mesh generation scripts which are available at this adress

If you wish to learn how they work you are welcome to read through my mesh generation introduction

Generating data

Here is a 2D texture I’m using for this tutorial.

A cheese map

I have found it on this website and simply added an alpha channel.

To start we will need to generate 2D array of bool values that will inform our mesh construction script whether or not there is a a terrain at this pixel. For the purpose of this tutorial we will call that a “solid” pixel. We will determine wheter the pixel is solid by using it’s alpha channel. Every pixel with a non zero alpha value is solid.

To store this values we will create a TerrainData class which will fill in bool array by iterating over every pixel of a given texture. We will also add a simple DrawGizmos() function to display our created values.

If you are running this on a slow machine you may want to skip drawing gizmos for it will use up a lot of resources. I find this visualisation helpfull but we will optimize this shortly.

public class TerrainData
{
private bool [,] _points;
int _width;
int _height;

public int Width => _width;
public int Height => _height;

public void DrawGizmos(float pixelsPerUnit)
{
if (_points != null)
{
for (int x = 0; x < _width; x++)
{
for (int y = 0; y < _height; y++)
{
Vector3 position = new Vector3(x, y, 0) / pixelsPerUnit;
Gizmos.color = _points[x, y] ? Color.red : Color.blue;

Gizmos.DrawSphere(position, 0.5f / pixelsPerUnit);
}
}
}
}

public TerrainData(Texture2D texture2D)
{
_width = texture2D.width;
_height = texture2D.height;
_points = new bool[_width, _height];
for (int x = 0; x < _width; x++)
{
for (int y = 0; y < _height; y++)
{
_points[x, y] = texture2D.GetPixel(x, y).a > Single.Epsilon;
}
}
}
}

We need a class that will actually put our generated terrain on the scene, and as of now, at least display terrain gizmos. So let’s write it now. I called mine DestructableTerrain. It is nothing to write home about. It holds a refernce to a 2D texture and calls TerrainData constructor and gizmo drawing function

public class DestructableTerrain : MonoBehaviour
{
[SerializeField]
private Texture2D _texture2D;

private TerrainData _terrainData;

private void OnDrawGizmosSelected()
{
if (_terrainData != null)
{
_terrainData.DrawGizmos(10f);
}
}

public void GenerateTerrain()
{
_terrainData = new TerrainData(_texture2D);
}

}

We also need a custom inspector script to call GenerateTerrain() function using a button. It will look like this.

[CustomEditor(typeof(DestructableTerrain))]
public class DestructibleTerrainEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
DestructableTerrain terrain = target as DestructableTerrain;
if (GUILayout.Button("Generate terrain"))
{
terrain.GenerateTerrain();
EditorUtility.SetDirty(terrain);
}
}
}

Remember to put all editor scrtipts in in Editor directory. Otherwise your game won’t build

Now on your scene let’s create an empty object and attach our DestructableTerrain component to this object. I named mine the same as the script.

After that fill in a refernce to the texture. Remember to have this texture ReadWrite enabled in import settings.

Make sure to enable ReadWrite on texture

The end result should look something like this.

Remember to enable gizmos in your scene view

We see the outline of our texture

Organizing Data

Drawing those gizmos was educational but we really made our computer sweat. So let’s optimize it quickly.

We will use a spatial partitioning techique called a QuadTree

If you ever took a CS class you will know what a binary tree is. Quad tree is an equivalent of that for a 2D space. In our case we will hold in memory largest possible uniform quads. What in means in practice?

We take all of our points and put them in a quad. This is our root.

Our roor quad

We check if this quad is uniform ie. if all of the points in it are in the same state. Either solid or not. If not (as in the image above) we divde it into four new ones

After we divided our original quad we evaluate each of it’s children one by one. We see that two quads are uniform and two need even more precision.

At the end we see that each quad stores points of only one type which will be much more performant for our purposes than rendering every element separetly because in the end we will be generating meshes from this data.

Simple enough, let’s code it!

Before we create a quad tree we need to create a quad. So let’s make a class for one right now. Our quad will have x and y starting cooridinates as well as width and height. So the big red quad on an the image above would have coordinates 0,0 and dimensions 2x2.

public class Quad 
{
private readonly int _xAdress;
private readonly int _yAdress;
private readonly int _width;
private readonly int _height;

private bool _hasChildren = false;
private Quad[] _children;

public Quad(int xAdress, int yAdress, int width, int height)
{
_xAdress = xAdress;
_yAdress = yAdress;
_width = width;
_height = height;
}
}

We will also need a subdivide function to acutally fill in the children array.

    
public class Quad
{
...
public Quad[] Subdivide()
{
int halfWidth = _width / 2;
int halfHeight = _height / 2;
_children = new[]
{
new Quad(_xAdress, _yAdress, halfWidth, halfHeight),
new Quad(_xAdress + halfWidth, _yAdress, halfWidth, halfHeight),
new Quad(_xAdress, _yAdress + halfHeight, halfWidth, halfHeight),
new Quad(_xAdress + halfWidth, _yAdress + halfHeight, halfWidth, halfHeight)
};
_hasChildren = true;
return _children;
}
}

We don’t want to divide quads that are already too small so let’s create a function for that back in Quad class.

public class Quad 
{
...

public bool IsDivisible()
{
return _width > 1 && _height > 1;
}
}

We also need to know whether or not the quad is uniform. So let’s add a function to terrain data that will check just that. We go through every points starting from quad origin and check if it’s the same as every other point in this quad. If it is so we return true, otherwise we return false.

public class TerrainData
{
...
public bool IsQuadUniform(Quad quad)
{
bool isOriginSolid = _points[quad.XAdress, quad.YAdress];
for (int xDelta = 0; xDelta <= quad.Width-1; xDelta++)
{
for (int yDelta = 0; yDelta <= quad.Height-1; yDelta++)
{
bool isOtherSolid = _points[quad.XAdress + xDelta, quad.YAdress + yDelta];
if (isOriginSolid != isOtherSolid)
{
return false;
}
}
}
return true;
}

With that out of the way let’s create QuadTree class to create and hold those quads. If you ever implemented a binary tree this will seem very familiar.

We start by creating our root quad and pushing it onto the stack. Then as long as there are quads in the stack we take on as check if it is uniform. If it is not then we subdivide it and push new quads onto the stack.

public class QuadTree
{
private TerrainData _terrainData;
private Quad _root;

public QuadTree(TerrainData terrainData)
{
Debug.Log("Texture width and height" + terrainData.Width + " " + terrainData.Height);
_terrainData = terrainData;
_root = new Quad(0,0, terrainData.Width , terrainData.Height);
Stack<Quad> quadsToCheck = new Stack<Quad>();
quadsToCheck.Push(_root);
while (quadsToCheck.TryPop(out Quad quad))
{
if (!terrainData.IsQuadUniform(quad))
{
Quad[] children = quad.Subdivide();
foreach (Quad child in children)
{
if (child.IsDivisible())
{
quadsToCheck.Push(child);
}
}
}
}
}
}

Now let’s create our quad tree in DestructableTerrain.GenerateTerrain()

public class DestructableTerrain : MonoBehaviour
{
...
private QuadTree _quadTree;
...
public void GenerateTerrain()
{
...
_quadTree = new QuadTree(_terrainData);
}
}

Now when you press the “Generate Terrain” you should see a new log in your console. But that’s only a little helpfull. We will need some real gizmos for this one.

To do that we will need to iterate through every quad in our tree, so we need a method to access each quads child. Let’s add a following method to our Quad class.

public class Quad 
{
...

public bool HasChildren => _hasChildren;

...

public bool TryGetChildren(out Quad[] children)
{
if (_hasChildren)
{
children = _children;
return true;
}

children = null;
return false;
}
}

To iterate of those children effectively let’s create a new class called QuadEnumerator extending IEnumerator

Extending IEnumerator class is very usefull if you want to call foreach loop on an object

public class QuadEnumerator : IEnumerator<Quad>
{
Stack<Quad> _quadsToIterate = new Stack<Quad>();
private Quad _root;
private Quad _current;

public QuadEnumerator(Quad root)
{
_root = root;
_quadsToIterate.Push(_root);
}


public bool MoveNext()
{
if (_quadsToIterate.TryPop(out Quad quad))
{
_current = quad;
if (quad.TryGetChildren(out Quad[] children))
{
foreach (Quad child in children)
{
_quadsToIterate.Push(child);
}
}

return true;
}
return false;
}

public void Reset()
{
_quadsToIterate.Clear();
_current = _root;
if (_current.TryGetChildren(out Quad[] children))
{
foreach (Quad child in children)
{
_quadsToIterate.Push(child);
}
}
}

public Quad Current => _current;

object IEnumerator.Current => Current;

public void Dispose()
{
_quadsToIterate.Clear();
}
}

Modify our QuadTree class to extend IEnumerable<Quad> class

public class QuadTree : IEnumerable<Quad>
{
...
public IEnumerator<Quad> GetEnumerator()
{
return new QuadEnumerator(_root);
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

Our gizmos will be a bit complex and we do not want to mix editor and runtime code too much so let’s turn QuadTree into a partial class. Add partial keyword to original partial class declaration.

public partial class QuadTree : IEnumerable<Quad>
{
...
}

Let’s store all the editor specific code in a separate file called QuadTree.Editor file. We will wrtie our gizmos here.

public partial class QuadTree
{
#if UNITY_EDITOR
public void DrawGizmos()
{
Stack<Quad> quadsToDraw = new Stack<Quad>();
quadsToDraw.Push(_root);
DrawQuadGizmos(_root, Color.green);
foreach (Quad quad in this)
{
bool isSolid = _terrainData.IsSolid(quad);

Color color = isSolid ? Color.red : Color.blue;
DrawQuadGizmos(quad, color);
}
}

private void DrawQuadGizmos(Quad quad, Color color)
{
float x = quad.XAdress + (quad.Width/2f);
float y = quad.YAdress + (quad.Height/2f);;
Vector3 center = new Vector3(x, y, 0) ;
Vector3 size = new Vector3(quad.Width, quad.Height);
Gizmos.color = color;
Gizmos.DrawWireCube(center, size);
color.a = 0.4f;
Gizmos.color = color;
Gizmos.DrawCube(center, size);
}
#endif
}

Look how easily we can now iterate over all the quads using foreach loop! It will be quite usefull later when generating a mesh.

Let’s now call this DrawGizmos function from our DestructibleTerrain class instead of displaying TerrainData gizmos like we did before

public class DestructableTerrain : MonoBehaviour
{
...
private void OnDrawGizmosSelected()
{
if (_quadTree != null)
{
_quadTree.DrawGizmos(10f);
}
}
...
}

Now, the moment of truth… How does it look? Well…

QuadTree with visible gaps

It certainly looks a bit like our original map... But we have a lot of of weird gaps. Take into account that we are assuming in our quad subdivision script that both height and width are a power of two. That’s the kind of mistake that using gizmos helps you catch early!

Let’s try to account for odd numbers in our Quad.Subdivide() function. We will simply create new variables maxHeight and maxWidth that will correspond to width of right rectangles and height of top rectangles and we will increase those by one in the event that width or height is an odd number.

public class Quad 
{
...
public Quad[] Subdivide()
{

int halfWidth = _width / 2;
int halfHeight = _height / 2;
int widthOddFix = _width % 2;
int heightOddFix = _height % 2;
int maxHeight = halfHeight + heightOddFix;
int maxWidth = halfWidth + widthOddFix;
_children = new[]
{
new Quad(_xAdress, _yAdress, halfWidth, halfHeight),
new Quad(_xAdress + halfWidth, _yAdress, maxWidth, halfHeight),
new Quad(_xAdress, _yAdress + halfHeight, halfWidth, maxHeight),
new Quad(_xAdress + halfWidth, _yAdress + halfHeight, maxWidth, maxHeight )
};
_hasChildren = true;
return _children;
}
...
}

It looks much more concise now

Subotimal representation of our texture with weird proportions

But it is still a bit wierd… look how stretched out our rectangles are. I would prefer to see some nice squares. It happens because our quads inherit our textures proportions… that also means that height becomes indivisible way before width does and our spatial partion becomes suboptimal. The problem will exasporate with wider textures.

There has to be a better way!

And there is. In the event that either of the paramters is 2 or more times bigger than the other one we will subdivide the quad either horizontally or vertially intending to squarify our quads a bit.

public class Quad
{
...
public Quad[] Subdivide()
{
if (_width >= 2 * _height)
{
int quaterWidth = _width / 4;
_children = new[]
{
new Quad(_xAdress, _yAdress, quaterWidth, _height),
new Quad(_xAdress + quaterWidth, _yAdress, quaterWidth, _height),
new Quad(_xAdress + (quaterWidth*2), _yAdress, quaterWidth, _height),
new Quad(_xAdress+ (quaterWidth*3), _yAdress, quaterWidth, _height),
};
_hasChildren = true;
return _children;
}
else
{
int halfWidth = _width / 2;
int halfHeight = _height / 2;
_children = new[]
{
new Quad(_xAdress, _yAdress, halfWidth, halfHeight),
new Quad(_xAdress + halfWidth, _yAdress, halfWidth, halfHeight),
new Quad(_xAdress, _yAdress + halfHeight, halfWidth, halfHeight),
new Quad(_xAdress + halfWidth, _yAdress + halfHeight, halfWidth, halfHeight)
};
_hasChildren = true;
return _children;
}
}
}

Also, let’s revisit our IsDivisible() function to reflect this new reality

public class Quad
{
...
public bool IsDivisible()
{
return _width * _height >= 4;
}
}

And how does that look?

Imprecise representation of our texture

Well, it is bettter but we can achieve an even higher precision by breaking up 2x1 and 1x2 quads. Handling this expections will addd a bit of code to an already overblown function, so let’s sudivide this function into three new, one for each sudivision type.

public class Quad 
{
...
public Quad[] Subdivide()
{
if (_width >= 2 * _height)
{
DivideHorizontally();
}
else if(_height >= 2 * _width)
{
DivideVertically();
}
else
{
DivideEqually();
}
_hasChildren = true;
return _children;
}

private void DivideVertically()
{
if (_height == 2)
{
Debug.Log("Partial height subdivide");
_children = new[]
{
new Quad(_xAdress, _yAdress, _width, 1),
new Quad(_xAdress, _yAdress +1, _width, 1),
};
}
else
{
int heightOddFix = _height % 4;
int quarterHeight= _height / 4;
_children = new[]
{
new Quad(_xAdress, _yAdress, _width, quarterHeight),
new Quad(_xAdress, _yAdress+ quarterHeight, _width, quarterHeight),
new Quad(_xAdress, _yAdress + (quarterHeight*2), _width, quarterHeight),
new Quad(_xAdress, _yAdress +(quarterHeight*3), _width, quarterHeight + heightOddFix),
};
}
}

private void DivideHorizontally()
{
if (_width == 2)
{
Debug.Log("Partial width subdivide");
_children = new[]
{
new Quad(_xAdress, _yAdress, 1, _height),
new Quad(_xAdress + 1, _yAdress, 1, _height),
};
}
else
{
int widthOddFix = _width % 4;
int quarterWidth = _width / 4;
_children = new[]
{
new Quad(_xAdress, _yAdress, quarterWidth, _height),
new Quad(_xAdress + quarterWidth, _yAdress, quarterWidth, _height),
new Quad(_xAdress + (quarterWidth * 2), _yAdress, quarterWidth, _height),
new Quad(_xAdress + (quarterWidth * 3) + widthOddFix, _yAdress, quarterWidth, _height),
};
}
}

private void DivideEqually()
{
int widthOddFix = _width % 2;
int heightOddFix = _height % 2;
int halfWidth = _width / 2;
int halfHeight = _height / 2;
int maxHeight = halfHeight + heightOddFix;
int maxWidth = halfWidth + widthOddFix;
_children = new[]
{
new Quad(_xAdress, _yAdress, halfWidth, halfHeight),
new Quad(_xAdress + halfWidth, _yAdress, maxWidth, halfHeight),
new Quad(_xAdress, _yAdress + halfHeight, halfWidth, maxHeight),
new Quad(_xAdress + halfWidth, _yAdress + halfHeight, maxWidth, maxHeight )
};
}
...
}

We also have to change IsDivisible() function once more, to allow for this edge cases to be handled

public class Quad 
{
...
public bool IsDivisible()
{
return _width * _height >= 2;
}
...
}

At the end, that is finally, good enough.

Perfect representation of our texture

Mesh generation

With that let’s finnaly move on to generating meshes. As a start let’s add a MeshFilter field to our DestructableTerrain class. Let’s also make sure that you have a MeshRenderer attached to your object with material albedo set as your map texture.

public class DestructableTerrain : MonoBehaviour
{
[SerializeField]
private MeshFilter _meshFilter;
...
}

We will also usually have to scale our maps quite a bit so let’s add a variable for that.

public class DestructableTerrain : MonoBehaviour
{
[SerializeField]
private float _pixelsPerUnit = 10f;
...
}

We will have to iterate thorugh all of the quads and convert them into mesh elements if they don’t have children and their origin is solid ie. they are solid and uniform.

Let’s write the following function and call it from our GenerateTerrain method.

public class DestructableTerrain : MonoBehaviour
{
...
public void GenerateTerrain()
{
...
ConstructMeshes();
}

private void ConstructMeshes()
{
MeshContructionHelper meshContructionHelper = new MeshContructionHelper();
foreach (Quad quad in _quadTree)
{
if (!quad.HasChildren)
{
bool isQuadSolid = _terrainData.IsSolid(quad);

if (isQuadSolid)
{
//our mesh generation code will go here
}
}
}
_meshFilter.mesh = meshContructionHelper.ConstructMesh();
}
}

Wonderful, now we have to convert image coordinates into a VertexData struct for further processing. The code for that is pretty straightforward. Look how we are filling in the uv coordinates so that our mesh and texture aligns properly.

public class DestructableTerrain : MonoBehaviour
{
...
private VertexData ImageCoordinatesToVertex(int x, int y)
{
//we convert texture coordinates to uv's
//by dividing them by texture dimensions
float uvX = (x / (float) _texture2D.width);
float uvY = (y / (float) _texture2D.height);
//we center our position by substraction half of texture dimension
//from texture coordinate and scale it
Vector3 position = new Vector3(x - ( _texture2D.width/2f), y - ( _texture2D.height/2f), 0) / (_pixelsPerUnit);
return new VertexData()
{
Postion = position,
Uv = new Vector2(uvX, uvY),
Normal = transform.forward
};
}
}

Then, we need a function that will convert quad into corner coordinates and return vertice array.

    private VertexData[] GetQuadCorners(Quad quad)
{
VertexData[] vertices = new[]
{
//lower left corner
ImageCoordinatesToVertex(quad.XAdress, quad.YAdress),
//lower right corner
ImageCoordinatesToVertex(quad.XAdress + quad.Width, quad.YAdress),
//upper right corner
ImageCoordinatesToVertex(quad.XAdress + quad.Width, quad.YAdress + quad.Height),
//upper left corner
ImageCoordinatesToVertex(quad.XAdress, quad.YAdress + quad.Height),
};
return vertices;
}

We also add a simple function to add a quad to a MeshContructionHelper and finally call it from our ConstructMesh function

public class DestructableTerrain : MonoBehaviour
{
...
private void ConstructMeshes()
{
MeshContructionHelper meshContructionHelper = new MeshContructionHelper();
foreach (Quad quad in _quadTree)
{
if (!quad.HasChildren)
{
bool isQuadSolid = _terrainData.IsSolid(quad);

if (isQuadSolid)
{
ConstructQuad(quad, meshContructionHelper);
}
}
}
_meshFilter.mesh = meshContructionHelper.ConstructMesh();
}

private void ConstructQuad(Quad quad, MeshContructionHelper meshConstructionHelper)
{
VertexData[] vertices = GetQuadCorners(quad);
//right triagnle
meshConstructionHelper.AddMeshSection(vertices[0], vertices[2], vertices[1]);
//left triangle
meshConstructionHelper.AddMeshSection(vertices[0], vertices[3], vertices[2]);
}

}

This the final effect for me

Beautifull terrain

This is quite a nice terrain we got here. I think we can be reasonably proud of ourselves.

Let’s destroy it.

Destruction

This is were the fun begins.

We will destroy our terrain using mouse click position to determine which area will be destroyed. We wll use Trigonometry class that came with MeshGenerationEssentials to find intersection point between ray sent from our camera and our terrain

public class DestructableTerrain : MonoBehaviour
{
...
//this will be the range of terrain destruction when clicking
[SerializeField]
private float _destructionRadius =3f;

...
private Camera _camera;
//a debug variable to draw a gizmo
private Vector3 _mousePosition = Vector3.zero;

private void OnDrawGizmosSelected()
{
...
Gizmos.DrawSphere(_mousePosition, _destructionRadius);
}

private void Awake()
{
_camera = Camera.main;
}

private void Update()
{
//changing gizmo position after clikcikng
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePosition = _camera.ScreenToWorldPoint(Input.mousePosition + new Vector3(0,0,5f));
if (Trigonometry.PointIntersectsAPlane(_camera.transform.position,
mousePosition,
transform.position,
Vector3.forward,
out Vector3 result))
{
_mousePosition = result;
}
}
}
...
}

Let’s add a new function to our TerrainData to set values within certain range to false.

public class TerrainData
{
...
public void DestroyTerrain(int centerX, int centerY, int range)
{
int xOrigin = centerX - range;
int xEnd = centerX + range;
int yOrigin = centerY - range;
int yEnd = centerY + range;

for (int x = xOrigin; x < xEnd; x++)
{
for (int y = yOrigin; y < yEnd; y++)
{
_points[x, y] = false;
}
}
}
}

Now we have to convert our 3D mouse position into texture coords and pass it DestroyTerrain() function. After that we recreate the quadsand reconstruct the meshes.

public class DestructableTerrain : MonoBehaviour
{
...
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
Vector3 mousePosition = _camera.ScreenToWorldPoint(Input.mousePosition + new Vector3(0,0,5f));
if (Trigonometry.PointIntersectsAPlane(_camera.transform.position,
mousePosition,
transform.position,
Vector3.forward,
out Vector3 result))
{
_mousePosition = result;
DestroyArea(result);
}
}
}
...
private void DestroyArea(Vector3 position)
{
int x = (int) (position.x * _pixelsPerUnit) + (_texture2D.width/ 2);
int y = (int) (position.y * _pixelsPerUnit) + (_texture2D.height/2);

_terrainData.DestroyTerrain(x,y, (int) (_destructionRadius* _pixelsPerUnit));
_quadTree = new QuadTree(_terrainData);
ConstructMeshes();
}
...
}

If you try to click anywhere on the terrain right now after clicking play you will get an error messager. That is because our terrain data is not serialized and is cleared every time we run the game or reload the scene.

The best option would be to save our precalculated TerrainData as an asset, and if this tutorial get’s some following I will write an appendix for that, but wiriting this article already took too long, so let’s just regenerate our mesh on the awake as well.

public class DestructableTerrain : MonoBehaviour
{
...
private void Awake()
{
...
GenerateTerrain();
}

...
}

For me the effect is something like this.

Squares of death

Well it does not look so good, let’s try making it a bit more circular!

We just have to check whenever affected pixel is in a fixed distance from the destruction origin an we got ourselves a circle!

public class TerrainData
{
...
public void DestroyTerrain(int centerX, int centerY, int range)
{
...
for (int x = xOrigin; x < xEnd; x++)
{
for (int y = yOrigin; y < yEnd; y++)
{
int xDelta = x - centerX;
int yDelta = y - centerY;
float sqrDistance = (xDelta * xDelta) + (yDelta*yDelta);

if (sqrDistance < range * range)
{
_points[x, y] = false;
}
}
}
}
}

Idea for a reader:

If you wish you may create a script that will convert 2D textures into 2D matrices and then use this matrix as a filter for which pixels are destroyed in order to create unique destruction patterns.

I like this much more. It has more of this “Worms” feel to it doesn’t it? But there is one more problem. If you click on the edge of the texture you will get an index out o bounds exception. There is a simple fix for that.

public class TerrainData
{
...
public void DestroyTerrain(int centerX, int centerY, int range)
{
int xOrigin = Mathf.Max(0, centerX - range);
int xEnd = Mathf.Min(_width-1,centerX + range);
int yOrigin = Mathf.Max(0,centerY - range);
int yEnd = Mathf.Min(_height-1, centerY + range);
...
}
}

In the future

This is the end of this tutorial and so here is the repo for a completed project:

https://github.com/hesmeron/Destructible2DTerrain.git

However we still have some things to consider:

A: We don’t want ot recalculate terrain data every time we load the game. Ideally we shold create an TerrainData asset to store such information and ideally do that from a separate TerrainConverter window.

B: We didn’t work on this quad tree all along to now recalculate all the quads everytime we destroy part of our terrain. We can optimize that by recalculating only the affected quads.

C: We should calculate a collider for our terrain, but for now we can just use our current mesh in MeshCollider component.

D: At some point it would be nice to make terrain affected by gravity, and other forces. Like in Noita. I wold like to do that one day very much

I’m still looking for work so I have some time on my hands. Even so I try to finish my own game in the meantime and this tutorial took a lot of time to prepare and i don’t even know if anyone will care.

So I have to draw a finish line somwhere.

But if you woud like to see any of the topic listed above explored in detail just let me know it the comment. Also feel free to suggest any new topic for a tutorial.

The next article will probably be about path generation for npc-s. So if that sounds like something you would like to learn about, stay tuned, or not. You can do what you want.

I hope you had some fun on this journey, I certainly did.

--

--