|
|
|
@@ -0,0 +1,978 @@
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Runtime-fractures the attached mesh object using OpenFracture's <see cref="Fragmenter"/>,
|
|
|
|
|
/// then pushes every produced fragment toward a configurable direction so the pieces
|
|
|
|
|
/// "drift"/fly off. Call <see cref="Shatter()"/> (or <see cref="Shatter(Vector3)"/> to
|
|
|
|
|
/// override the direction) from your own gameplay code.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[RequireComponent(typeof(MeshFilter))]
|
|
|
|
|
[RequireComponent(typeof(MeshRenderer))]
|
|
|
|
|
[RequireComponent(typeof(Rigidbody))]
|
|
|
|
|
public class FractureAndDrift : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// How the cut-plane normal is chosen at each subdivision step.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public enum SliceMode
|
|
|
|
|
{
|
|
|
|
|
/// <summary>Original OpenFracture behaviour: random normal over the enabled axes.</summary>
|
|
|
|
|
Random,
|
|
|
|
|
/// <summary>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.</summary>
|
|
|
|
|
LongestAxis,
|
|
|
|
|
/// <summary>Random normal biased by <see cref="axisWeights"/>. Higher weight on an axis produces more cuts
|
|
|
|
|
/// perpendicular to it, i.e. that dimension is subdivided more finely.</summary>
|
|
|
|
|
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<Rigidbody>();
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fractures the object and launches the fragments along the configured drift direction.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void Shatter()
|
|
|
|
|
{
|
|
|
|
|
Shatter(ResolveDriftDirection());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fractures the object and launches the fragments along <paramref name="worldDirection"/>.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="worldDirection">Direction in world space the fragments should fly toward.</param>
|
|
|
|
|
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<MeshFilter>();
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 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).
|
|
|
|
|
/// </summary>
|
|
|
|
|
private IEnumerator PrewarmRoutine()
|
|
|
|
|
{
|
|
|
|
|
MeshFilter meshFilter = GetComponent<MeshFilter>();
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Reveals the pre-built fragment container and launches its fragments. Used by the prewarm path.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void RevealAndLaunch(Vector3 direction)
|
|
|
|
|
{
|
|
|
|
|
if (fragmentRoot != null)
|
|
|
|
|
{
|
|
|
|
|
fragmentRoot.SetActive(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LaunchFragments(direction);
|
|
|
|
|
FinishShatter();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates the collector object that holds the produced fragments (matches OpenFracture's convention).
|
|
|
|
|
/// </summary>
|
|
|
|
|
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>();
|
|
|
|
|
fragmentReclaimer.Configure(fragmentLifetime, reclaimStagger, reclaimShrink);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (deactivateSourceAfterShatter)
|
|
|
|
|
{
|
|
|
|
|
gameObject.SetActive(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 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.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void ReclaimFragments()
|
|
|
|
|
{
|
|
|
|
|
if (fragmentReclaimer != null)
|
|
|
|
|
{
|
|
|
|
|
fragmentReclaimer.BeginReclaim();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Iterates the freshly created fragments and applies the launch velocity + spin.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void LaunchFragments(Vector3 direction)
|
|
|
|
|
{
|
|
|
|
|
if (fragmentRoot == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector3 center = transform.position;
|
|
|
|
|
var bodies = fragmentRoot.GetComponentsInChildren<Rigidbody>();
|
|
|
|
|
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<FragmentGravityField>();
|
|
|
|
|
driver.gravity = gravity;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Configures fragment collisions via the physics layer-collision matrix instead of per-pair
|
|
|
|
|
/// Physics.IgnoreCollision. All fragments live on <see cref="FragmentLayerName"/>; 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.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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<Collider>();
|
|
|
|
|
var sources = FindObjectsByType<FractureAndDrift>(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<Collider>();
|
|
|
|
|
if (sourceCollider == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int f = 0; f < fragmentColliders.Length; f++)
|
|
|
|
|
{
|
|
|
|
|
if (fragmentColliders[f] != null)
|
|
|
|
|
{
|
|
|
|
|
Physics.IgnoreCollision(fragmentColliders[f], sourceCollider, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Resolves the dedicated fragment layer index, caching the lookup. Returns -1 if the project has no
|
|
|
|
|
/// layer named <see cref="FragmentLayerName"/> (falls back to no layer assignment).
|
|
|
|
|
/// </summary>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Rotates <paramref name="direction"/> by a random angle within the scatter cone.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Builds a template GameObject each fragment clones. Mirrors OpenFracture's Fracture.CreateFragmentTemplate.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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<MeshFilter>();
|
|
|
|
|
|
|
|
|
|
// Normal material in slot 0, cut-face material in slot 1.
|
|
|
|
|
var meshRenderer = obj.AddComponent<MeshRenderer>();
|
|
|
|
|
meshRenderer.sharedMaterials = new Material[2]
|
|
|
|
|
{
|
|
|
|
|
GetComponent<MeshRenderer>().sharedMaterial,
|
|
|
|
|
fractureOptions.insideMaterial
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var thisCollider = GetComponent<Collider>();
|
|
|
|
|
var fragmentCollider = obj.AddComponent<MeshCollider>();
|
|
|
|
|
fragmentCollider.convex = true;
|
|
|
|
|
if (thisCollider != null)
|
|
|
|
|
{
|
|
|
|
|
fragmentCollider.sharedMaterial = thisCollider.sharedMaterial;
|
|
|
|
|
fragmentCollider.isTrigger = thisCollider.isTrigger;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var thisRigidBody = GetComponent<Rigidbody>();
|
|
|
|
|
var fragmentRigidBody = obj.AddComponent<Rigidbody>();
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Subdivision loop that mirrors <see cref="Fragmenter.Fracture"/>, but chooses the cut-plane
|
|
|
|
|
/// normal per <see cref="sliceMode"/> 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.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void FractureControlled(GameObject fragmentTemplate)
|
|
|
|
|
{
|
|
|
|
|
Mesh srcMesh = GetComponent<MeshFilter>().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<FragmentData>();
|
|
|
|
|
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<FragmentData>();
|
|
|
|
|
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<Rigidbody>().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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Picks the slice-plane normal for the next cut based on the current fragment bounds and slice mode.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Removes and returns the fragment with the largest bounding-box volume from the queue,
|
|
|
|
|
/// preserving the order of the remaining items.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private static FragmentData DequeueLargest(Queue<FragmentData> 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Instantiates a fragment GameObject from mesh data. Mirrors the private Fragmenter.CreateFragment.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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<MeshFilter>().sharedMesh = meshes[k];
|
|
|
|
|
|
|
|
|
|
var collider = fragment.GetComponent<MeshCollider>();
|
|
|
|
|
collider.sharedMesh = meshes[k];
|
|
|
|
|
collider.convex = true;
|
|
|
|
|
|
|
|
|
|
var size = meshes[k].bounds.size;
|
|
|
|
|
var rigidBody = fragment.GetComponent<Rigidbody>();
|
|
|
|
|
rigidBody.mass = (size.x * size.y * size.z) / Mathf.Max(density, Mathf.Epsilon);
|
|
|
|
|
|
|
|
|
|
i++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Continuously pulls a set of rigidbodies toward a fixed world-space acceleration every physics step.
|
|
|
|
|
/// Attached to the fragment container by <see cref="FractureAndDrift"/> 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).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class FragmentGravityField : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
/// <summary>World-space acceleration (m/s^2) applied to every fragment each FixedUpdate.</summary>
|
|
|
|
|
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<Rigidbody>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Collects (destroys) a batch of fragments asynchronously instead of all at once. Attached to the
|
|
|
|
|
/// surviving fragment container by <see cref="FractureAndDrift"/>. It either waits for an explicit
|
|
|
|
|
/// <see cref="BeginReclaim"/> call (driven by the track controller's fade timing) or, as a fallback,
|
|
|
|
|
/// auto-starts after <c>fallbackDelay</c> seconds. Fragments are destroyed one-by-one spread across
|
|
|
|
|
/// <c>stagger</c> seconds, optionally shrinking each one to nothing first for a graceful collect.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Starts the staggered collection. Safe to call multiple times; only the first call takes effect.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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<Transform>();
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|