// Shatter Toolkit
// Copyright 2015 Gustav Olsson
using System.Collections.Generic;
using UnityEngine;
namespace ShatterToolkit
{
[RequireComponent(typeof(MeshFilter))]
public class ShatterTool : MonoBehaviour
{
[SerializeField]
protected int generation = 1;
[SerializeField]
protected int generationLimit = 3;
[SerializeField]
protected int cuts = 2;
[SerializeField]
protected bool fillCut = true;
[SerializeField]
protected bool sendPreSplitMessage = false;
[SerializeField]
protected bool sendPostSplitMessage = false;
[SerializeField]
protected HullType internalHullType = HullType.FastHull;
protected bool isIntact = true;
protected IHull hull;
protected Vector3 center;
///
/// Gets or sets the current generation of this ShatterTool instance.
/// By default, a game object is of generation 1. When a game object
/// is shattered using ShatterTool.Shatter() all new game objects
/// will be considered of generation 2, and so on.
/// For example, this value can be used to vary the color of a
/// game object depending on how many times it has been shattered.
///
public int Generation
{
get { return generation; }
set { generation = Mathf.Max(value, 1); }
}
///
/// Gets or sets the generation limit of this ShatterTool instance.
/// This value restricts how many times a game object may be shattered
/// using ShatterTool.Shatter(). A game object will only be able to shatter
/// if ShatterTool.Generation is less than ShatterTool.GenerationLimit.
///
public int GenerationLimit
{
get { return generationLimit; }
set { generationLimit = Mathf.Max(value, 1); }
}
///
/// Gets or sets the number of times the game object will be cut when ShatterTool.Shatter() occurs.
///
public int Cuts
{
get { return cuts; }
set { cuts = Mathf.Max(value, 1); }
}
///
/// Gets or sets whether the cut region should be triangulated.
/// If true, the connected UvMapper component will control the vertex properties of the filled area.
/// When the ShatterTool is used on double-sided meshes with zero thickness, such as planes, this value
/// should be false.
///
public bool FillCut
{
get { return fillCut; }
set { fillCut = value; }
}
///
/// Gets or sets whether a PreSplit(Plane[] planes) message should be sent to the original game object prior to a split occurs.
/// The supplied object will be the array of Planes that will be used to split the game object.
///
public bool SendPreSplitMessage
{
get { return sendPreSplitMessage; }
set { sendPreSplitMessage = value; }
}
///
/// Gets or sets whether a PostSplit(GameObject[] newGameObjects) message should be sent to the original game object
/// after a split has occured. The message will be sent before destroying the original game object.
/// The supplied object will be an array of all new GameObjects created during the split.
///
public bool SendPostSplitMessage
{
get { return sendPostSplitMessage; }
set { sendPostSplitMessage = value; }
}
///
/// Gets or sets the type of the internal hull used to shatter the mesh. The FastHull implementation is roughly 20-50% faster
/// than the LegacyHull implementation and requires no time to startup. The LegacyHull implementation is more robust in extreme
/// cases and is provided for backwards compatibility. This setting can't be changed during runtime.
///
public HullType InternalHullType
{
get { return internalHullType; }
set { internalHullType = value; }
}
///
/// Determines whether this game object is of the first generation. (Generation == 1)
///
public bool IsFirstGeneration
{
get { return generation == 1; }
}
///
/// Determines whether this game object is of the last generation. (Generation >= GenerationLimit)
///
public bool IsLastGeneration
{
get { return generation >= generationLimit; }
}
///
/// Gets the worldspace center of the game object. Only works during runtime.
///
public Vector3 Center
{
get { return transform.TransformPoint(center); }
}
protected void CalculateCenter()
{
// Get the localspace center of the mesh bounds
center = GetComponent().sharedMesh.bounds.center;
}
public void Start()
{
Mesh sharedMesh = GetComponent().sharedMesh;
// Initialize the first generation hull
if (hull == null)
{
if (internalHullType == HullType.FastHull)
{
hull = new FastHull(sharedMesh);
}
else if (internalHullType == HullType.LegacyHull)
{
hull = new LegacyHull(sharedMesh);
}
}
// Update properties
CalculateCenter();
}
///
/// Shatters the game object at a point, instantiating the pieces as new
/// game objects (clones of the original) and destroying the original game object when finished.
/// If the game object has reached the generation limit, nothing will happen.
/// Apart from taking the generation into account, this is equivalent to calling
/// ShatterTool.Split() using randomly generated planes passing through the point.
///
///
/// The world-space point.
///
public void Shatter(Vector3 point)
{
if (!IsLastGeneration)
{
// Increase generation
generation++;
// Split the hull using randomly generated planes passing through the point
Plane[] planes = new Plane[cuts];
for (int i = 0; i < planes.Length; i++)
{
planes[i] = new Plane(Random.onUnitSphere, point);
}
Split(planes);
}
}
///
/// Splits the game object using an array of planes, instantiating the pieces as new
/// game objects (clones of the original) and destroying the original game object when finished.
///
///
/// An array of world-space planes with unit-length normals.
///
public void Split(Plane[] planes)
{
if (planes != null && planes.Length > 0 && isIntact && hull != null && !hull.IsEmpty)
{
UvMapper uvMapper = GetComponent();
ColorMapper colorMapper = GetComponent();
if (sendPreSplitMessage)
{
SendMessage("PreSplit", planes, SendMessageOptions.DontRequireReceiver);
}
Vector3[] points, normals;
ConvertPlanesToLocalspace(planes, out points, out normals);
IList newHulls;
CreateNewHulls(uvMapper, colorMapper, points, normals, out newHulls);
GameObject[] newGameObjects;
CreateNewGameObjects(newHulls, out newGameObjects);
if (sendPostSplitMessage)
{
SendMessage("PostSplit", newGameObjects, SendMessageOptions.DontRequireReceiver);
}
Destroy(gameObject);
isIntact = false;
}
}
protected void ConvertPlanesToLocalspace(Plane[] planes, out Vector3[] points, out Vector3[] normals)
{
points = new Vector3[planes.Length];
normals = new Vector3[planes.Length];
for (int i = 0; i < planes.Length; i++)
{
Plane plane = planes[i];
Vector3 localPoint = transform.InverseTransformPoint(plane.normal * -plane.distance);
Vector3 localNormal = transform.InverseTransformDirection(plane.normal);
localNormal.Scale(transform.localScale);
localNormal.Normalize();
points[i] = localPoint;
normals[i] = localNormal;
}
}
protected void CreateNewHulls(UvMapper uvMapper, ColorMapper colorMapper, Vector3[] points, Vector3[] normals, out IList newHulls)
{
newHulls = new List();
// Add the starting hull
newHulls.Add(hull);
for (int j = 0; j < points.Length; j++)
{
int previousHullCount = newHulls.Count;
for (int i = 0; i < previousHullCount; i++)
{
IHull previousHull = newHulls[0];
// Split the previous hull
IHull a, b;
previousHull.Split(points[j], normals[j], fillCut, uvMapper, colorMapper, out a, out b);
// Update the list
newHulls.Remove(previousHull);
if (!a.IsEmpty)
{
newHulls.Add(a);
}
if (!b.IsEmpty)
{
newHulls.Add(b);
}
}
}
}
protected void CreateNewGameObjects(IList newHulls, out GameObject[] newGameObjects)
{
// Get new meshes
Mesh[] newMeshes = new Mesh[newHulls.Count];
float[] newVolumes = new float[newHulls.Count];
float totalVolume = 0.0f;
for (int i = 0; i < newHulls.Count; i++)
{
Mesh mesh = newHulls[i].GetMesh();
Vector3 size = mesh.bounds.size;
float volume = size.x * size.y * size.z;
newMeshes[i] = mesh;
newVolumes[i] = volume;
totalVolume += volume;
}
MeshFilter meshFilter = GetComponent();
MeshCollider meshCollider = GetComponent();
Rigidbody rigidbody = GetComponent();
// Remove mesh references to speed up instantiation
meshFilter.sharedMesh = null;
if (meshCollider != null)
{
meshCollider.sharedMesh = null;
}
// Create new game objects
newGameObjects = new GameObject[newHulls.Count];
for (int i = 0; i < newHulls.Count; i++)
{
IHull newHull = newHulls[i];
Mesh newMesh = newMeshes[i];
float volume = newVolumes[i];
GameObject newGameObject = (GameObject)Instantiate(gameObject);
// Set shatter tool
ShatterTool newShatterTool = newGameObject.GetComponent();
if (newShatterTool != null)
{
newShatterTool.hull = newHull;
}
// Set mesh filter
MeshFilter newMeshFilter = newGameObject.GetComponent();
if (newMeshFilter != null)
{
newMeshFilter.sharedMesh = newMesh;
}
// Set mesh collider
MeshCollider newMeshCollider = newGameObject.GetComponent();
if (newMeshCollider != null)
{
newMeshCollider.sharedMesh = newMesh;
}
// Set rigidbody
Rigidbody newRigidbody = newGameObject.GetComponent();
if (rigidbody != null && newRigidbody != null)
{
newRigidbody.mass = rigidbody.mass * (volume / totalVolume);
if (!newRigidbody.isKinematic)
{
newRigidbody.velocity = rigidbody.GetPointVelocity(newRigidbody.worldCenterOfMass);
newRigidbody.angularVelocity = rigidbody.angularVelocity;
}
}
// Update properties
newShatterTool.CalculateCenter();
newGameObjects[i] = newGameObject;
}
}
}
}