using System.Collections; using System.Collections.Generic; using UnityEngine; /// /// Runtime-fractures the attached mesh object using OpenFracture's , /// then pushes every produced fragment toward a configurable direction so the pieces /// "drift"/fly off. Call (or to /// override the direction) from your own gameplay code. /// [RequireComponent(typeof(MeshFilter))] [RequireComponent(typeof(MeshRenderer))] [RequireComponent(typeof(Rigidbody))] public class FractureAndDrift : MonoBehaviour { /// /// How the cut-plane normal is chosen at each subdivision step. /// public enum SliceMode { /// Original OpenFracture behaviour: random normal over the enabled axes. Random, /// Always cut perpendicular to the fragment's current longest axis. Best for avoiding stretched /// pieces on long/thin objects, because long dimensions get subdivided first. LongestAxis, /// Random normal biased by . Higher weight on an axis produces more cuts /// perpendicular to it, i.e. that dimension is subdivided more finely. AxisWeighted } [Header("Fracture")] [Tooltip("Fragment count / axes / inside material. Same options used by OpenFracture's Fracture component.")] public FractureOptions fractureOptions = new FractureOptions(); [Tooltip("Random = stock OpenFracture. LongestAxis = always cut the longest side first (best anti-stretch). " + "AxisWeighted = bias cuts toward the axes with higher weight.")] public SliceMode sliceMode = SliceMode.Random; [Tooltip("Only used in AxisWeighted mode. Higher weight on an axis = that dimension is subdivided more (more cuts perpendicular to it). " + "e.g. (3,1,1) cuts a long-X object roughly 3x more along X.")] public Vector3 axisWeights = Vector3.one; [Tooltip("Only used in LongestAxis mode. Random tilt (0..1) added to the cut plane so repeated cuts aren't perfectly parallel. 0 = perfectly axis-aligned cuts.")] [Range(0f, 1f)] public float longestAxisJitter = 0.15f; [Tooltip("If true, logs what the controlled fracture actually did (source mesh stats, how many slices " + "succeeded vs came back empty, final fragment count). Turn this on once to diagnose 'still big blocks'.")] public bool fractureDebugLogs = false; [Header("Drift")] [Tooltip("Direction the fragments fly toward. Interpreted in the space set by 'Direction Is Local'.")] public Vector3 driftDirection = Vector3.up; [Tooltip("If true, driftDirection is relative to this object's rotation; if false it is world space.")] public bool directionIsLocal = false; [Tooltip("Base speed (m/s) applied to each fragment along the drift direction.")] public float driftSpeed = 5f; [Tooltip("Extra random speed [0..value] added on top of driftSpeed for a natural burst.")] public float driftSpeedRandomness = 1.5f; [Tooltip("Random sideways scatter (cone half-angle in degrees) around the drift direction. 0 = perfectly straight.")] [Range(0f, 90f)] public float scatterAngle = 15f; [Tooltip("Extra outward push (m/s) from the fracture center, gives an explosion feel. 0 = disabled.")] public float explosionSpeed = 0f; [Tooltip("Random angular velocity (rad/s) applied so fragments tumble.")] public float spinSpeed = 4f; [Header("Source Physics")] [Tooltip("If true, the source object's Rigidbody is frozen (kinematic, no gravity) until Shatter() is called. " + "This stops the object from falling/jittering under physics before it breaks. Highly recommended: the " + "source body is only used to compute fragment mass, it does not need to simulate before shattering.")] public bool freezeSourceUntilShatter = true; [Header("Fragment Physics")] [Tooltip("If true, fragments are affected by Unity's global gravity (always straight down) after being launched. " + "Ignored when 'Use Custom Gravity' is enabled.")] public bool fragmentsUseGravity = true; [Tooltip("Caps how fast PhysX may push a fragment when it resolves an overlap (depenetration). This is the REAL " + "fix for fragments 'accelerating'/exploding: freshly cut convex hulls overlap each other and any nearby " + "geometry, and by default PhysX separates them with an almost unlimited velocity, injecting huge energy. " + "A small value (1-2) makes overlaps resolve gently. Applies regardless of collision layers/ignores.")] public float maxDepenetrationVelocity = 1f; [Tooltip("If true, the freshly spawned fragments collide with each other. Leave OFF for a clean drift: at birth " + "the convex fragment hulls overlap, and Unity resolves that overlap with large depenetration impulses " + "that blast the pieces apart. Ignoring sibling collisions removes that explosion.")] public bool fragmentsCollideWithEachOther = false; [Tooltip("If true, fragments will NOT collide with other, still-unshattered FractureAndDrift objects in the scene. " + "Keep ON: otherwise fragments smash into the solid neighbouring objects and fly off chaotically, which is " + "why earlier-triggered objects looked wrong while the last-triggered one (no solid neighbours left) drifted cleanly.")] public bool ignoreOtherSourceObjects = true; [Tooltip("If true, fragments ignore Unity's global gravity and are instead continuously pulled toward " + "'Custom Gravity' every physics step. Use this to make pieces drift toward an arbitrary direction.")] public bool useCustomGravity = false; [Tooltip("Acceleration vector (m/s^2) applied to every fragment each physics step when 'Use Custom Gravity' is on. " + "e.g. (0,-9.81,0) = normal down; (5,0,0) = pulled toward +X; interpreted in the space set by 'Custom Gravity Is Local'.")] public Vector3 customGravity = new Vector3(0f, -9.81f, 0f); [Tooltip("If true, 'Custom Gravity' is relative to this object's rotation at shatter time; if false it is world space.")] public bool customGravityIsLocal = false; [Header("Cleanup")] [Tooltip("Fallback: if nothing calls ReclaimFragments() first, the fragments are auto-reclaimed this many " + "seconds after shattering. <= 0 keeps them forever (until something calls ReclaimFragments()).")] public float fragmentLifetime = 5f; [Tooltip("When reclaiming, fragments are destroyed one-by-one spread across this many seconds instead of all " + "at once. This is the 'async collect' behaviour. 0 = destroy the whole batch together.")] public float reclaimStagger = 1f; [Tooltip("If true, each fragment smoothly shrinks to nothing before being destroyed, for a graceful collect " + "instead of a hard pop.")] public bool reclaimShrink = true; [Tooltip("If true, this GameObject is deactivated after shattering (mirrors OpenFracture behaviour).")] public bool deactivateSourceAfterShatter = true; [Header("Prewarm")] [Tooltip("If true, the expensive mesh slicing runs asynchronously in Start(): the fragments are built up front " + "and kept hidden & frozen. Shatter() then becomes a cheap, instant reveal + launch, so there is no " + "slicing hitch at the moment the track breaks. Leave off to slice on-demand when Shatter() is called.")] public bool prewarmFragmentsOnStart = false; [Header("Debug")] [Tooltip("If true, pressing 'Debug Key' triggers the shatter at runtime. For testing only.")] public bool enableDebugKey = false; [Tooltip("Key that triggers the shatter when 'Enable Debug Key' is on.")] public KeyCode debugKey = KeyCode.F; private GameObject fragmentRoot; private FragmentReclaimer fragmentReclaimer; private bool hasShattered; // Prewarm state: when prewarmFragmentsOnStart is on, the fragments are built ahead of time and parked // here (hidden + kinematic). Shatter() then just reveals & launches them instead of slicing on the spot. private bool prewarmComplete; private bool prewarmInProgress; private Vector3 pendingLaunchDirection; private bool launchPending; // Name of the dedicated physics layer every fragment is placed on. The layer-collision matrix is // configured so this layer ignores itself, giving global cross-batch fragment isolation for free. private const string FragmentLayerName = "FractureFragment"; private static int cachedFragmentLayer = -1; private static bool fragmentLayerResolved; private void Awake() { if (freezeSourceUntilShatter) { // The source body is only needed to read mass for fragment computation. Freeze it so the // object doesn't fall or jitter under physics before Shatter() is called. var body = GetComponent(); if (body != null) { body.isKinematic = true; body.useGravity = false; } } } private void Start() { if (prewarmFragmentsOnStart) { StartCoroutine(PrewarmRoutine()); } } private void Update() { if (enableDebugKey && !hasShattered && Input.GetKeyDown(debugKey)) { Shatter(); } } /// /// Fractures the object and launches the fragments along the configured drift direction. /// public void Shatter() { Shatter(ResolveDriftDirection()); } /// /// Fractures the object and launches the fragments along . /// /// Direction in world space the fragments should fly toward. public void Shatter(Vector3 worldDirection) { if (hasShattered) { return; } Vector3 direction = worldDirection.sqrMagnitude > Mathf.Epsilon ? worldDirection.normalized : transform.up; // Prewarm path: the expensive slicing already ran (or is running) in Start(). if (prewarmFragmentsOnStart) { if (prewarmComplete) { // Fragments are pre-built and parked. This is now a cheap, instant reveal + launch. hasShattered = true; RevealAndLaunch(direction); return; } if (prewarmInProgress) { // Slicing hasn't finished yet; remember the request and launch the moment it completes. pendingLaunchDirection = direction; launchPending = true; return; } // Prewarm was requested but Start() hasn't run yet (or failed) — fall through to slice on-demand. } MeshFilter meshFilter = GetComponent(); if (meshFilter == null || meshFilter.sharedMesh == null) { Debug.LogWarning("[FractureAndDrift] No mesh to fracture.", this); return; } hasShattered = true; CreateFragmentRoot(); GameObject fragmentTemplate = CreateFragmentTemplate(); if (sliceMode != SliceMode.Random) { // Custom subdivision loop that controls cut orientation to avoid stretched fragments. FractureControlled(fragmentTemplate); Destroy(fragmentTemplate); LaunchFragments(direction); FinishShatter(); } else if (fractureOptions.asynchronous) { StartCoroutine(Fragmenter.FractureAsync( gameObject, fractureOptions, fragmentTemplate, fragmentRoot.transform, () => { Destroy(fragmentTemplate); LaunchFragments(direction); FinishShatter(); })); } else { Fragmenter.Fracture( gameObject, fractureOptions, fragmentTemplate, fragmentRoot.transform); Destroy(fragmentTemplate); LaunchFragments(direction); FinishShatter(); } } /// /// Builds all fragments ahead of time in Start() and parks them hidden & inactive, so that the /// actual Shatter() is a cheap reveal + launch with no slicing hitch. Runs the slicing across frames /// (the async slicer yields; the controlled/sync slicers run in one frame but still off the break moment). /// private IEnumerator PrewarmRoutine() { MeshFilter meshFilter = GetComponent(); if (meshFilter == null || meshFilter.sharedMesh == null) { yield break; } prewarmInProgress = true; CreateFragmentRoot(); // Park the container inactive BEFORE slicing so every fragment spawns inactive and never simulates // (falls/drifts) during the multi-frame prewarm. Shatter() reveals it later via SetActive(true). fragmentRoot.SetActive(false); GameObject fragmentTemplate = CreateFragmentTemplate(); if (sliceMode != SliceMode.Random) { FractureControlled(fragmentTemplate); Destroy(fragmentTemplate); } else if (fractureOptions.asynchronous) { yield return StartCoroutine(Fragmenter.FractureAsync( gameObject, fractureOptions, fragmentTemplate, fragmentRoot.transform, () => { })); Destroy(fragmentTemplate); } else { Fragmenter.Fracture( gameObject, fractureOptions, fragmentTemplate, fragmentRoot.transform); Destroy(fragmentTemplate); } // Container is already inactive (parked before slicing), so nothing simulated during prewarm. prewarmInProgress = false; prewarmComplete = true; // If Shatter() was called while we were still slicing, honour it now. if (launchPending) { launchPending = false; hasShattered = true; RevealAndLaunch(pendingLaunchDirection); } } /// /// Reveals the pre-built fragment container and launches its fragments. Used by the prewarm path. /// private void RevealAndLaunch(Vector3 direction) { if (fragmentRoot != null) { fragmentRoot.SetActive(true); } LaunchFragments(direction); FinishShatter(); } /// /// Creates the collector object that holds the produced fragments (matches OpenFracture's convention). /// private void CreateFragmentRoot() { fragmentRoot = new GameObject($"{name}Fragments"); fragmentRoot.transform.SetParent(transform.parent); fragmentRoot.transform.position = transform.position; fragmentRoot.transform.rotation = transform.rotation; fragmentRoot.transform.localScale = Vector3.one; } private void FinishShatter() { // Attach the reclaimer to the fragment container (which stays active). It waits either for an // explicit ReclaimFragments() call or, as a fallback, for fragmentLifetime to elapse, then // collects the fragments asynchronously (staggered) rather than destroying them all at once. if (fragmentRoot != null) { fragmentReclaimer = fragmentRoot.AddComponent(); fragmentReclaimer.Configure(fragmentLifetime, reclaimStagger, reclaimShrink); } if (deactivateSourceAfterShatter) { gameObject.SetActive(false); } } /// /// Begins the asynchronous, staggered collection of this track's fragments. Safe to call once the /// object has shattered; a no-op otherwise. This overrides the fragmentLifetime fallback timer. /// public void ReclaimFragments() { if (fragmentReclaimer != null) { fragmentReclaimer.BeginReclaim(); } } /// /// Iterates the freshly created fragments and applies the launch velocity + spin. /// private void LaunchFragments(Vector3 direction) { if (fragmentRoot == null) { return; } Vector3 center = transform.position; var bodies = fragmentRoot.GetComponentsInChildren(); for (int i = 0; i < bodies.Length; i++) { Rigidbody body = bodies[i]; if (body == null) { continue; } // When custom gravity is on, disable Unity's global (down-only) gravity so the // per-step custom acceleration is the only gravity acting on the fragment. body.useGravity = !useCustomGravity && fragmentsUseGravity; // Safety net: guarantee the depenetration clamp is applied to every produced body, even ones // that might not have inherited it from the template (e.g. extra pieces from FindDisconnectedMeshes). if (maxDepenetrationVelocity > 0f) { body.maxDepenetrationVelocity = maxDepenetrationVelocity; } Vector3 launchDir = ApplyScatter(direction); float speed = driftSpeed + Random.value * Mathf.Max(0f, driftSpeedRandomness); Vector3 velocity = launchDir * speed; if (explosionSpeed > 0f) { Vector3 fromCenter = body.worldCenterOfMass - center; if (fromCenter.sqrMagnitude > Mathf.Epsilon) { velocity += fromCenter.normalized * explosionSpeed; } } body.linearVelocity = velocity; if (spinSpeed > 0f) { body.angularVelocity = Random.insideUnitSphere * spinSpeed; } } ConfigureFragmentCollisions(); if (useCustomGravity) { // The source object gets deactivated after shattering, so the custom-gravity driver // must live on the fragment container (which stays active) to keep applying force. Vector3 gravity = customGravityIsLocal ? transform.TransformDirection(customGravity) : customGravity; var driver = fragmentRoot.AddComponent(); driver.gravity = gravity; } } /// /// Configures fragment collisions via the physics layer-collision matrix instead of per-pair /// Physics.IgnoreCollision. All fragments live on ; disabling that /// layer's collision with itself makes EVERY fragment ignore EVERY other fragment - same batch or a /// different track's batch, at any time, with no per-collider bookkeeping. This is why the earlier /// pairwise approach kept leaking cross-batch collisions: newly spawned colliders were never paired /// against batches that shattered later. The matrix rule has no such ordering dependency. /// private void ConfigureFragmentCollisions() { int fragmentLayer = ResolveFragmentLayer(); if (fragmentLayer < 0) { return; } // Fragments never collide with each other (unless explicitly opted in). This is a global matrix // rule, so it covers same-batch AND cross-batch (another track shattering at the same time) // with no per-collider bookkeeping and no ordering dependency. Physics.IgnoreLayerCollision(fragmentLayer, fragmentLayer, !fragmentsCollideWithEachOther); if (ignoreOtherSourceObjects) { // Fragments ignore the still-solid source objects. Done per-collider (NOT via the layer matrix) // because sources sit on shared layers like Default that the ground/platforms also use - a // whole-layer ignore would let fragments fall through the floor. Per-collider keeps it precise. var fragmentColliders = fragmentRoot.GetComponentsInChildren(); var sources = FindObjectsByType(FindObjectsSortMode.None); for (int s = 0; s < sources.Length; s++) { FractureAndDrift source = sources[s]; // Skip sources that already shattered - their solid collider is gone. Keep 'this' one: // its collider is still active this frame and the fragments spawn right on top of it. if (source == null || (source.hasShattered && source != this)) { continue; } var sourceCollider = source.GetComponent(); if (sourceCollider == null) { continue; } for (int f = 0; f < fragmentColliders.Length; f++) { if (fragmentColliders[f] != null) { Physics.IgnoreCollision(fragmentColliders[f], sourceCollider, true); } } } } } /// /// Resolves the dedicated fragment layer index, caching the lookup. Returns -1 if the project has no /// layer named (falls back to no layer assignment). /// private static int ResolveFragmentLayer() { if (!fragmentLayerResolved) { cachedFragmentLayer = LayerMask.NameToLayer(FragmentLayerName); fragmentLayerResolved = true; if (cachedFragmentLayer < 0) { Debug.LogWarning($"[FractureAndDrift] Layer '{FragmentLayerName}' not found. Add it in " + "Project Settings > Tags and Layers so fragments can be isolated from collisions."); } } return cachedFragmentLayer; } /// /// Rotates by a random angle within the scatter cone. /// private Vector3 ApplyScatter(Vector3 direction) { if (scatterAngle <= 0f) { return direction; } // Random rotation within a cone of half-angle 'scatterAngle' around 'direction'. float angle = Random.Range(0f, scatterAngle); float roll = Random.Range(0f, 360f); Quaternion cone = Quaternion.AngleAxis(angle, Vector3.right) * Quaternion.identity; Quaternion spinAround = Quaternion.AngleAxis(roll, Vector3.forward); Quaternion align = Quaternion.FromToRotation(Vector3.forward, direction); return align * spinAround * cone * Vector3.forward; } private Vector3 ResolveDriftDirection() { return directionIsLocal ? transform.TransformDirection(driftDirection) : driftDirection; } /// /// Builds a template GameObject each fragment clones. Mirrors OpenFracture's Fracture.CreateFragmentTemplate. /// private GameObject CreateFragmentTemplate() { GameObject obj = new GameObject("Fragment") { tag = tag }; // Put every fragment on the dedicated fragment layer. The physics layer-collision matrix is // configured (once) so this layer never collides with itself, which cleanly stops ANY fragment // from colliding with ANY other fragment - same batch or a different track's batch, at any time. int fragmentLayer = ResolveFragmentLayer(); if (fragmentLayer >= 0) { obj.layer = fragmentLayer; } obj.AddComponent(); // Normal material in slot 0, cut-face material in slot 1. var meshRenderer = obj.AddComponent(); meshRenderer.sharedMaterials = new Material[2] { GetComponent().sharedMaterial, fractureOptions.insideMaterial }; var thisCollider = GetComponent(); var fragmentCollider = obj.AddComponent(); fragmentCollider.convex = true; if (thisCollider != null) { fragmentCollider.sharedMaterial = thisCollider.sharedMaterial; fragmentCollider.isTrigger = thisCollider.isTrigger; } var thisRigidBody = GetComponent(); var fragmentRigidBody = obj.AddComponent(); fragmentRigidBody.linearDamping = thisRigidBody.linearDamping; fragmentRigidBody.angularDamping = thisRigidBody.angularDamping; fragmentRigidBody.useGravity = fragmentsUseGravity; // Root cause of the "fragments accelerate" blast: overlapping convex hulls are separated by PhysX // with a near-unlimited velocity by default. Clamp it so overlaps resolve gently instead of // launching pieces. Every fragment (both fracture paths) clones this template, so setting it here // covers all of them, independent of any collision-layer/ignore configuration. if (maxDepenetrationVelocity > 0f) { fragmentRigidBody.maxDepenetrationVelocity = maxDepenetrationVelocity; } return obj; } /// /// Subdivision loop that mirrors , but chooses the cut-plane /// normal per instead of a fully random one. This is what lets a given /// axis be subdivided more finely so long/thin fragments don't come out stretched. /// private void FractureControlled(GameObject fragmentTemplate) { Mesh srcMesh = GetComponent().sharedMesh; var sourceMesh = new FragmentData(srcMesh); if (fractureDebugLogs) { Debug.Log($"[FractureAndDrift] '{name}' FractureControlled start. mode={sliceMode} " + $"targetCount={fractureOptions.fragmentCount} sourceVerts={srcMesh.vertexCount} " + $"sourceTris={srcMesh.triangles.Length / 3} subMeshes={srcMesh.subMeshCount} " + $"srcBoundsSize={srcMesh.bounds.size} readable={srcMesh.isReadable}", this); if (srcMesh.subMeshCount > 1) { Debug.LogWarning($"[FractureAndDrift] '{name}' source mesh has {srcMesh.subMeshCount} submeshes. " + "OpenFracture only slices submesh 0 — geometry in other submeshes is dropped. " + "Combine the model into a single submesh/material if pieces look missing or too coarse.", this); } } var fragments = new Queue(); fragments.Enqueue(sourceMesh); // Subdivide the largest-remaining fragment each step until we hit the target count. // Processing the largest one first (rather than stock FIFO) keeps fragment sizes even. // Guard against degenerate slices: if a piece refuses to split (one side comes back empty), // re-enqueuing it unchanged would spin forever AND leave that big block intact. We instead // drop it into a "done" set so it stops being reconsidered, and bail out if nothing splits. var done = new List(); int guardIterations = fractureOptions.fragmentCount * 8 + 16; int producedSplits = 0; int degenerateSlices = 0; while (fragments.Count + done.Count < fractureOptions.fragmentCount && fragments.Count > 0) { if (guardIterations-- <= 0) { if (fractureDebugLogs) { Debug.LogWarning($"[FractureAndDrift] '{name}' subdivision hit iteration guard; " + "stopping early. The mesh likely can't be split further with the current settings.", this); } break; } FragmentData meshData = DequeueLargest(fragments); meshData.CalculateBounds(); Vector3 normal = ChooseCutNormal(meshData.Bounds); MeshSlicer.Slice(meshData, normal, meshData.Bounds.center, fractureOptions.textureScale, fractureOptions.textureOffset, out FragmentData topSlice, out FragmentData bottomSlice); bool topEmpty = topSlice.triangleCount == 0; bool bottomEmpty = bottomSlice.triangleCount == 0; // A real split yields geometry on BOTH sides. If one side is empty the plane didn't actually // divide this piece, so keep it aside as finished instead of looping on it forever. if (topEmpty || bottomEmpty) { degenerateSlices++; done.Add(meshData); continue; } producedSplits++; fragments.Enqueue(topSlice); fragments.Enqueue(bottomSlice); } int i = 0; var parentSize = srcMesh.bounds.size; var parentMass = GetComponent().mass; float density = (parentSize.x * parentSize.y * parentSize.z) / Mathf.Max(parentMass, Mathf.Epsilon); foreach (FragmentData meshData in fragments) { CreateControlledFragment(meshData, fragmentTemplate, density, ref i); } foreach (FragmentData meshData in done) { CreateControlledFragment(meshData, fragmentTemplate, density, ref i); } if (fractureDebugLogs) { Debug.Log($"[FractureAndDrift] '{name}' FractureControlled done. successfulSplits={producedSplits} " + $"degenerateSlices={degenerateSlices} fragmentsCreated={i}. " + (degenerateSlices > producedSplits && producedSplits < 4 ? "Most slices failed to divide the mesh -> that is why you still see big blocks. " : ""), this); } } /// /// Picks the slice-plane normal for the next cut based on the current fragment bounds and slice mode. /// private Vector3 ChooseCutNormal(Bounds bounds) { if (sliceMode == SliceMode.LongestAxis) { Vector3 size = bounds.size; // Normal points along the longest dimension, so the cut plane is perpendicular to it // and splits that long dimension in half. Vector3 normal = Vector3.right; if (size.y >= size.x && size.y >= size.z) normal = Vector3.up; else if (size.z >= size.x && size.z >= size.y) normal = Vector3.forward; if (longestAxisJitter > 0f) { normal += Random.insideUnitSphere * longestAxisJitter; } return normal.sqrMagnitude > Mathf.Epsilon ? normal.normalized : Vector3.up; } // AxisWeighted: bias each component by its weight so heavier axes get more perpendicular cuts. Vector3 w = axisWeights; Vector3 weighted = new Vector3( (fractureOptions.xAxis ? 1f : 0f) * Mathf.Max(0f, w.x) * Random.Range(-1f, 1f), (fractureOptions.yAxis ? 1f : 0f) * Mathf.Max(0f, w.y) * Random.Range(-1f, 1f), (fractureOptions.zAxis ? 1f : 0f) * Mathf.Max(0f, w.z) * Random.Range(-1f, 1f)); return weighted.sqrMagnitude > Mathf.Epsilon ? weighted.normalized : Vector3.up; } /// /// Removes and returns the fragment with the largest bounding-box volume from the queue, /// preserving the order of the remaining items. /// private static FragmentData DequeueLargest(Queue fragments) { int count = fragments.Count; FragmentData largest = null; float largestVolume = float.MinValue; // Rotate the queue once, tracking the largest, then rotate again dropping that one. for (int i = 0; i < count; i++) { FragmentData candidate = fragments.Dequeue(); candidate.CalculateBounds(); Vector3 s = candidate.Bounds.size; float volume = s.x * s.y * s.z; if (volume > largestVolume) { largestVolume = volume; largest = candidate; } fragments.Enqueue(candidate); } for (int i = 0; i < count; i++) { FragmentData candidate = fragments.Dequeue(); if (!ReferenceEquals(candidate, largest)) { fragments.Enqueue(candidate); } } return largest; } /// /// Instantiates a fragment GameObject from mesh data. Mirrors the private Fragmenter.CreateFragment. /// private void CreateControlledFragment(FragmentData meshData, GameObject fragmentTemplate, float density, ref int i) { if (meshData.triangleCount == 0) { return; } Mesh[] meshes; Mesh fragmentMesh = meshData.ToMesh(); if (fractureOptions.detectFloatingFragments) { meshes = MeshUtils.FindDisconnectedMeshes(fragmentMesh); } else { meshes = new Mesh[] { fragmentMesh }; } for (int k = 0; k < meshes.Length; k++) { GameObject fragment = Instantiate(fragmentTemplate, fragmentRoot.transform); fragment.name = $"Fragment{i}"; fragment.transform.localPosition = Vector3.zero; fragment.transform.localRotation = Quaternion.identity; fragment.transform.localScale = transform.localScale; meshes[k].name = System.Guid.NewGuid().ToString(); fragment.GetComponent().sharedMesh = meshes[k]; var collider = fragment.GetComponent(); collider.sharedMesh = meshes[k]; collider.convex = true; var size = meshes[k].bounds.size; var rigidBody = fragment.GetComponent(); rigidBody.mass = (size.x * size.y * size.z) / Mathf.Max(density, Mathf.Epsilon); i++; } } } /// /// Continuously pulls a set of rigidbodies toward a fixed world-space acceleration every physics step. /// Attached to the fragment container by when "Use Custom Gravity" is on, /// so fragments drift toward an arbitrary direction instead of Unity's straight-down global gravity. /// It lives on the surviving fragment root (not the source object, which is deactivated after shattering). /// public class FragmentGravityField : MonoBehaviour { /// World-space acceleration (m/s^2) applied to every fragment each FixedUpdate. public Vector3 gravity; private Rigidbody[] bodies; private void FixedUpdate() { // Refresh the body list lazily: FindDisconnectedMeshes can add fragments a frame late, // and null entries appear as the container is torn down at end of life. if (bodies == null) { bodies = GetComponentsInChildren(); } for (int i = 0; i < bodies.Length; i++) { Rigidbody body = bodies[i]; if (body == null || body.isKinematic) { continue; } // ForceMode.Acceleration ignores mass, so every fragment falls at the same rate (like real gravity). body.AddForce(gravity, ForceMode.Acceleration); } } } /// /// Collects (destroys) a batch of fragments asynchronously instead of all at once. Attached to the /// surviving fragment container by . It either waits for an explicit /// call (driven by the track controller's fade timing) or, as a fallback, /// auto-starts after fallbackDelay seconds. Fragments are destroyed one-by-one spread across /// stagger seconds, optionally shrinking each one to nothing first for a graceful collect. /// public class FragmentReclaimer : MonoBehaviour { private float fallbackDelay; private float stagger; private bool shrink; private bool reclaiming; public void Configure(float fallbackDelay, float stagger, bool shrink) { this.fallbackDelay = fallbackDelay; this.stagger = Mathf.Max(0f, stagger); this.shrink = shrink; // Fallback timer: if nothing calls BeginReclaim() first, start on our own after the delay. if (fallbackDelay > 0f) { Invoke(nameof(BeginReclaim), fallbackDelay); } } /// /// Starts the staggered collection. Safe to call multiple times; only the first call takes effect. /// public void BeginReclaim() { if (reclaiming) { return; } reclaiming = true; CancelInvoke(nameof(BeginReclaim)); StartCoroutine(ReclaimRoutine()); } private IEnumerator ReclaimRoutine() { // Snapshot the current fragments (skip the top-level container transform itself). var fragments = new List(); foreach (Transform child in transform) { if (child != null) { fragments.Add(child); } } int count = fragments.Count; if (count == 0) { Destroy(gameObject); yield break; } // Time budget between consecutive fragment removals. float interval = count > 1 ? stagger / (count - 1) : 0f; for (int i = 0; i < count; i++) { Transform fragment = fragments[i]; if (fragment != null) { if (shrink) { StartCoroutine(ShrinkAndDestroy(fragment.gameObject, Mathf.Max(0.05f, interval))); } else { Destroy(fragment.gameObject); } } if (interval > 0f) { yield return new WaitForSeconds(interval); } } // Once the last fragment is gone (allow shrink time to finish), remove the container. yield return new WaitForSeconds(shrink ? Mathf.Max(0.05f, interval) : 0f); Destroy(gameObject); } private IEnumerator ShrinkAndDestroy(GameObject fragment, float duration) { Transform t = fragment.transform; Vector3 startScale = t.localScale; float elapsed = 0f; while (elapsed < duration && fragment != null) { elapsed += Time.deltaTime; float k = Mathf.Clamp01(elapsed / duration); t.localScale = Vector3.Lerp(startScale, Vector3.zero, k); yield return null; } if (fragment != null) { Destroy(fragment); } } }