So a while back I posted about Planetoids, my Asteroids inspired game where the rocks are actually sliced exactly where hit. I though to explain a little how I did that.

High-level

The basic idea is quite simple. Each rock is convex, so when hit there will be exactly two sides that get sliced. Then I create two new shapes, which I use to create the two sliced sides. These shapes will again be convex.

Finding the sides that got sliced and splitting them up can also be very easy. I generate the rocks by randomly selecting a few points on a circle, and connecting those. This gives a nicely looking convex rock with ordered points. It’s important that these points are ordered, that makes the slicing very simple. When slicing I run a simple line-line intersection algorithm, this calculates the points where the rock is hit, and the index of the side. Since the sides are ordered, I now create two new shapes, one with the points between the two hit sides (first-hit-index to second-hit-index), and one shape with the other side. Then I add both hit-points to both sides. And tada, we have two new rocks.

Read on to see a more in-depth explanation with code.

Set-up

Each planetoid has a slice method:

public Planetoid Slice( Ray2D laser, float laserPower, PlanetoidsManager manager ) { ...

It is called by the PlanetoidsManager, and returns the part that got sliced off, which the PlanetoidsManager will then add to it’s list so later it can also be sliced. I use a Ray2D for the laser, and a simple float for it’s power. This power is used to make the planetoids fly apart when hit.

Finding sides

First I transform the laser into the planetoid’s local space:

Vector2 start = transform.InverseTransformPoint(laser.origin); Vector2 end = transform.InverseTransformPoint( laser.origin + laser.direction * Laser.LASER_DISTANCE);

We iterate over all the sides and try to find hit-points:

SlicePoint[] slicePoints = new SlicePoint[2]; Vector2[] vertices = _collider.points; int foundPoints = 0; for (int i = 0; i < vertices.Length && foundPoints <= 2; ++i) { ...

(The SlicePoint class is just a combination of it’s index, and the actual point.)

For each side we see if it is sliced:

float x1 = start.x, x2 = end.x, x3 = vertices[i].x, x4 = vertices[(i+1)%vertices.Length].x; float y1 = start.y, y2 = end.y, y3 = vertices[i].y, y4 = vertices[(i+1)%vertices.Length].y; Vector2 intersectionPoint = new Vector2( ( ((x1*y2 - y1*x2)*(x3 - x4)) - ((x1-x2)*(x3*y4 - y3*x4)) ) / ( ((x1-x2)*(y3-y4)) - ((y1-y2)*(x3-x4)) ), ( ((x1*y2 - y1*x2)*(y3 - y4)) - ((y1-y2)*(x3*y4 - y3*x4)) ) / ( ((x1-x2)*(y3-y4))-((y1-y2)*(x3-x4)) ) );

For more information about the algorithm I used you can check Wikipedia. I also use x1, x2, … because that’s easier to write.

This code however assumes the sides are infinitely long, so we have to check if the hit was actually on the side, and if it is we add it to our slicePoints array:

float startx = Mathf.Min( vertices[i].x, vertices[(i+1)%vertices.Length].x); float endx = Mathf.Max( vertices[i].x, vertices[(i+1)%vertices.Length].x); float starty = Mathf.Min( vertices[i].y, vertices[(i+1)%vertices.Length].y); float endy = Mathf.Max( vertices[i].y, vertices[(i+1)%vertices.Length].y); float start2x = Mathf.Min(start.x, end.x); float end2x = Mathf.Max(start.x, end.x); float start2y = Mathf.Min(start.y, end.y); float end2y = Mathf.Max(start.y,end.y); if ( intersectionPoint.x >= startx && intersectionPoint.x < endx && intersectionPoint.y >= starty && intersectionPoint.y < endy && intersectionPoint.x >= start2x && intersectionPoint.x < end2x && intersectionPoint.y >= start2y && intersectionPoint.y < end2y ) { slicePoints[foundPoints] = new SlicePoint( intersectionPoint, i); ++foundPoints; }

Voila, now we have the sides that got sliced. If exactly two are found we have a hit and continue. No points means we missed this planetoids. It could happen only one point was found, if we shot almost exactly on one of the points or some rounding errors happened. This however almost never happens.

Creating new shapes

Now that we have the sides, we need to create the shapes of the two sides. First we calculate how many points each shape will have and create an array to remember these:

int numberOfPoints1 = Mathf.Abs( slicePoints[0].side - slicePoints[1].side); int numberOfPoints2 = vertices.Length - numberOfPoints1; Vector2[] sides1 = new Vector2[numberOfPoints1 + 2]; Vector2[] sides2 = new Vector2[numberOfPoints2 + 2];

As you can see, numberOfPoints is actually the number without the extra hit-points. This because we need those numbers later, and otherwise I would always have to substract 2.

Now we need to create the new shapes:

int counter = slicePoints[0].side +1; for (var i = 0; i < numberOfPoints1; ++i) { sides1[i] = vertices[counter]; ++counter; } sides1[numberOfPoints1 + 0] = slicePoints[1].point; sides1[numberOfPoints1 + 1] = slicePoints[0].point;

So first we add the points of the original rock between the two hit-indexes. Then we add the two extra points. Since I add them to the back of the array the second found hit-point will always be the first one that needs to be added to the array.

We create the second shape on exactly the same way:

for (var i = 0; i < numberOfPoints2; ++i) { counter = counter % vertices.Length; sides2[i] = vertices[counter]; ++counter; } sides2[numberOfPoints2 + 0] = slicePoints[0].point; sides2[numberOfPoints2 + 1] = slicePoints[1].point;

Here we make sure the counter wraps around the number of vertices, by using the modulo function. Also note the last two points are added reversed so the points stay ordered.

Creating the rocks

Now that we have the shapes, all that remains is creating the actual rocks. First we have to update the current one. I just use the shape both for the physics as for the visuals, generating a flat mesh based on these points.This happens in an ‘initialize’ function, so all I have to do is call it on the current rock:

Initialize(sides1, manager);

The method looks like this, and speaks for itself:

public void Initialize (Vector2[] points, PlanetoidsManager manager) { int sideCount = points.Length; Vector3[] pointsV3 = new Vector3[sideCount]; Color32[] colors = new Color32[sideCount]; for (int i = 0; i < sideCount; ++i) { pointsV3[i] = points[i]; colors[i] = _color; } // set collider _collider.points = points; // create triangles int[] triangles = new int[(sideCount-2)*3]; for (int i = 0; i < sideCount-2; ++i) { triangles[i*3] = 0; triangles[i*3+1] = i+2; triangles[i*3+2] = i+1; } // set mesh _meshFilter.mesh.Clear(); _meshFilter.mesh.vertices = pointsV3; _meshFilter.mesh.triangles = triangles; _meshFilter.mesh.colors32 = colors; // set mass float area = GetArea(points); _rigidbody.mass = _density * area; }

Next we create the new rock:

Planetoid newRock = manager.GetNewOrCashedPlanetoid(); newRock.Initialize(sides2, manager); newRock.transform.position = transform.position; newRock.transform.rotation = transform.rotation; newRock._rigidbody.velocity = linVel; newRock._rigidbody.angularVelocity = angleVel; newRock._density = _density;

The manager’s GetNewOrCashedPlanetoid() method cashed previously destroyed rocks and returns one if one is available. This to prevent needless instantiating to improve performance.

Finishing

And now we have the two sides. All that remains is adding the force of the laser:

_rigidbody.AddForce(lr, ForceMode2D.Impulse); newRock._rigidbody.AddForce(-lr, ForceMode2D.Impulse);

Lastly we return the new rock so the manager can do with it what it needs.

There, now you know how it all works ;)