using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanroJump.Gameplay
{
///
/// Поведение ломкой платформы: после приземления выключает коллайдеры и визуально разбивает платформу.
///
public sealed class FragilePlatformBehaviour : MonoBehaviour, IPlatformLandingHandler, IPlatformLifecycle
{
[SerializeField] [Min(0f)] private float breakDelay = 0.08f;
[SerializeField] [Min(0f)] private float breakAnimationDuration = 0.22f;
[SerializeField] [Min(0f)] private float breakHorizontalOffset = 0.45f;
[SerializeField] [Min(0f)] private float breakFallDistance = 0.45f;
[SerializeField] [Min(0f)] private float breakRotation = 18f;
[SerializeField] private bool suppressAutoJump;
[SerializeField] private Behaviour[] disabledOnBreak;
[SerializeField] private Collider2D[] colliders;
[SerializeField] private Transform[] breakParts;
[SerializeField] private SpriteRenderer[] hiddenOnBreak;
private CancellationTokenSource breakCancellation;
private readonly List rendererBuffer = new();
private readonly List colliderBuffer = new();
private readonly List stalePoseKeys = new();
private readonly Dictionary initialBreakPoses = new();
///
/// Кэширует цели выключения при загрузке платформы.
///
private void Awake()
{
CacheTargetsIfNeeded();
}
///
/// Запускает разрушение платформы после посадки игрока.
///
/// True, если нужно подавить стандартный автопрыжок игрока.
public bool TryHandleLanding(JumpPlayerController player, PlatformLandingContext context)
{
if (breakCancellation == null)
{
breakCancellation = new CancellationTokenSource();
BreakAfterDelayAsync(breakCancellation).Forget(Debug.LogException);
}
return suppressAutoJump;
}
///
/// Восстанавливает платформу при первом создании.
///
public void Initialize(PlatformSpawnContext context)
{
Restore();
}
///
/// Восстанавливает платформу при рецикле.
///
public void Recycle(PlatformSpawnContext context)
{
Restore();
}
///
/// Восстанавливает платформу при ручном сбросе.
///
public void ResetPlatform()
{
Restore();
}
///
/// Ждет задержку и выключает выбранные части платформы.
///
private async UniTask BreakAfterDelayAsync(CancellationTokenSource cancellationSource)
{
try
{
var token = cancellationSource.Token;
if (breakDelay > 0f)
{
await UniTask.Delay(
System.TimeSpan.FromSeconds(breakDelay),
DelayType.DeltaTime,
PlayerLoopTiming.Update,
token);
}
RefreshBreakVisualTargets();
SetTargetsEnabled(false);
await AnimateBreakVisualsAsync(token);
}
catch (System.OperationCanceledException)
{
}
finally
{
if (breakCancellation == cancellationSource)
{
breakCancellation = null;
cancellationSource.Dispose();
}
}
}
///
/// Останавливает разрушение и возвращает цели в активное состояние.
///
private void OnDestroy()
{
CancelBreakTask();
}
private void Restore()
{
CancelBreakTask();
CacheTargetsIfNeeded();
RefreshBreakVisualTargets();
RestoreBreakVisuals();
SetTargetsEnabled(true);
}
///
/// Автоматически собирает коллайдеры и визуальные части, если они не назначены в инспекторе.
///
private void CacheTargetsIfNeeded()
{
if (colliders == null || colliders.Length == 0)
{
colliderBuffer.Clear();
GetComponentsInChildren(true, colliderBuffer);
colliders = colliderBuffer.FindAll(IsPlatformBreakCollider).ToArray();
}
RefreshBreakVisualTargets();
}
///
/// Обновляет список текущих визуальных объектов, включая runtime-контент на платформе.
///
private void RefreshBreakVisualTargets()
{
rendererBuffer.Clear();
GetComponentsInChildren(true, rendererBuffer);
hiddenOnBreak = rendererBuffer.FindAll(IsPlatformBreakRenderer).ToArray();
breakParts = BuildBreakPartsFromRenderers(hiddenOnBreak);
RemoveStaleInitialPoses();
for (var index = 0; index < breakParts.Length; index++)
{
var part = breakParts[index];
if (part == null || initialBreakPoses.ContainsKey(part))
{
continue;
}
initialBreakPoses.Add(part, LocalPose.From(part));
}
}
///
/// Включает или выключает коллайдеры и дополнительные Behaviour-компоненты платформы.
///
private void SetTargetsEnabled(bool enabled)
{
if (colliders != null)
{
foreach (var targetCollider in colliders)
{
if (targetCollider != null)
{
targetCollider.enabled = enabled;
}
}
}
if (disabledOnBreak == null)
{
return;
}
foreach (var behaviour in disabledOnBreak)
{
if (behaviour != null)
{
behaviour.enabled = enabled;
}
}
}
///
/// Разводит части платформы в стороны; если частей нет, просто скрывает renderers.
///
private async UniTask AnimateBreakVisualsAsync(CancellationToken cancellationToken)
{
if (breakParts == null || breakParts.Length == 0 || breakAnimationDuration <= 0f)
{
cancellationToken.ThrowIfCancellationRequested();
SetRenderersEnabled(false);
return;
}
var elapsed = 0f;
while (elapsed < breakAnimationDuration)
{
elapsed += Time.deltaTime;
ApplyBreakVisualProgress(Mathf.Clamp01(elapsed / breakAnimationDuration));
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
ApplyBreakVisualProgress(1f);
SetRenderersEnabled(false);
}
private void CancelBreakTask()
{
if (breakCancellation == null)
{
return;
}
var cancellation = breakCancellation;
breakCancellation = null;
cancellation.Cancel();
cancellation.Dispose();
}
///
/// Возвращает визуальные части платформы в исходное состояние.
///
private void RestoreBreakVisuals()
{
if (breakParts != null)
{
for (var index = 0; index < breakParts.Length; index++)
{
var part = breakParts[index];
if (part == null)
{
continue;
}
if (!initialBreakPoses.TryGetValue(part, out var initialPose))
{
initialPose = LocalPose.From(part);
initialBreakPoses.Add(part, initialPose);
}
initialPose.ApplyTo(part);
}
}
SetRenderersEnabled(true);
}
///
/// Применяет текущий прогресс разрушения к каждой визуальной части.
///
private void ApplyBreakVisualProgress(float progress)
{
if (breakParts == null)
{
return;
}
for (var index = 0; index < breakParts.Length; index++)
{
var part = breakParts[index];
if (part == null)
{
continue;
}
if (!initialBreakPoses.TryGetValue(part, out var initialPose))
{
initialPose = LocalPose.From(part);
initialBreakPoses.Add(part, initialPose);
}
var side = GetBreakSide(index, breakParts.Length);
var offset = new Vector3(side * breakHorizontalOffset, -breakFallDistance, 0f) * progress;
part.localPosition = initialPose.LocalPosition + offset;
part.localRotation = initialPose.LocalRotation * Quaternion.Euler(0f, 0f, -side * breakRotation * progress);
}
}
///
/// Убирает из кэша позы уничтоженных runtime-объектов.
///
private void RemoveStaleInitialPoses()
{
stalePoseKeys.Clear();
foreach (var pair in initialBreakPoses)
{
if (pair.Key == null)
{
stalePoseKeys.Add(pair.Key);
}
}
foreach (var staleKey in stalePoseKeys)
{
initialBreakPoses.Remove(staleKey);
}
stalePoseKeys.Clear();
}
///
/// Включает или выключает визуальные renderers платформы.
///
private void SetRenderersEnabled(bool enabled)
{
if (hiddenOnBreak == null)
{
return;
}
foreach (var spriteRenderer in hiddenOnBreak)
{
if (spriteRenderer != null)
{
spriteRenderer.enabled = enabled;
}
}
}
///
/// Строит список уникальных Transform по SpriteRenderer дочерних частей платформы.
///
private static Transform[] BuildBreakPartsFromRenderers(SpriteRenderer[] renderers)
{
if (renderers == null || renderers.Length == 0)
{
return System.Array.Empty();
}
var parts = new System.Collections.Generic.List(renderers.Length);
foreach (var spriteRenderer in renderers)
{
if (spriteRenderer == null || spriteRenderer.transform == null || parts.Contains(spriteRenderer.transform))
{
continue;
}
parts.Add(spriteRenderer.transform);
}
parts.Sort((left, right) => left.localPosition.x.CompareTo(right.localPosition.x));
return parts.ToArray();
}
private static bool IsPlatformBreakRenderer(SpriteRenderer spriteRenderer)
{
return spriteRenderer != null && spriteRenderer.GetComponentInParent(true) == null;
}
private static bool IsPlatformBreakCollider(Collider2D targetCollider)
{
return targetCollider != null && targetCollider.GetComponentInParent(true) == null;
}
///
/// Возвращает направление разлета части: левая часть идет влево, правая - вправо.
///
private static float GetBreakSide(int index, int count)
{
if (count <= 1)
{
return 0f;
}
return index < count * 0.5f ? -1f : 1f;
}
private readonly struct LocalPose
{
public LocalPose(Vector3 localPosition, Quaternion localRotation)
{
LocalPosition = localPosition;
LocalRotation = localRotation;
}
public Vector3 LocalPosition { get; }
public Quaternion LocalRotation { get; }
public static LocalPose From(Transform transform)
{
return new LocalPose(transform.localPosition, transform.localRotation);
}
public void ApplyTo(Transform transform)
{
transform.localPosition = LocalPosition;
transform.localRotation = LocalRotation;
}
}
}
}