// 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; } } } }