997 lines
38 KiB
C#
997 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;
|
|
[SerializeField] private bool enableDebugKeyTrigger = true;
|
|
[SerializeField] private bool debugLogs = true;
|
|
[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();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|