Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/TrackCrashController.cs
T
2026-07-30 23:15:58 +08:00

1002 lines
38 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using KD.Destro2D;
using UnityEngine;
public class TrackCrashController : MonoBehaviour
{
public static TrackCrashController Instance { get; private set; }
public enum TrackCrashShape
{
Auto,
Radial,
RectCut
}
[System.Serializable]
public class TrackEntry
{
public int trackIndex;
public GameObject trackObject;
public bool hideOriginalWhenCrashed = true;
public float restoreDelay = 0f;
}
[Header("Track Crash")]
[SerializeField] private List<TrackEntry> tracks = new List<TrackEntry>(5);
[SerializeField] private bool useDestro2D = true;
[SerializeField] private bool autoRestoreOnTrackCrash = false;
[SerializeField] private float defaultRestoreDelay = 0f;
[Tooltip("Editor-only debug hotkeys (1-5) to trigger track crash. Never active in builds.")]
[SerializeField] private bool enableDebugKeyTrigger = false;
[SerializeField] private bool debugLogs = false;
[SerializeField] private bool useAutoFractureSize = true;
[SerializeField] private float fractureCoreRadius = 0.05f;
[SerializeField] private float fractureOuterRadius = 0.45f;
[SerializeField] private float fractureOuterRadiusScale = 1.1f;
[SerializeField] private float fractureNoiseScale = 10f;
[SerializeField] private float fractureThickness = 0.12f;
[SerializeField] private int fractureLines = 8;
[SerializeField] private TrackCrashShape trackCrashShape = TrackCrashShape.Auto;
[SerializeField] private float rectCutOversize = 1.15f;
[SerializeField] private float chunkDetectDelay = 0.08f;
[SerializeField] private int rectCutCount = 3;
[SerializeField] private float rectCutBandScale = 0.05f;
[SerializeField] private float rectCutPositionSpan = 0.65f;
[SerializeField] private int rectCrossCutCount = 1;
[SerializeField] private float rectCrossCutBandScale = 0.22f;
[SerializeField] private float chunkExplodeForce = 2.4f;
[SerializeField] private float chunkExplodeTorque = 30f;
[SerializeField] private Vector2 chunkWindDirection = Vector2.zero;
[SerializeField] private float chunkWindForce = 0f;
[SerializeField] private Vector2 chunkGravityDirection = Vector2.down;
[SerializeField] private bool keepDestro2DBaseTrackVisible = true;
[SerializeField] private bool keepChunksInPlace = true;
[SerializeField] private bool disableChunkGravity = true;
[SerializeField] private bool autoTuneSplitHandler = true;
[SerializeField] private int splitHandlerMaxChunkCount = 12;
[SerializeField] private int splitHandlerMinCount = 3;
[SerializeField] private bool manualChunkMotion = true;
[SerializeField] private float chunkGravityStrength = 6f;
[SerializeField] private float chunkLinearDamping = 4f;
[SerializeField] private float chunkAngularDamping = 5f;
[SerializeField] private float chunkMotionDuration = 0.45f;
[SerializeField] private bool useRuntimeSpriteFallback = true;
[SerializeField] private int runtimeFallbackColumns = 3;
[SerializeField] private int runtimeFallbackRows = 12;
[SerializeField] private int runtimeFallbackMinVisiblePieces = 6;
[SerializeField] private float runtimeFallbackRandomOffset = 0.015f;
private readonly Dictionary<int, TrackEntry> trackMap = new Dictionary<int, TrackEntry>();
private readonly HashSet<int> crashedTracks = new HashSet<int>();
private readonly HashSet<int> processingTracks = new HashSet<int>();
private readonly Dictionary<int, Coroutine> restoreRoutines = new Dictionary<int, Coroutine>();
private readonly Dictionary<int, GameObject> fractureProxies = new Dictionary<int, GameObject>();
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else if (Instance != this)
{
Destroy(gameObject);
return;
}
RebuildCache();
}
private void OnValidate()
{
if (!Application.isPlaying)
return;
RebuildCache();
}
#if UNITY_EDITOR
private void Update()
{
// Editor-only debug hotkeys; compiled out of player builds so a serialized scene
// value can never leave number-key crash triggers or per-frame Input polling active.
if (!enableDebugKeyTrigger)
return;
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 1 pressed.");
TriggerTrackCrash(0);
}
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 2 pressed.");
TriggerTrackCrash(1);
}
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 3 pressed.");
TriggerTrackCrash(2);
}
else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 4 pressed.");
TriggerTrackCrash(3);
}
else if (Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5))
{
if (debugLogs) Debug.Log("[TrackCrash] Debug key 5 pressed.");
TriggerTrackCrash(4);
}
}
#endif
private void RebuildCache()
{
trackMap.Clear();
for (int i = 0; i < tracks.Count; i++)
{
TrackEntry entry = tracks[i];
if (entry == null || entry.trackObject == null)
continue;
trackMap[entry.trackIndex] = entry;
}
}
public static void TrackCrash(int trackIndex)
{
var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<TrackCrashController>();
if (controller == null)
return;
controller.TriggerTrackCrash(trackIndex);
}
public static void RestoreTrack(int trackIndex)
{
var controller = Instance != null ? Instance : SceneObjectLookupCache.FindAny<TrackCrashController>();
if (controller == null)
return;
controller.RestoreTrackInternal(trackIndex);
}
public void TriggerTrackCrash(int trackIndex)
{
if (!trackMap.TryGetValue(trackIndex, out TrackEntry entry) || entry == null || entry.trackObject == null)
{
if (debugLogs) Debug.LogWarning($"[TrackCrash] Missing track entry or track object for index {trackIndex}.");
return;
}
if (crashedTracks.Contains(trackIndex))
{
if (debugLogs) Debug.Log($"[TrackCrash] Track {trackIndex} is already crashed.");
return;
}
if (processingTracks.Contains(trackIndex))
{
if (debugLogs) Debug.Log($"[TrackCrash] Track {trackIndex} is already processing.");
return;
}
StartCoroutine(TriggerTrackCrashRoutine(trackIndex, entry));
}
private IEnumerator TriggerTrackCrashRoutine(int trackIndex, TrackEntry entry)
{
processingTracks.Add(trackIndex);
try
{
DestroyFractureProxy(trackIndex);
bool fractureSucceeded = !useDestro2D;
SplitHandler splitHandler = null;
int chunkCountBefore = -1;
int chunkCountAfter = -1;
float outerRadius = fractureOuterRadius;
bool waitForChunkDetection = false;
bool usedDestro2D = false;
bool usedRuntimeSpriteFallback = false;
Vector2 fractureCenter = entry.trackObject.transform.position;
Destro2DMain resolvedDestro = null;
SpriteRenderer resolvedSpriteRenderer = null;
TrackCrashShape resolvedShape = TrackCrashShape.Radial;
GameObject fractureTarget = entry.trackObject;
if (useDestro2D)
{
fractureTarget = CreateFractureProxy(trackIndex, entry);
var destro = fractureTarget != null ? fractureTarget.GetComponent<KD.Destro2D.Destro2DMain>() : null;
if (destro == null)
{
destro = fractureTarget != null ? fractureTarget.GetComponentInChildren<KD.Destro2D.Destro2DMain>(true) : null;
}
if (destro != null)
{
usedDestro2D = true;
resolvedDestro = destro;
if (debugLogs) Debug.Log($"[TrackCrash] Fracturing track {trackIndex} on {fractureTarget.name}.");
try
{
splitHandler = destro.GetComponent<SplitHandler>();
TuneSplitHandler(splitHandler);
chunkCountBefore = splitHandler != null ? splitHandler.splitChunks.Count : -1;
var spriteRenderer = ResolveDestroSpriteRenderer(destro, fractureTarget);
resolvedSpriteRenderer = spriteRenderer;
resolvedShape = ResolveCrashShape(spriteRenderer);
if (spriteRenderer != null)
{
fractureCenter = spriteRenderer.bounds.center;
if (useAutoFractureSize)
{
float extent = Mathf.Max(spriteRenderer.bounds.extents.x, spriteRenderer.bounds.extents.y);
outerRadius = Mathf.Max(fractureOuterRadius, extent * fractureOuterRadiusScale);
}
}
TriggerDestro2D(destro, spriteRenderer, fractureCenter, outerRadius);
waitForChunkDetection = true;
}
catch (Exception ex)
{
fractureSucceeded = false;
if (debugLogs) Debug.LogError($"[TrackCrash] Fracture failed for track {trackIndex}: {ex}");
}
}
else if (debugLogs)
{
Debug.LogWarning($"[TrackCrash] No Destro2DMain found on track {trackIndex} object {(fractureTarget != null ? fractureTarget.name : "NULL")} or its children.");
}
}
if (waitForChunkDetection)
{
yield return new WaitForSecondsRealtime(Mathf.Max(0.01f, chunkDetectDelay));
chunkCountAfter = splitHandler != null ? splitHandler.splitChunks.Count : -1;
fractureSucceeded = splitHandler == null || chunkCountAfter > chunkCountBefore;
if (!fractureSucceeded && resolvedDestro != null && resolvedSpriteRenderer != null && resolvedShape == TrackCrashShape.RectCut)
{
if (debugLogs)
{
Debug.Log($"[TrackCrash] Track {trackIndex} primary cut did not split. Applying aggressive fallback cut.");
}
ApplyAggressiveRectCut(resolvedDestro, resolvedSpriteRenderer, fractureCenter);
yield return new WaitForSecondsRealtime(Mathf.Max(0.01f, chunkDetectDelay));
chunkCountAfter = splitHandler != null ? splitHandler.splitChunks.Count : -1;
fractureSucceeded = splitHandler == null || chunkCountAfter > chunkCountBefore;
}
if (fractureSucceeded && splitHandler != null)
{
int visibleChunkCount = CountLiveChunks(splitHandler);
if (visibleChunkCount < 2)
{
fractureSucceeded = false;
if (debugLogs)
{
Debug.Log($"[TrackCrash] Track {trackIndex} Destro2D only produced {visibleChunkCount} visible chunk(s); switching to runtime sprite fallback.");
}
}
}
if (fractureSucceeded && splitHandler != null)
{
PrepareChunksForFinalState(splitHandler, fractureCenter);
}
if (debugLogs)
{
Debug.Log($"[TrackCrash] Track {trackIndex} chunk count before={chunkCountBefore}, after={chunkCountAfter}, success={fractureSucceeded}, outerRadius={outerRadius}.");
}
}
if (!fractureSucceeded && useRuntimeSpriteFallback)
{
DestroyFractureProxy(trackIndex);
fractureTarget = CreateRuntimeSpriteFallbackProxy(trackIndex, entry, out int pieceCount);
fractureSucceeded = fractureTarget != null && pieceCount >= runtimeFallbackMinVisiblePieces;
usedRuntimeSpriteFallback = fractureSucceeded;
if (debugLogs)
{
Debug.Log($"[TrackCrash] Track {trackIndex} runtime sprite fallback pieces={pieceCount}, success={fractureSucceeded}.");
}
}
if (!fractureSucceeded)
{
DestroyFractureProxy(trackIndex);
yield break;
}
crashedTracks.Add(trackIndex);
bool shouldHideOriginalObject = entry.hideOriginalWhenCrashed;
if (usedDestro2D)
{
shouldHideOriginalObject = true;
}
if (shouldHideOriginalObject)
{
entry.trackObject.SetActive(false);
}
if (usedDestro2D && !usedRuntimeSpriteFallback && fractureTarget != null)
{
if (keepDestro2DBaseTrackVisible)
{
fractureTarget.SetActive(true);
}
else
{
fractureTarget.SetActive(false);
}
}
if (usedRuntimeSpriteFallback && fractureTarget != null)
{
fractureTarget.SetActive(true);
}
if (autoRestoreOnTrackCrash)
{
float delay = entry.restoreDelay > 0f ? entry.restoreDelay : defaultRestoreDelay;
if (delay > 0f)
{
if (restoreRoutines.TryGetValue(trackIndex, out var routine) && routine != null)
{
StopCoroutine(routine);
}
restoreRoutines[trackIndex] = StartCoroutine(RestoreAfterDelay(trackIndex, delay));
}
}
}
finally
{
processingTracks.Remove(trackIndex);
}
}
private void TriggerDestro2D(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, float outerRadius)
{
if (destro == null)
return;
TrackCrashShape shape = ResolveCrashShape(spriteRenderer);
if (shape == TrackCrashShape.RectCut)
{
ApplyRectCut(destro, spriteRenderer, fractureCenter);
return;
}
destro.DynamicFracture(fractureCenter, fractureCoreRadius, outerRadius, fractureNoiseScale, fractureThickness, fractureLines);
}
private TrackCrashShape ResolveCrashShape(SpriteRenderer spriteRenderer)
{
if (trackCrashShape != TrackCrashShape.Auto)
return trackCrashShape;
if (spriteRenderer == null)
return TrackCrashShape.Radial;
Vector3 size = spriteRenderer.bounds.size;
return size.y > size.x * 2f || size.x > size.y * 2f
? TrackCrashShape.RectCut
: TrackCrashShape.Radial;
}
private void ApplyRectCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
{
ApplyRectCutInternal(destro, spriteRenderer, fractureCenter, Mathf.Max(1, rectCutCount), Mathf.Clamp01(rectCutPositionSpan), Mathf.Max(0.005f, rectCutBandScale));
ApplyCrossRectCutInternal(destro, spriteRenderer, fractureCenter, Mathf.Max(0, rectCrossCutCount), Mathf.Max(0.01f, rectCrossCutBandScale));
}
private void ApplyAggressiveRectCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
{
ApplyRectCutInternal(
destro,
spriteRenderer,
fractureCenter,
Mathf.Max(3, rectCutCount + 2),
Mathf.Max(0.75f, Mathf.Clamp01(rectCutPositionSpan)),
Mathf.Max(0.08f, rectCutBandScale));
ApplyCrossRectCutInternal(
destro,
spriteRenderer,
fractureCenter,
Mathf.Max(1, rectCrossCutCount + 1),
Mathf.Max(0.12f, rectCrossCutBandScale));
}
private void ApplyRectCutInternal(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, int cutCount, float span, float bandScale)
{
if (destro == null || spriteRenderer == null)
return;
Vector3 extents = spriteRenderer.bounds.extents;
bool verticalTrack = extents.y >= extents.x;
for (int i = 0; i < cutCount; i++)
{
float t = cutCount == 1 ? 0.5f : (float)i / (cutCount - 1);
float offsetNormalized = Mathf.Lerp(-span, span, t);
Vector2 cutPosition = fractureCenter;
RectDestruction rect = new RectDestruction();
if (verticalTrack)
{
cutPosition.y += extents.y * offsetNormalized;
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
rect.b = Mathf.Max(fractureThickness, extents.y * bandScale);
}
else
{
cutPosition.x += extents.x * offsetNormalized;
rect.l = Mathf.Max(fractureThickness, extents.x * bandScale);
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
}
rect.Setup(destro.gameObject);
destro.DynamicDestroyWorld(cutPosition, new List<Destruction> { rect });
}
}
private void ApplyLongitudinalCut(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter)
{
if (destro == null || spriteRenderer == null)
return;
Vector3 extents = spriteRenderer.bounds.extents;
bool verticalTrack = extents.y >= extents.x;
RectDestruction rect = new RectDestruction();
if (verticalTrack)
{
rect.l = Mathf.Max(fractureThickness * 0.6f, extents.x * 0.1f);
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
}
else
{
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
rect.b = Mathf.Max(fractureThickness * 0.6f, extents.y * 0.1f);
}
rect.Setup(destro.gameObject);
destro.DynamicDestroyWorld(fractureCenter, new List<Destruction> { rect });
}
private void ApplyCrossRectCutInternal(Destro2DMain destro, SpriteRenderer spriteRenderer, Vector2 fractureCenter, int cutCount, float bandScale)
{
if (destro == null || spriteRenderer == null || cutCount <= 0)
return;
Vector3 extents = spriteRenderer.bounds.extents;
bool verticalTrack = extents.y >= extents.x;
float usableSpan = 0.6f;
for (int i = 0; i < cutCount; i++)
{
float t = cutCount == 1 ? 0.5f : (float)i / (cutCount - 1);
float offsetNormalized = Mathf.Lerp(-usableSpan, usableSpan, t);
Vector2 cutPosition = fractureCenter;
RectDestruction rect = new RectDestruction();
if (verticalTrack)
{
cutPosition.x += extents.x * offsetNormalized;
rect.l = Mathf.Max(fractureThickness, extents.x * bandScale);
rect.b = Mathf.Max(fractureThickness, extents.y * rectCutOversize);
}
else
{
cutPosition.y += extents.y * offsetNormalized;
rect.l = Mathf.Max(fractureThickness, extents.x * rectCutOversize);
rect.b = Mathf.Max(fractureThickness, extents.y * bandScale);
}
rect.Setup(destro.gameObject);
destro.DynamicDestroyWorld(cutPosition, new List<Destruction> { rect });
}
}
private void TuneSplitHandler(SplitHandler splitHandler)
{
if (!autoTuneSplitHandler || splitHandler == null)
return;
splitHandler.MaxChunkCount = Mathf.Max(splitHandler.MaxChunkCount, splitHandlerMaxChunkCount);
splitHandler.minCount = Mathf.Max(1, Mathf.Min(splitHandler.minCount, splitHandlerMinCount));
}
private SpriteRenderer ResolveDestroSpriteRenderer(Destro2DMain destro, GameObject fallbackObject)
{
if (destro != null)
{
SpriteRenderer directRenderer = destro.GetComponent<SpriteRenderer>();
if (directRenderer != null)
return directRenderer;
SpriteRenderer childRenderer = destro.GetComponentInChildren<SpriteRenderer>(true);
if (childRenderer != null)
return childRenderer;
}
if (fallbackObject != null)
{
SpriteRenderer entryRenderer = fallbackObject.GetComponent<SpriteRenderer>();
if (entryRenderer != null)
return entryRenderer;
return fallbackObject.GetComponentInChildren<SpriteRenderer>(true);
}
return null;
}
private void PushChunksAway(SplitHandler splitHandler, Vector2 fractureCenter)
{
if (splitHandler == null || splitHandler.splitChunks == null || splitHandler.splitChunks.Count == 0)
return;
Vector2 normalizedWindDirection = chunkWindDirection.sqrMagnitude > 0.0001f
? chunkWindDirection.normalized
: Vector2.zero;
foreach (GameObject chunk in splitHandler.splitChunks)
{
if (chunk == null)
continue;
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
SplitHandler chunkSplitHandler = chunk.GetComponent<SplitHandler>();
if (rb == null)
continue;
Vector2 worldChunkCenter = chunk.transform.position;
if (chunkSplitHandler != null)
{
worldChunkCenter = chunk.transform.TransformPoint(chunkSplitHandler.chunkCentre);
}
Vector2 direction = worldChunkCenter - fractureCenter;
if (direction.sqrMagnitude < 0.0001f)
{
direction = UnityEngine.Random.insideUnitCircle.normalized;
}
Vector2 forceVector = direction.normalized * chunkExplodeForce;
if (normalizedWindDirection != Vector2.zero && chunkWindForce > 0f)
{
forceVector += normalizedWindDirection * chunkWindForce;
}
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
rb.AddForce(forceVector, ForceMode2D.Impulse);
if (chunkExplodeTorque > 0f)
{
float torqueSign = UnityEngine.Random.value > 0.5f ? 1f : -1f;
rb.AddTorque(chunkExplodeTorque * torqueSign, ForceMode2D.Impulse);
}
}
}
private void PrepareChunksForFinalState(SplitHandler splitHandler, Vector2 fractureCenter)
{
if (splitHandler == null || splitHandler.splitChunks == null || splitHandler.splitChunks.Count == 0)
return;
if (manualChunkMotion)
{
StartCoroutine(DriveChunksManually(splitHandler, fractureCenter));
return;
}
if (!keepChunksInPlace)
{
PushChunksAway(splitHandler, fractureCenter);
return;
}
foreach (GameObject chunk in splitHandler.splitChunks)
{
if (chunk == null)
continue;
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
if (rb == null)
continue;
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
if (disableChunkGravity)
{
rb.gravityScale = 0f;
}
rb.constraints = RigidbodyConstraints2D.FreezePosition | RigidbodyConstraints2D.FreezeRotation;
}
}
private System.Collections.IEnumerator RestoreAfterDelay(int trackIndex, float delay)
{
yield return new WaitForSecondsRealtime(delay);
RestoreTrackInternal(trackIndex);
restoreRoutines.Remove(trackIndex);
processingTracks.Remove(trackIndex);
}
private void RestoreTrackInternal(int trackIndex)
{
if (!trackMap.TryGetValue(trackIndex, out TrackEntry entry) || entry == null || entry.trackObject == null)
return;
DestroyFractureProxy(trackIndex);
if (entry.trackObject != null)
{
entry.trackObject.SetActive(true);
}
crashedTracks.Remove(trackIndex);
}
public void RebuildFromSceneLookup()
{
RebuildCache();
}
private IEnumerator DriveChunksManually(SplitHandler splitHandler, Vector2 fractureCenter)
{
List<Rigidbody2D> rigidbodies = new List<Rigidbody2D>();
List<Vector2> velocities = new List<Vector2>();
List<float> angularVelocities = new List<float>();
Vector2 normalizedWind = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero;
Vector2 normalizedGravity = disableChunkGravity || chunkGravityDirection.sqrMagnitude <= 0.0001f
? Vector2.zero
: chunkGravityDirection.normalized;
foreach (GameObject chunk in splitHandler.splitChunks)
{
if (chunk == null)
continue;
Rigidbody2D rb = chunk.GetComponent<Rigidbody2D>();
if (rb == null)
continue;
SplitHandler chunkSplitHandler = chunk.GetComponent<SplitHandler>();
Vector2 worldChunkCenter = chunk.transform.position;
if (chunkSplitHandler != null)
{
worldChunkCenter = chunk.transform.TransformPoint(chunkSplitHandler.chunkCentre);
}
Vector2 radialDirection = worldChunkCenter - fractureCenter;
if (radialDirection.sqrMagnitude < 0.0001f)
{
radialDirection = UnityEngine.Random.insideUnitCircle.normalized;
}
Vector2 velocity = Vector2.zero;
if (!keepChunksInPlace)
{
velocity += radialDirection.normalized * chunkExplodeForce;
velocity += normalizedWind * chunkWindForce;
}
rigidbodies.Add(rb);
velocities.Add(velocity);
angularVelocities.Add(UnityEngine.Random.Range(-chunkExplodeTorque, chunkExplodeTorque));
rb.gravityScale = 0f;
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
}
float elapsed = 0f;
while (elapsed < chunkMotionDuration)
{
float dt = Time.deltaTime;
for (int i = 0; i < rigidbodies.Count; i++)
{
Rigidbody2D rb = rigidbodies[i];
if (rb == null)
continue;
Vector2 velocity = velocities[i];
float angularVelocity = angularVelocities[i];
velocity += normalizedGravity * chunkGravityStrength * dt;
velocity += normalizedWind * chunkWindForce * dt;
velocity = Vector2.Lerp(velocity, Vector2.zero, Mathf.Clamp01(chunkLinearDamping * dt));
angularVelocity = Mathf.Lerp(angularVelocity, 0f, Mathf.Clamp01(chunkAngularDamping * dt));
rb.position += velocity * dt;
rb.rotation += angularVelocity * dt;
velocities[i] = velocity;
angularVelocities[i] = angularVelocity;
}
elapsed += dt;
yield return null;
}
for (int i = 0; i < rigidbodies.Count; i++)
{
if (rigidbodies[i] == null)
continue;
rigidbodies[i].linearVelocity = Vector2.zero;
rigidbodies[i].angularVelocity = 0f;
if (keepChunksInPlace)
{
rigidbodies[i].constraints = RigidbodyConstraints2D.FreezePosition | RigidbodyConstraints2D.FreezeRotation;
}
}
}
private GameObject CreateFractureProxy(int trackIndex, TrackEntry entry)
{
if (entry == null || entry.trackObject == null)
return null;
GameObject proxy = Instantiate(entry.trackObject, entry.trackObject.transform.parent);
proxy.name = entry.trackObject.name + "_FractureProxy";
proxy.transform.position = entry.trackObject.transform.position;
proxy.transform.rotation = entry.trackObject.transform.rotation;
proxy.transform.localScale = entry.trackObject.transform.localScale;
fractureProxies[trackIndex] = proxy;
return proxy;
}
private void DestroyFractureProxy(int trackIndex)
{
if (!fractureProxies.TryGetValue(trackIndex, out GameObject proxy))
return;
fractureProxies.Remove(trackIndex);
if (proxy != null)
{
Destroy(proxy);
}
}
private int CountLiveChunks(SplitHandler splitHandler)
{
if (splitHandler == null || splitHandler.splitChunks == null)
return 0;
int count = 0;
foreach (GameObject chunk in splitHandler.splitChunks)
{
if (chunk == null || !chunk.activeInHierarchy)
continue;
SpriteRenderer spriteRenderer = chunk.GetComponent<SpriteRenderer>();
if (spriteRenderer == null || spriteRenderer.sprite == null)
continue;
count++;
}
return count;
}
private GameObject CreateRuntimeSpriteFallbackProxy(int trackIndex, TrackEntry entry, out int pieceCount)
{
pieceCount = 0;
if (entry == null || entry.trackObject == null)
return null;
SpriteRenderer sourceRenderer = ResolveDestroSpriteRenderer(null, entry.trackObject);
if (sourceRenderer == null || sourceRenderer.sprite == null || sourceRenderer.sprite.texture == null)
return null;
Sprite sourceSprite = sourceRenderer.sprite;
Rect sourceRect = sourceSprite.rect;
if (sourceRect.width < 2f || sourceRect.height < 2f)
return null;
int columns = Mathf.Max(1, runtimeFallbackColumns);
int rows = Mathf.Max(1, runtimeFallbackRows);
float aspect = sourceRect.height / Mathf.Max(1f, sourceRect.width);
if (aspect > 2f)
{
rows = Mathf.Max(rows, Mathf.CeilToInt(aspect * columns * 1.75f));
}
GameObject proxy = new GameObject(entry.trackObject.name + "_RuntimeFractureProxy");
proxy.transform.SetParent(entry.trackObject.transform.parent, false);
proxy.transform.position = sourceRenderer.transform.position;
proxy.transform.rotation = sourceRenderer.transform.rotation;
proxy.transform.localScale = sourceRenderer.transform.lossyScale;
fractureProxies[trackIndex] = proxy;
TrackCrashRuntimeSpriteCleanup cleanup = proxy.AddComponent<TrackCrashRuntimeSpriteCleanup>();
List<Transform> pieceTransforms = new List<Transform>();
List<Vector2> pieceVelocities = new List<Vector2>();
List<float> pieceAngularVelocities = new List<float>();
float pixelsPerUnit = Mathf.Max(1f, sourceSprite.pixelsPerUnit);
float minLocalX = -sourceSprite.pivot.x / pixelsPerUnit;
float minLocalY = -sourceSprite.pivot.y / pixelsPerUnit;
for (int row = 0; row < rows; row++)
{
float normalizedY0 = (float)row / rows;
float normalizedY1 = (float)(row + 1) / rows;
float yMin = sourceRect.y + (sourceRect.height * normalizedY0);
float yMax = sourceRect.y + (sourceRect.height * normalizedY1);
yMin = Mathf.Clamp(yMin, sourceRect.y, sourceRect.yMax - 1f);
yMax = Mathf.Clamp(yMax, yMin + 1f, sourceRect.yMax);
for (int col = 0; col < columns; col++)
{
float normalizedX0 = (float)col / columns;
float normalizedX1 = (float)(col + 1) / columns;
float xMin = sourceRect.x + (sourceRect.width * normalizedX0);
float xMax = sourceRect.x + (sourceRect.width * normalizedX1);
xMin = Mathf.Clamp(xMin, sourceRect.x, sourceRect.xMax - 1f);
xMax = Mathf.Clamp(xMax, xMin + 1f, sourceRect.xMax);
Rect pieceRect = Rect.MinMaxRect(xMin, yMin, xMax, yMax);
if (pieceRect.width < 1f || pieceRect.height < 1f)
continue;
GameObject piece = new GameObject($"Piece_{row}_{col}");
piece.transform.SetParent(proxy.transform, false);
SpriteRenderer pieceRenderer = piece.AddComponent<SpriteRenderer>();
pieceRenderer.sprite = Sprite.Create(
sourceSprite.texture,
pieceRect,
new Vector2(0.5f, 0.5f),
pixelsPerUnit,
0,
SpriteMeshType.FullRect);
cleanup.Register(pieceRenderer.sprite);
pieceRenderer.sharedMaterial = sourceRenderer.sharedMaterial;
pieceRenderer.color = sourceRenderer.color;
pieceRenderer.flipX = sourceRenderer.flipX;
pieceRenderer.flipY = sourceRenderer.flipY;
pieceRenderer.maskInteraction = sourceRenderer.maskInteraction;
pieceRenderer.sortingLayerID = sourceRenderer.sortingLayerID;
pieceRenderer.sortingOrder = sourceRenderer.sortingOrder;
pieceRenderer.renderingLayerMask = sourceRenderer.renderingLayerMask;
piece.layer = sourceRenderer.gameObject.layer;
float localPieceX = (pieceRect.x - sourceRect.x);
float localPieceY = (pieceRect.y - sourceRect.y);
float localCenterX = minLocalX + (localPieceX + pieceRect.width * 0.5f) / pixelsPerUnit;
float localCenterY = minLocalY + (localPieceY + pieceRect.height * 0.5f) / pixelsPerUnit;
Vector2 randomOffset = runtimeFallbackRandomOffset > 0f
? UnityEngine.Random.insideUnitCircle * runtimeFallbackRandomOffset
: Vector2.zero;
piece.transform.localPosition = new Vector3(localCenterX + randomOffset.x, localCenterY + randomOffset.y, 0f);
piece.transform.localRotation = Quaternion.identity;
piece.transform.localScale = Vector3.one;
pieceTransforms.Add(piece.transform);
Vector2 radialDirection = ((Vector2)piece.transform.position - (Vector2)sourceRenderer.bounds.center);
if (radialDirection.sqrMagnitude < 0.0001f)
{
radialDirection = UnityEngine.Random.insideUnitCircle.normalized;
}
Vector2 initialVelocity = Vector2.zero;
if (!keepChunksInPlace)
{
initialVelocity += radialDirection.normalized * chunkExplodeForce;
if (chunkWindDirection.sqrMagnitude > 0.0001f)
{
initialVelocity += chunkWindDirection.normalized * chunkWindForce;
}
}
pieceVelocities.Add(initialVelocity);
pieceAngularVelocities.Add(keepChunksInPlace ? 0f : UnityEngine.Random.Range(-chunkExplodeTorque, chunkExplodeTorque));
pieceCount++;
}
}
if (pieceCount <= 0)
{
Destroy(proxy);
fractureProxies.Remove(trackIndex);
return null;
}
if (manualChunkMotion && !keepChunksInPlace)
{
StartCoroutine(DriveRuntimeSpritePieces(pieceTransforms, pieceVelocities, pieceAngularVelocities));
}
return proxy;
}
private IEnumerator DriveRuntimeSpritePieces(
List<Transform> pieceTransforms,
List<Vector2> pieceVelocities,
List<float> pieceAngularVelocities)
{
Vector2 normalizedWind = chunkWindDirection.sqrMagnitude > 0.0001f ? chunkWindDirection.normalized : Vector2.zero;
Vector2 normalizedGravity = disableChunkGravity || chunkGravityDirection.sqrMagnitude <= 0.0001f
? Vector2.zero
: chunkGravityDirection.normalized;
float elapsed = 0f;
while (elapsed < chunkMotionDuration)
{
float dt = Time.deltaTime;
for (int i = 0; i < pieceTransforms.Count; i++)
{
Transform pieceTransform = pieceTransforms[i];
if (pieceTransform == null)
continue;
Vector2 velocity = pieceVelocities[i];
float angularVelocity = pieceAngularVelocities[i];
velocity += normalizedGravity * chunkGravityStrength * dt;
velocity += normalizedWind * chunkWindForce * dt;
velocity = Vector2.Lerp(velocity, Vector2.zero, Mathf.Clamp01(chunkLinearDamping * dt));
angularVelocity = Mathf.Lerp(angularVelocity, 0f, Mathf.Clamp01(chunkAngularDamping * dt));
pieceTransform.position += (Vector3)(velocity * dt);
pieceTransform.Rotate(0f, 0f, angularVelocity * dt, Space.Self);
pieceVelocities[i] = velocity;
pieceAngularVelocities[i] = angularVelocity;
}
elapsed += dt;
yield return null;
}
}
}
public sealed class TrackCrashRuntimeSpriteCleanup : MonoBehaviour
{
private readonly List<Sprite> runtimeSprites = new List<Sprite>();
public void Register(Sprite sprite)
{
if (sprite != null)
{
runtimeSprites.Add(sprite);
}
}
private void OnDestroy()
{
for (int i = 0; i < runtimeSprites.Count; i++)
{
if (runtimeSprites[i] != null)
{
Destroy(runtimeSprites[i]);
}
}
runtimeSprites.Clear();
}
}