450 lines
15 KiB
C#
450 lines
15 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using Cysharp.Threading.Tasks;
|
||
using System.Threading;
|
||
|
||
public class RadarView : MonoBehaviour
|
||
{
|
||
[SerializeField] private RadarOrigin origin;
|
||
[SerializeField] private float radarRadius = 100f;
|
||
[SerializeField] private bool showDebugGizmos = true;
|
||
|
||
[Header("Icon Prefabs")]
|
||
[SerializeField] private GameObject enemyShipPrefab;
|
||
[SerializeField] private GameObject projectilePrefab;
|
||
[SerializeField] private GameObject destroyedShipPrefab;
|
||
|
||
[Header("Scan Settings")]
|
||
[SerializeField] private float scanWidth = 30f;
|
||
|
||
[Header("Fade Settings")]
|
||
[SerializeField] private float iconFadeOutSpeed = 1f;
|
||
[SerializeField] private float detectionCooldown = 2f;
|
||
|
||
[Header("Destroyed Ship Blink Settings")]
|
||
[SerializeField] private int blinkCount = 5;
|
||
[SerializeField] private float blinkDuration = 0.2f;
|
||
|
||
[Header("Scan Animation")]
|
||
[SerializeField] private List<Sprite> radarSlides;
|
||
[SerializeField] private float anglePerSecond = 180;
|
||
[SerializeField] private Image image;
|
||
|
||
private DangersModel dangersModel;
|
||
private RectTransform radarRectTransform;
|
||
private float currentScanAngle = 0;
|
||
|
||
// Храним активные иконки
|
||
private List<RadarIcon> activeIcons = new List<RadarIcon>();
|
||
|
||
// Храним таймеры последнего обнаружения
|
||
private Dictionary<Transform, float> lastDetectionTime = new Dictionary<Transform, float>();
|
||
|
||
// Отслеживаем уничтоженные корабли для мигания
|
||
private Dictionary<EnemyShip, CancellationTokenSource> blinkingShips = new Dictionary<EnemyShip, CancellationTokenSource>();
|
||
|
||
private void Awake()
|
||
{
|
||
EnemyShip.ShipDefeated += OnShipDefeated;
|
||
}
|
||
|
||
public void Initialize(RadarOrigin radarOrigin, DangersModel model)
|
||
{
|
||
origin = radarOrigin;
|
||
dangersModel = model;
|
||
radarRectTransform = GetComponent<RectTransform>();
|
||
|
||
if (radarRectTransform == null)
|
||
{
|
||
Debug.LogWarning("RadarView должен быть на объекте с RectTransform!");
|
||
}
|
||
|
||
if (dangersModel != null)
|
||
{
|
||
dangersModel.OnUpdate += OnDangersModelUpdated;
|
||
}
|
||
}
|
||
|
||
private void OnDangersModelUpdated(DangersModel model)
|
||
{
|
||
// Обновляем списки
|
||
}
|
||
|
||
private void OnShipDefeated(EnemyShip ship)
|
||
{
|
||
if (ship == null) return;
|
||
|
||
// Запускаем мигание для уничтоженного корабля
|
||
BlinkDestroyedShipIcon(ship).Forget();
|
||
}
|
||
|
||
private async UniTaskVoid BlinkDestroyedShipIcon(EnemyShip ship)
|
||
{
|
||
// Создаем иконку для уничтоженного корабля в текущей позиции
|
||
if (ship != null && ship.transform != null)
|
||
{
|
||
// Отменяем предыдущее мигание если оно было
|
||
if (blinkingShips.TryGetValue(ship, out CancellationTokenSource existingCts))
|
||
{
|
||
existingCts.Cancel();
|
||
existingCts.Dispose();
|
||
blinkingShips.Remove(ship);
|
||
}
|
||
|
||
// Создаем иконку для уничтоженного корабля
|
||
RadarIcon icon = CreateIcon(ship.transform, IconType.DestroyedShip);
|
||
if (icon != null)
|
||
{
|
||
// Создаем новый токен отмены
|
||
var blinkCts = new CancellationTokenSource();
|
||
blinkingShips[ship] = blinkCts;
|
||
|
||
try
|
||
{
|
||
// Выполняем мигание
|
||
await PerformBlink(icon, blinkCts.Token);
|
||
|
||
// После мигания начинаем затухание
|
||
if (icon != null && !icon.IsDestroyed)
|
||
{
|
||
icon.StartFadeOut();
|
||
}
|
||
}
|
||
catch (System.OperationCanceledException)
|
||
{
|
||
// Мигание отменено - все равно начинаем затухание
|
||
if (icon != null && !icon.IsDestroyed)
|
||
{
|
||
icon.StartFadeOut();
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
if (blinkingShips.ContainsKey(ship))
|
||
blinkingShips.Remove(ship);
|
||
blinkCts.Dispose();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private async UniTask PerformBlink(RadarIcon icon, CancellationToken cancellationToken)
|
||
{
|
||
for (int i = 0; i < blinkCount; i++)
|
||
{
|
||
if (cancellationToken.IsCancellationRequested) break;
|
||
|
||
// Исчезаем
|
||
icon.SetAlpha(0f);
|
||
await UniTask.Delay((int)(blinkDuration * 1000), cancellationToken: cancellationToken);
|
||
|
||
if (cancellationToken.IsCancellationRequested) break;
|
||
|
||
// Появляемся
|
||
icon.SetAlpha(1f);
|
||
await UniTask.Delay((int)(blinkDuration * 1000), cancellationToken: cancellationToken);
|
||
}
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
if (origin == null || dangersModel == null) return;
|
||
|
||
// Обновляем угол сканирования
|
||
currentScanAngle = (currentScanAngle + anglePerSecond * Time.deltaTime) % 360;
|
||
|
||
// Обновляем спрайт сканирования
|
||
if (radarSlides != null && radarSlides.Count > 0 && image != null)
|
||
{
|
||
int slideIndex = (int)(currentScanAngle / (360f / radarSlides.Count));
|
||
slideIndex = Mathf.Clamp(slideIndex, 0, radarSlides.Count - 1);
|
||
image.sprite = radarSlides[slideIndex];
|
||
}
|
||
|
||
// Проверяем объекты на обнаружение
|
||
CheckForDetections();
|
||
|
||
// Обновляем существующие иконки (затухание)
|
||
UpdateExistingIcons();
|
||
}
|
||
|
||
private void CheckForDetections()
|
||
{
|
||
// Проверяем вражеские корабли
|
||
foreach (var ship in dangersModel.AliveShips)
|
||
{
|
||
if (ship != null && ship.HealthComponent != null && ship.HealthComponent.Alive)
|
||
{
|
||
CheckAndCreateIcon(ship.transform, IconType.EnemyShip);
|
||
}
|
||
}
|
||
|
||
// Проверяем снаряды
|
||
foreach (var projectile in dangersModel.Projectiles)
|
||
{
|
||
if (projectile != null)
|
||
{
|
||
CheckAndCreateIcon(projectile.transform, IconType.Projectile);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void CheckAndCreateIcon(Transform target, IconType iconType)
|
||
{
|
||
if (target == null) return;
|
||
|
||
// Проверяем, находится ли цель в зоне сканирования прямо сейчас
|
||
if (IsInScanZone(target))
|
||
{
|
||
// Проверяем кулдаун для кораблей (для снарядов кулдауна нет)
|
||
bool useCooldown = iconType != IconType.Projectile;
|
||
|
||
if (!useCooldown ||
|
||
!lastDetectionTime.ContainsKey(target) ||
|
||
Time.time - lastDetectionTime[target] >= detectionCooldown)
|
||
{
|
||
// Создаем иконку
|
||
CreateIcon(target, iconType);
|
||
|
||
if (useCooldown)
|
||
{
|
||
lastDetectionTime[target] = Time.time;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private bool IsInScanZone(Transform target)
|
||
{
|
||
if (origin == null || target == null) return false;
|
||
|
||
// Получаем угол до цели
|
||
Vector3 directionToTarget = target.position - origin.transform.position;
|
||
float angleToTarget = Vector3.SignedAngle(origin.transform.forward, directionToTarget, Vector3.up);
|
||
|
||
// Проверяем дистанцию
|
||
float distance = directionToTarget.magnitude;
|
||
if (distance > origin.Radius) return false;
|
||
|
||
// Проверяем угол
|
||
float halfWidth = scanWidth / 2;
|
||
float normalizedScanAngle = NormalizeAngle(currentScanAngle);
|
||
float normalizedTargetAngle = NormalizeAngle(angleToTarget);
|
||
float angleDifference = Mathf.DeltaAngle(normalizedScanAngle, normalizedTargetAngle);
|
||
|
||
return Mathf.Abs(angleDifference) <= halfWidth;
|
||
}
|
||
|
||
private RadarIcon CreateIcon(Transform target, IconType iconType)
|
||
{
|
||
// Выбираем префаб в зависимости от типа иконки
|
||
GameObject selectedPrefab = null;
|
||
|
||
switch (iconType)
|
||
{
|
||
case IconType.EnemyShip:
|
||
selectedPrefab = enemyShipPrefab;
|
||
break;
|
||
case IconType.Projectile:
|
||
selectedPrefab = projectilePrefab;
|
||
break;
|
||
case IconType.DestroyedShip:
|
||
selectedPrefab = destroyedShipPrefab;
|
||
break;
|
||
}
|
||
|
||
if (selectedPrefab == null || target == null) return null;
|
||
|
||
GameObject iconObject = Instantiate(selectedPrefab, transform);
|
||
RadarIcon icon = iconObject.GetComponent<RadarIcon>();
|
||
|
||
if (icon == null)
|
||
{
|
||
icon = iconObject.AddComponent<RadarIcon>();
|
||
}
|
||
|
||
// Получаем позицию для радара (фиксируем в момент обнаружения)
|
||
Vector3 radarData = origin.TransformOnRadar(target);
|
||
Vector2 fixedPosition = new Vector2(radarData.x, radarData.y) * radarRadius;
|
||
float fixedRotation = -radarData.z;
|
||
|
||
// Для уничтоженных кораблей не начинаем затухание сразу
|
||
bool startFadeImmediately = (iconType != IconType.DestroyedShip);
|
||
|
||
icon.Initialize(fixedPosition, fixedRotation, iconFadeOutSpeed, startFadeImmediately);
|
||
activeIcons.Add(icon);
|
||
|
||
return icon;
|
||
}
|
||
|
||
private void UpdateExistingIcons()
|
||
{
|
||
// Обновляем все иконки и удаляем уничтоженные
|
||
for (int i = activeIcons.Count - 1; i >= 0; i--)
|
||
{
|
||
if (activeIcons[i] == null || activeIcons[i].IsDestroyed)
|
||
{
|
||
activeIcons.RemoveAt(i);
|
||
}
|
||
else
|
||
{
|
||
activeIcons[i].UpdateLife();
|
||
}
|
||
}
|
||
}
|
||
|
||
private float NormalizeAngle(float angle)
|
||
{
|
||
angle = angle % 360;
|
||
if (angle > 180) angle -= 360;
|
||
if (angle < -180) angle += 360;
|
||
return angle;
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
EnemyShip.ShipDefeated -= OnShipDefeated;
|
||
|
||
if (dangersModel != null)
|
||
{
|
||
dangersModel.OnUpdate -= OnDangersModelUpdated;
|
||
}
|
||
|
||
// Отменяем все мигания
|
||
foreach (var cts in blinkingShips.Values)
|
||
{
|
||
cts.Cancel();
|
||
cts.Dispose();
|
||
}
|
||
blinkingShips.Clear();
|
||
|
||
// Уничтожаем все иконки
|
||
foreach (var icon in activeIcons)
|
||
{
|
||
if (icon != null)
|
||
Destroy(icon.gameObject);
|
||
}
|
||
activeIcons.Clear();
|
||
lastDetectionTime.Clear();
|
||
}
|
||
|
||
private void OnDrawGizmos()
|
||
{
|
||
if (!showDebugGizmos || origin == null) return;
|
||
|
||
Gizmos.color = Color.white;
|
||
Gizmos.DrawWireSphere(origin.transform.position, origin.Radius);
|
||
|
||
// Визуализируем зону сканирования
|
||
Vector3 center = origin.transform.position;
|
||
Vector3 forward = origin.transform.forward;
|
||
|
||
float halfWidth = scanWidth / 2;
|
||
float minAngle = currentScanAngle - halfWidth;
|
||
float maxAngle = currentScanAngle + halfWidth;
|
||
|
||
Vector3 leftBoundary = Quaternion.Euler(0, minAngle, 0) * forward;
|
||
Vector3 rightBoundary = Quaternion.Euler(0, maxAngle, 0) * forward;
|
||
|
||
Gizmos.color = new Color(1, 1, 0, 0.5f);
|
||
Gizmos.DrawRay(center, leftBoundary * origin.Radius);
|
||
Gizmos.DrawRay(center, rightBoundary * origin.Radius);
|
||
}
|
||
|
||
private enum IconType
|
||
{
|
||
EnemyShip,
|
||
Projectile,
|
||
DestroyedShip
|
||
}
|
||
}
|
||
|
||
// Обновленный компонент для иконки радара
|
||
public class RadarIcon : MonoBehaviour
|
||
{
|
||
private RectTransform rectTransform;
|
||
private CanvasGroup canvasGroup;
|
||
private float fadeOutSpeed = 1f;
|
||
private float currentAlpha = 1f;
|
||
private bool isDestroyed = false;
|
||
private bool isFading = false;
|
||
|
||
public bool IsDestroyed => isDestroyed;
|
||
|
||
public void Initialize(Vector2 position, float rotation, float fadeSpeed, bool startFadeImmediately = true)
|
||
{
|
||
rectTransform = GetComponent<RectTransform>();
|
||
if (rectTransform == null)
|
||
{
|
||
rectTransform = gameObject.AddComponent<RectTransform>();
|
||
}
|
||
|
||
// Используем CanvasGroup для управления прозрачностью всего объекта
|
||
canvasGroup = GetComponent<CanvasGroup>();
|
||
if (canvasGroup == null)
|
||
{
|
||
canvasGroup = gameObject.AddComponent<CanvasGroup>();
|
||
}
|
||
|
||
// Устанавливаем позицию и поворот
|
||
rectTransform.anchoredPosition = position;
|
||
rectTransform.localRotation = Quaternion.Euler(0, 0, rotation);
|
||
|
||
// Настраиваем затухание
|
||
fadeOutSpeed = fadeSpeed;
|
||
currentAlpha = 1f;
|
||
canvasGroup.alpha = 1f;
|
||
|
||
// Начинаем затухание только если нужно
|
||
isFading = startFadeImmediately;
|
||
isDestroyed = false;
|
||
}
|
||
|
||
public void UpdateLife()
|
||
{
|
||
if (isDestroyed) return;
|
||
|
||
// Только если иконка сейчас затухает
|
||
if (isFading)
|
||
{
|
||
// Уменьшаем прозрачность
|
||
currentAlpha -= fadeOutSpeed * Time.deltaTime;
|
||
|
||
if (currentAlpha <= 0)
|
||
{
|
||
DestroyIcon();
|
||
return;
|
||
}
|
||
|
||
// Обновляем прозрачность
|
||
SetAlpha(currentAlpha);
|
||
}
|
||
}
|
||
|
||
public void StartFadeOut()
|
||
{
|
||
if (!isDestroyed && !isFading)
|
||
{
|
||
isFading = true;
|
||
}
|
||
}
|
||
|
||
public void SetAlpha(float alpha)
|
||
{
|
||
if (canvasGroup != null && !isDestroyed)
|
||
{
|
||
currentAlpha = Mathf.Clamp01(alpha);
|
||
canvasGroup.alpha = currentAlpha;
|
||
}
|
||
}
|
||
|
||
public void DestroyIcon()
|
||
{
|
||
if (!isDestroyed)
|
||
{
|
||
isDestroyed = true;
|
||
Destroy(gameObject);
|
||
}
|
||
}
|
||
} |