Destructible Terrain in Ferr2D

Describes how to modify Ferr2D to create destructible terrain. The Ferr2D source code isn't mine to distribute, so I won't be including any source code with this post. Anyone wanting to reproduce this will have to buy Ferr2D and manually modify the source.

Warning: this article hasn’t been updated for the most recent version of Ferr2D.

This post will describe how I modified Ferr2D to create terrain that is destructible. The Ferr2D source code isn’t mine to distribute, so I won’t be including any source code with this post. Anyone wanting to reproduce this will have to buy Ferr2D and manually modify the source.

Ferr2D: Unity Asset Store

Disclaimer: This is a quick hack to get Ferr2D to break up the terrain it produces into destructible chunks. There’s probably a much better way to incorporate destructible terrain into Ferr2D. Also, this code will only create destructible terrain for terrain objects with a fill type of “closed”.

Setup

If you haven’t done so already, import Ferr2D into your project, and create a new “Decorative 2D Terrain”. Set its Fill Type to “Closed”, and create a shape that has more than just 2 points.

Modifying the PathTerrainEditor

First, we’re going to create the GUI for the destructible terrain. Open Ferr2DT_PathTerrainEditor.cs in your IDE.

At the top of the file, add a public member called showDestructible:

public class Ferr2DT_PathTerrainEditor : Editor {
  ...
  bool showDestructible = true;
  ...
}

In the method OnInspectorGUI at line 169, add the following code:

showDestructible = EditorGUILayout.Foldout(showDestructible, "DESTRUCTIBLE");
if (showDestructible)
{
  EditorGUI.indentLevel = 2;
  sprite.isDestructible = EditorGUILayout.Toggle("Is Destructible", sprite.isDestructible);
  sprite.chunkSizeX = EditorGUILayout.FloatField("Chunk Size X", Mathf.Max(sprite.chunkSizeX, 0.05f));
  sprite.chunkSizeY = EditorGUILayout.FloatField("Chunk Size Y", Mathf.Max(sprite.chunkSizeY, 0.05f));
  sprite.chunkJitterX = EditorGUILayout.FloatField("Chunk Size Jitter X", sprite.chunkJitterX);
  sprite.chunkJitterY = EditorGUILayout.FloatField("Chunk Size Jitter Y", sprite.chunkJitterY);
  EditorGUI.indentLevel = 0;
}

In Ferr2DT_PathTerrain.cs, add the following public members:

public bool isDestructible = false;
public float chunkSizeX = 1f;
public float chunkSizeY = 1f;
public float chunkJitterX = 0.3f;
public float chunkJitterY = 0.3f;

Modifying PathTerrain and DynamicMesh

When an instance of PathTerrain is modified in PathTerrainEditor, a call to the RecreatePath method in Ferr2DT_PathTerrain.cs is made. This is where we’ll check to see if isDestructible is true and handle the (re)creation of destructible chunks. The way I’ve chosen to handle the creation of the chunks, is to create a new child object on the terrain object named “DestructibleContainer” that will act as a container for all the chunks. Every time RecreatePath is called and isDestructible is true, DestructibleContainer will be deleted and recreated.

At the top of RecreatePath, add the local variable:

Transform destructibleContainerTransform = transform.FindChild("DestructibleContainer");

Before the call to MatchOverride(), handle the destruction of an existing container object:

// Destroy destructible object container if it exists
if (destructibleContainerTransform != null)
{
  DestroyImmediate(destructibleContainerTransform.gameObject);
}

Now we need to add a conditional before the line with the comment compile the mesh!.

if (isDestructible)
{
  RecreateDestructibleChunks();
  GetComponent<MeshFilter>().sharedMesh.Clear();
}
else
{
  // compile the mesh!
  // everything from here to the end of the RecreatePath method
  // should be enclosed in this part of the conditional
  ...
}

Now we’ll create the RecreateDestructibleChunks method. I’ll just go ahead and post this method in its entirety, and we’ll fill in the missing pieces later.

private void RecreateDestructibleChunks()
{
  MeshRenderer mainMeshRenderer = GetComponent<MeshRenderer>();
  Transform containerTransform = transform.FindChild("DestructibleContainer");
  BaseDestructibleScript sourceDestructibleScript = GetComponent<BaseDestructibleScript>();
  int triangleCount = dMesh.indices.Count / 3;
  int indexCounter = 0;
  GameObject container = null;

  if (sourceDestructibleScript == null)
  {
    Debug.LogError("Destructible terrains need a BaseDestructibleScript component.");
    return;
  }

  // Create container object if it doesn't exist
  if (containerTransform == null)
  {
    container = new GameObject("DestructibleContainer");
    container.transform.parent = transform;
    container.transform.localPosition = Vector3.zero;
  }

  for (int i = 0; i < triangleCount; i++)
  {
    int i1 = dMesh.indices[indexCounter++];
    int i2 = dMesh.indices[indexCounter++];
    int i3 = dMesh.indices[indexCounter++];
    Vector3 p1 = dMesh.verts[i1];
    Vector3 p2 = dMesh.verts[i2];
    Vector3 p3 = dMesh.verts[i3];
    Vector3 center = (p1 + p2 + p3) / 3f;
    Vector2 uv1 = dMesh.uvs[i1];
    Vector2 uv2 = dMesh.uvs[i2];
    Vector2 uv3 = dMesh.uvs[i3];
    bool isBody = dMesh.isBodyIndex(i1) && dMesh.isBodyIndex(i2) && dMesh.isBodyIndex(i3);
    GameObject chunk = new GameObject((isBody ? "BodyChunk" : "TrimChunk") + i);
    MeshFilter meshFilter = chunk.AddComponent<MeshFilter>();
    MeshRenderer meshRenderer = chunk.AddComponent<MeshRenderer>();
    Material material = mainMeshRenderer.sharedMaterials[isBody ? 0 : 1];
    PolygonCollider2D polygonCollider = chunk.AddComponent<PolygonCollider2D>();
    Rigidbody2D body = chunk.AddComponent<Rigidbody2D>();
    Mesh mesh = new Mesh();

    p1 -= center;
    p2 -= center;
    p3 -= center;

    chunk.transform.parent = container.transform;
    chunk.transform.localPosition = center;
    chunk.layer = gameObject.layer;

    polygonCollider.points = new Vector2[] { p1, p2, p3 };

    body.isKinematic = true;
    body.mass = 0.2f;

    mesh.vertices = new Vector3[] { p1, p2, p3 };
    mesh.uv = new Vector2[] { uv1, uv2, uv3 };
    mesh.triangles = new int[] { 0, 1, 2 };
    mesh.RecalculateNormals();
    mesh.RecalculateBounds();
    mesh.colors = new Color[] { vertexColor, vertexColor, vertexColor };
    meshFilter.mesh = mesh;

    meshRenderer.sharedMaterial = material;
  }
}

Alright, that block of code is referencing a bunch of missing code. So let’s fill in the blanks. First, lets expose a few member variables in Ferr2DT_DynamicMesh.cs. In the Fields and Properties region, add:

public List<Vector3> verts { get { return mVerts; } }
public List<int> indices { get { return mIndices; } }
public List<Vector2> uvs { get { return mUVs; } }
public List<Color> colors { get { return mColors; } }

Next, lets write the isBodyIndex method. In the Fields and Properties region, add:

List<int> mBodyIndices;

Initialize it in the constructor Ferr2DT_DynamicMesh():

mBodyIndices = new List<int>();

In the General Methods region, add:

public bool isBodyIndex(int index)
{
  return mBodyIndices.Contains(index);
}

We also need to add mBodyIndices to Ferr2DT_DynamicMesh ’s Clear method:

public void  Clear                 ()
{
  ...
        mBodyIndices.Clear();
  ...
}

Now we’re going to modify Ferr2DT_DynamicMesh ’s AddFace methods, so we can distinguish between edge and body/fill faces. There are two AddFace methods. For the first, add an extra parameter to the end of the method:

public void AddFace(int aV1, int aV2, int aV3, bool isOnBody) {

At the end of the first AddFace method, add the following:

if (isOnBody)
{
  mBodyIndices.Add(aV1);
  mBodyIndices.Add(aV2);
  mBodyIndices.Add(aV3);
}

Add the same parameter to the end of the second AddFace method:

public void AddFace(int aV1, int aV2, int aV3, int aV4, bool isOnBody) {

At the end of the second AddFace method, add the following:

if (isOnBody)
{
  mBodyIndices.Add(aV1);
  mBodyIndices.Add(aV2);
  mBodyIndices.Add(aV3);
  mBodyIndices.Add(aV4);
}

Now we need to modify all the calls to AddFace, and determine from the context they’re used whether they’re being used to add edge faces, or body/fill faces.

In Ferr2DT_DynamicMesh, there’s a method called ExtrudeZ with four calls to AddFace. ExtrudeZ, as far as I know, is only called when dealing with edges. So we’ll add a false to the end of all four calls.

The rest of the calls to AddFace are in Ferr2DT_PathTerrain. There’s three in the SlicedQuad method. Once again, as far as I can tell this method is only ever called when dealing with edges. So add false to the end of all AddFace calls in this method.

The next calls to AddFace are in AddCap. Caps are only used on edges, so add false to the end of all AddFace calls in this method.

The next call to AddFace is in AddFill. AddFill is a method that breaks up the filled portion of the terrain into triangles, and adds those faces to the dynamic mesh. That means that this is part of what I’m calling the “body”, so add true to the AddFace call in this method.

All calls to AddFace should be updated now.

Let’s go ahead and create an empty BaseDestructibleScript for now. I’ll explain what it’s for later.

Modifying the Triangulation

Up until now we’ve been setting up the code for the main changes that make the destructible terrain possible: a different method of triangulation. Generally, when a polygon is decomposed into triangles, the input is a list of points that define the edges of the polygon. Poly2Tri supports a type of point called a “Steiner Point”, that we can add to the polygon after we add the points that define the edge. We will be placing the steiner points inside the polygon using the spacing defined by the chunkSize and chunkJitter variables we added to PathTerrain earlier. After we add those points and decompose the polygon, we will be given a list of triangles that has been broken up into a bunch of small, fairly uniform, chunks.

Unfortunately, the triangulation library that Ferr2D uses (LibTessDotNet) doesn’t support Steiner points. So we’ll have to include Poly2Tri, and make a separate code path for triangulation when isDestructible is true.

Checkout the source for Poly2Tri: https://code.google.com/p/poly2tri/source/checkout?repo=cs . You’ll probably have to install a mercurial client to check it out.

Once you have the Poly2Tri source, create a new folder in your Unity assets named “Poly2Tri”. Drag the following files/folders into it:

Now we need to create the new triangulation method. In Ferr2DT_Triangulator.cs add “using Poly2Tri;” to the top of the file. Then, in the Public Methods region, add:

public static void GetVerticesAndIndices(List<Vector2> inputPoints, out List<Vector2> outputPoints, out List<int> outputIndices, float chunkWidth, float chunkHeight, float jitterX, float jitterY)
{
  Polygon poly;
  List<PolygonPoint> points = new List<PolygonPoint>();
  List<Vector2> interpolatedPoints = new List<Vector2>();
  float edgeCutLength = (chunkWidth + chunkHeight) * 0.5f;
  int triangleIndex = 0;
  int numChunksX;
  int numChunksY;

  outputPoints = new List<Vector2>();
  outputIndices = new List<int>();

  // Cut all the edges into smaller segments (so triangulation won't create long, thin shards along the edges)
  for (int i = 0; i < inputPoints.Count; i++)
  {
    Vector2 a;
    Vector2 b;
    float distance;
    int numCuts;
    float cutLength;

    a = i == 0 ? inputPoints[inputPoints.Count - 1] : inputPoints[i - 1];
    b = inputPoints[i];

    distance=  (b - a).magnitude;
    numCuts = (int)(distance / edgeCutLength);
    cutLength = distance / (float)numCuts;

    for (int j = 0; j < numCuts; j++)
    {
      Vector2 p = Vector2.Lerp(a, b, Mathf.Max(0f, Mathf.Min(1f, (j * cutLength / distance))));

      interpolatedPoints.Add(p);
    }
  }

  // Convert points to P2T, and create polygon
  foreach (Vector2 p in interpolatedPoints)
  {
    points.Add(new PolygonPoint(p.x, p.y));
  }
  poly = new Polygon(points);

  // Calculate number of chunks to split the polygon up into
  numChunksX = (int)(poly.Bounds.Width / chunkWidth);
  numChunksY = (int)(poly.Bounds.Height / chunkHeight);

  // Add steiner points (this is the reason for using Poly2Tri)
  UnityEngine.Random.seed = (int)(poly.Bounds.Left * poly.Bounds.Top);
  for (int i = 0; i < numChunksX; i++)
  {
    for (int j = 0; j < numChunksY; j++)
    {
      TriangulationPoint p = new TriangulationPoint(
          i * chunkWidth + poly.Bounds.Left + UnityEngine.Random.Range(-jitterX, jitterX),
          j * -chunkHeight + poly.Bounds.Top + UnityEngine.Random.Range(-jitterY, jitterY));

      if (poly.IsPointInside(p))
      {
          poly.AddSteinerPoint(p);
      }
    }
  }

  // Triangulate
  P2T.Triangulate(poly);

  // Build output from triangulated polygon
  foreach (DelaunayTriangle triangle in poly.Triangles)
  {
    TriangulationPoint p1 = triangle.Points[0];
    TriangulationPoint p2 = triangle.PointCWFrom(p1);
    TriangulationPoint p3 = triangle.PointCWFrom(p2);

    outputPoints.Add(new Vector2(p1.Xf, p1.Yf));
    outputPoints.Add(new Vector2(p2.Xf, p2.Yf));
    outputPoints.Add(new Vector2(p3.Xf, p3.Yf));
    outputIndices.Add(triangleIndex++);
    outputIndices.Add(triangleIndex++);
    outputIndices.Add(triangleIndex++);
  }
}

Now we have to use our new triangulation method. In order to do that, we’re going to have to modify AddFill in Ferr2DT_PathTerrain.cs.

At the top of AddFill, add the local variable:

List<int> indices;

Remove the line:

List<int> indices = Ferr2DT_Triangulator.GetIndices(ref fillVerts, true, fill == Ferr2DT_FillMode.InvertedClosed);

Replace everything after the line:

int       offset  = dMesh.VertCount;

with:

if (isDestructible && fill != Ferr2DT_FillMode.InvertedClosed)
{
  List<Vector2> newFillVerts;

  Ferr2DT_Triangulator.GetVerticesAndIndices(fillVerts, out newFillVerts, out indices, chunkSizeX, chunkSizeY, chunkJitterX, chunkJitterY);
  fillVerts = newFillVerts;
}
else
{
  indices = Ferr2DT_Triangulator.GetIndices(ref fillVerts, true, fill == Ferr2DT_FillMode.InvertedClosed);
}

for (int i = 0; i < fillVerts.Count; i++) {
  dMesh.AddVertex(fillVerts[i].x, fillVerts[i].y, fillZ, fillVerts[i].x / scale.x, fillVerts[i].y / scale.y);
}
for (int i = 0; i < indices.Count; i+=3) {
  try {
    dMesh.AddFace(indices[i    ] + offset,
                  indices[i + 1] + offset,
                  indices[i + 2] + offset,
                  true);
  } catch {

  }
}

What this is doing, is creating a separate code path for the new Poly2Tri triangulation method. If the fill mode isn’t “Inverted Closed”, and “Is Destructible” is checked, we use the Poly2Tri triangulation method. Otherwise, we use the original triangulation method.

Chunk Behaviour

At this point, the destructible terrain is ready to be used. That’s where BaseDestructibleScript comes into play. This is a script that will be copied to each individual chunk, and will define how to react to being broken. To handle the copying of the script to the chunks, we need to add some code to Ferr2DT_PathTerrain ’s Start method:

if (isDestructible)
{
  CopyDestructibleScript();
}

Here’s the CopyDestructibleScript method:

private void CopyDestructibleScript()
{
  BaseDestructibleScript source = GetComponent<BaseDestructibleScript>();
  GameObject container = transform.FindChild("DestructibleContainer").gameObject;

  foreach (Transform child in container.transform)
  {
    BaseDestructibleScript dest = (BaseDestructibleScript)child.gameObject.AddComponent(source.GetType().ToString());

    dest.copyFrom(source);
  }
}

And here’s my BaseDestructibleScript code (contains a little bit of game-specific code):

public class BaseDestructibleScript : MonoBehaviour 
{
  public List<string> tags;
  public PhysicsMaterial2D chunkPhysicsMaterial;

  virtual public void onCollision(GameObject collisionObject)
  {
    ShrinkAndDestroyScript shrinkAndDestroy = gameObject.AddComponent<ShrinkAndDestroyScript>();
    Vector2 velocity = collisionObject.rigidbody2D.velocity;

    gameObject.layer = LayerMask.NameToLayer("Default");
    transform.parent = null;
    rigidbody2D.isKinematic = false;
    rigidbody2D.AddForce(velocity * 100f + Random.insideUnitCircle * 50f);
    rigidbody2D.AddTorque(Random.Range(-1f, 1f) * 50f);
    shrinkAndDestroy.time = 0.5f;
  }

  virtual public void copyFrom(BaseDestructibleScript source)
  {
    tags = source.tags;
    chunkPhysicsMaterial = source.chunkPhysicsMaterial;

    collider2D.sharedMaterial = chunkPhysicsMaterial;
  }
}

The main thing to focus on is the onCollision call. It’s up to you to figure out how you want to call this method. I’ll explain how I call it, but I have to warn that it’s fairly convoluted because of Unity’s terrible collision management.

The easiest way to call onCollision would be to add a OnCollisionEnter2D method to BaseDestructibleScript, and call it from there. The problem with doing it that way is that the object colliding with it will usually only break off one chunk at a time, since the collision is resolved and prevents the colliding object from penetrating further. Having a rock that smashes halfway through a destructible terrain before stopping is pretty much impossible using this method.

Box2D has a PreSolve callback that allows you to detect a collision and disable it before the collision is actually resolved. Unfortunately, Unity doesn’t have that and requires you to fiddle around with different physics layers (which is a horribly frustrating alternative, in my opinion).

So what I do, is create two new physics layers. One for DestructibleTerrain, and one for TerrainDestructor (rocks, dynamite, etc..).
Set the TerrainDestructor and DestructibleTerrain layers to not collide with each other. Then add a TerrainDestructorScript to any object you want to destroy terrain, and have it search for game objects on the DestructibleTerrain layer using Physics2D.OverlapCircleAll. For each result, check the BaseDestructibleScript to see if the destructor’s tag is in the terrain’s list of acceptable tags. If it is, call its onCollision method.

Like I said, terribly convoluted. But so far it’s the only workaround I’ve found to not having a PreSolve callback.

Cleaning Up

Since the original mesh isn’t being used anymore, we can get rid of it at runtime. In Ferr2DT_PathTerrain ’s Start method, add:

if (isDestructible)
{
  Destroy(GetComponent<BaseDestructibleScript>());
}

Feel free to comment or shoot me an email if this didn’t work for you, or if something’s unclear. Email would probably be better, as my comment system has been ravaged by spammers recently, and I might not see the notice from legitimate commenters.