Files
EnglewoodLAB/Assets/Scripts/Factory/Alarm/AlarmIconManager.cs

447 lines
20 KiB
C#

using DG.Tweening;
using System.Collections.Generic;
using UnityEngine;
using UVC.Data;
using UVC.Data.Core;
using UVC.Factory.Cameras;
using UVC.Factory.Component;
namespace UVC.Factory.Alarm
{
/// <summary>
/// 특정 설비(FactoryObject)에 연결된 알람 아이콘 UI를 관리하는 클래스입니다.
/// 알람이 하나일 때는 단일 아이콘, 여러 개일 때는 클러스터(묶음) 아이콘을 표시합니다.
/// 클러스터 아이콘을 클릭하면 개별 알람 아이콘들을 원형으로 펼쳐 보여주는 기능을 담당합니다.
/// </summary>
public class AlarmIconManager : MonoBehaviour
{
[Header("UI Components")]
[Tooltip("알람이 여러 개일 때 표시될 묶음 아이콘 UI입니다.")]
[SerializeField] private AlarmClusterIcon clusterView;
[Tooltip("알람 아이콘들이 펼쳐질 때 부모가 될 UI 영역입니다.")]
[SerializeField] private GameObject expandedView;
[Tooltip("개별 알람을 표시할 아이콘의 프리팹입니다.")]
[SerializeField] private GameObject alarmSingleIconManagerPrefab; // 개별 알람 아이콘
[Header("UI Layout Settings")]
[Tooltip("3D 설비 객체와 UI 아이콘 사이의 수직(Y) 간격입니다.")]
[SerializeField] private float objectYOffset = 10f;
[Tooltip("아이콘을 펼쳤을 때 개별 아이콘 사이의 간격입니다.")]
[SerializeField] private float iconSpacing = 20f;
[Tooltip("아이콘을 펼쳤을 때의 중심점 X축 오프셋입니다.")]
[SerializeField] private float expandOffsetX = 0f;
[Tooltip("아이콘을 펼쳤을 때의 중심점 Y축 오프셋입니다.")]
[SerializeField] private float expandOffsetY = 0f;
// 이 UI가 따라다닐 3D 설비 객체의 Transform입니다.
private Transform targetObject;
// 이 UI가 관리하는 모든 알람 데이터의 리스트입니다.
private List<DataObject> alarms = new List<DataObject>();
// 현재 클러스터가 펼쳐진 상태인지 여부를 나타내는 플래그입니다.
private bool isExpanded = false;
// 알람이 하나일 때 사용되는 단일 알람 아이콘의 인스턴스입니다.
private AlarmSingleIconManager singleAlarmIcon1 = null;
// UI 위치 계산에 필요한 컴포넌트들입니다.
private RectTransform rectTransform;
private Canvas canvas;
// UI 애니메이션을 위한 DoTween 핸들러입니다.
private Tweener uiSpacingTweener;
// 사용자가 클러스터를 클릭하여 줌인했는지 상태를 추적하는 변수입니다.
private bool isZoomIn = false;
/// <summary>
/// MonoBehaviour가 활성화될 때 호출되는 Unity 생명주기 메서드입니다.
/// UI 위치 계산에 필요한 RectTransform과 Canvas 컴포넌트를 찾아 저장합니다.
/// </summary>
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvas = GetComponentInParent<Canvas>();
}
/// <summary>
/// AlarmIconManager를 초기화합니다. AlarmManager에 의해 호출됩니다.
/// </summary>
/// <param name="target">알람이 발생한 3D 설비 객체</param>
/// <param name="initialAlarm">최초로 추가될 알람 데이터</param>
public void Initialize(FactoryObject target, DataObject initialAlarm)
{
this.targetObject = target.transform;
AddAlarm(initialAlarm); // 첫 알람을 리스트에 추가
if (clusterView == null || expandedView == null || alarmSingleIconManagerPrefab == null)
{
Debug.LogError("필수 UI 컴포넌트가 할당되지 않았습니다. AlarmUIController를 확인하세요.");
return;
}
// 클러스터 아이콘 클릭 시 OnPointerClick 메서드가 호출되도록 이벤트 핸들러를 등록합니다.
clusterView.OnClickHandler += OnPointerClick;
// 카메라 높이가 변경될 때마다 OnCameraPositionUpChangedHandler가 호출되도록 등록합니다.
FactoryCameraController.Instance.OnCameraPositionUpChanged += OnCameraPositionUpChangedHandler;
}
/// <summary>
/// 매 프레임의 마지막에 호출되는 Unity 생명주기 메서드입니다.
/// 3D 객체의 위치를 2D 스크린 좌표로 변환하여 UI 아이콘이 객체를 따라다니도록 위치를 업데이트합니다.
/// </summary>
void LateUpdate()
{
if (targetObject == null || Camera.main == null || canvas == null)
{
// 필수 객체가 없으면 모든 UI를 비활성화하고 종료합니다.
if (gameObject.activeSelf) gameObject.SetActive(false);
return;
}
// 카메라의 정면 방향과 카메라에서 타겟을 향하는 방향 벡터를 계산합니다.
Vector3 cameraForward = Camera.main.transform.forward;
Vector3 toTarget = (targetObject.position - Camera.main.transform.position).normalized;
// 두 벡터의 내적(Dot product)을 계산하여 타겟 객체가 카메라 앞에 있는지 확인합니다.
// 내적 값이 0보다 크면 객체가 카메라 시야각 내에 있다는 의미입니다.
if (Vector3.Dot(cameraForward, toTarget) > 0)
{
// 객체가 카메라 앞에 있을 때만 UI를 활성화하고 위치를 업데이트합니다.
if (!gameObject.activeSelf)
{
gameObject.SetActive(true);
UpdateView(); // 비활성화 상태였다면 뷰를 다시 활성화합니다.
}
// 3D 월드 좌표를 2D 스크린 좌표로 변환합니다.
Vector3 screenPoint = Camera.main.WorldToScreenPoint(targetObject.position + Vector3.up);
// 캔버스의 렌더 모드에 따라 좌표 변환 방식을 다르게 처리합니다.
// Canvas Render Mode가 Screen Space - Camera일 경우
if (canvas.renderMode == RenderMode.ScreenSpaceCamera)
{
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(canvas.transform as RectTransform, screenPoint, canvas.worldCamera, out localPoint);
localPoint.y += objectYOffset; // Y축 오프셋 적용
rectTransform.localPosition = localPoint;
}
// Canvas Render Mode가 Screen Space - Overlay일 경우
else
{
screenPoint.y += objectYOffset; // Y축 오프셋 적용
rectTransform.position = screenPoint;
}
}
else
{
// 타겟이 카메라 뒤에 있으면 UI를 비활성화합니다.
if (gameObject.activeSelf) gameObject.SetActive(false);
//if (clusterView.gameObject.activeSelf) clusterView.gameObject.SetActive(false);
//if (expandedView.activeSelf) expandedView.SetActive(false);
//if (singleAlarmIcon1 != null && singleAlarmIcon1.gameObject.activeSelf) singleAlarmIcon1.gameObject.SetActive(false);
}
}
/// <summary>
/// 이 매니저가 특정 알람을 이미 관리하고 있는지 확인합니다.
/// </summary>
/// <param name="alarm">확인할 알람 데이터</param>
/// <returns>포함하고 있으면 true, 아니면 false</returns>
public bool ContainsAlarm(DataObject alarm)
{
return alarms.Exists(a => a.Id == alarm.Id);
}
/// <summary>
/// 새로운 알람을 추가합니다.
/// </summary>
/// <param name="alarm">추가할 알람 데이터</param>
public void AddAlarm(DataObject alarm)
{
alarms.Add(alarm);
UpdateView(); // 알람 리스트가 변경되었으므로 뷰를 업데이트합니다.
}
/// <summary>
/// 기존 알람의 정보를 업데이트합니다.
/// </summary>
/// <param name="alarm">업데이트할 알람 데이터</param>
public void UpdateAlarm(DataObject alarm)
{
// 실제로는 alarmId로 찾아서 업데이트해야 함
int index = alarms.FindIndex(a => a.Id == alarm.Id);
if (index >= 0)
{
foreach (var key in alarm.Keys)
{
alarms[index][key] = alarm[key]; // 기존 알람 데이터를 새 데이터로 교체
}
UpdateView(); // 뷰 업데이트
}
else
{
Debug.LogWarning($"Alarm with ID {alarm.Id} not found for update.");
}
}
/// <summary>
/// 특정 알람을 제거합니다.
/// </summary>
/// <param name="alarm">제거할 알람 데이터</param>
public void RemoveAlarm(DataObject alarm)
{
// 실제로는 alarmId로 찾아서 지워야 함
alarms.Remove(alarm);
UpdateView(); // 뷰 업데이트
}
/// <summary>
/// 현재 알람 상태에 맞게 UI를 갱신합니다.
/// 알람 개수와 확장 상태(IsExpanded)에 따라 어떤 UI를 보여줄지 결정합니다.
/// </summary>
private void UpdateView()
{
if (isExpanded)
{
// 확장된 상태라면, 개별 아이콘들을 다시 그립니다. (알람 추가/제거 시 대응)
ExpandCluster();
}
else
{
if (alarms.Count == 1)
{
// 알람이 1개이면 단일 아이콘 뷰를 활성화합니다.
clusterView.gameObject.SetActive(false);
expandedView.SetActive(false);
if (singleAlarmIcon1 == null)
{
// 단일 아이콘이 없으면 새로 생성합니다.
singleAlarmIcon1 = Instantiate(alarmSingleIconManagerPrefab, transform).GetComponent<AlarmSingleIconManager>();
}
singleAlarmIcon1.gameObject.SetActive(true);
singleAlarmIcon1.SetData(alarms[0], targetObject);
}
else if (alarms.Count > 1) // 축소된(기본) 상태
{
// 알람이 2개 이상이면 클러스터 뷰를 활성화합니다.
clusterView.gameObject.SetActive(true);
clusterView.AlarmCount = alarms.Count.ToString(); // 알람 개수 텍스트 업데이트
expandedView.SetActive(false);
if (singleAlarmIcon1 != null) singleAlarmIcon1.gameObject.SetActive(false); // 단일 뷰도 꺼야 함
}
else // alarms.Count == 0
{
// 알람이 없으면 모든 UI를 비활성화하고 이 게임 오브젝트를 파괴합니다.
// (AlarmManager에서 처리하므로 여기서는 비활성화만)
clusterView.gameObject.SetActive(false);
expandedView.SetActive(false);
if (singleAlarmIcon1 != null) singleAlarmIcon1.gameObject.SetActive(false);
}
}
}
/// <summary>
/// 클러스터 아이콘이 클릭되었을 때 호출되는 이벤트 핸들러입니다.
/// </summary>
public void OnPointerClick()
{
isZoomIn = true; // 사용자가 줌인 동작을 시작했음을 기록
if (alarms.Count > 1)
{
if (singleAlarmIcon1 != null && singleAlarmIcon1.IsDetailViewActive)
{
singleAlarmIcon1.HideDetail(); // 단일 아이콘 숨김
}
// 카메라를 타겟 객체에 포커스하고, 클러스터를 펼칩니다.
FactoryCameraController.Instance.FocusOnTarget(targetObject.position, 15.0f); // 예시: 5미터 거리로 줌
ExpandCluster();
}
}
/// <summary>
/// 클러스터 뷰를 펼쳐서 개별 알람 아이콘들을 원형으로 배열합니다.
/// </summary>
private void ExpandCluster()
{
isExpanded = true;
clusterView.gameObject.SetActive(false); // 클러스터 뷰는 숨김
expandedView.SetActive(true); // 확장 뷰는 보임
if (singleAlarmIcon1 != null) singleAlarmIcon1.gameObject.SetActive(false);
// 재활용을 위해 기존에 생성된 아이콘들을 딕셔너리에 저장합니다. (Key: 알람 ID, Value: 아이콘 인스턴스)
var existingIcons = new Dictionary<string, AlarmSingleIconManager>();
foreach (Transform child in expandedView.transform)
{
AlarmSingleIconManager icon = child.GetComponent<AlarmSingleIconManager>();
if (icon != null && icon.Data != null)
{
if (!existingIcons.ContainsKey(icon.Data.Id))
{
existingIcons.Add(icon.Data.Id, icon);
}
}
}
var activeIcons = new List<AlarmSingleIconManager>();
// 현재 알람 목록을 순회하며 아이콘을 업데이트하거나 새로 생성
foreach (var alarmData in alarms)
{
AlarmSingleIconManager icon;
if (existingIcons.TryGetValue(alarmData.Id, out icon))
{
// 기존 아이콘 재사용
icon.SetData(alarmData, targetObject);
existingIcons.Remove(alarmData.Id); // 처리된 아이콘은 딕셔너리에서 제거
}
else
{
// 새 아이콘 생성
GameObject iconObj = Instantiate(alarmSingleIconManagerPrefab, expandedView.transform);
icon = iconObj.GetComponent<AlarmSingleIconManager>();
icon.SetData(alarmData, targetObject);
icon.OnDetail = OnDetailSingle;
}
activeIcons.Add(icon);
}
// 더 이상 사용되지 않는 기존 아이콘들 삭제
foreach (var icon in existingIcons.Values)
{
icon.OnDetail = null;
Destroy(icon.gameObject);
}
// alarmSingleIconPrefab의 RectTransform을 가져와 아이콘의 너비를 계산합니다.
RectTransform iconRect = alarmSingleIconManagerPrefab.GetComponent<RectTransform>();
if (iconRect == null)
{
Debug.LogError("alarmSingleIconPrefab에 RectTransform 컴포넌트가 없습니다.");
return;
}
// 아이콘들을 원형으로 배치하는 로직
float iconWidth = iconRect.rect.width;
// 아이콘의 개수와 너비, 간격을 고려하여 필요한 원주를 계산합니다.
float circumference = (iconWidth + iconSpacing) * activeIcons.Count;
// 원주를 이용하여 반지름(radius)을 계산합니다.
float radius = circumference / (2f * Mathf.PI);
// 중심 오프셋을 적용할 Vector3를 생성합니다.
Vector3 centerOffset = new Vector3(expandOffsetX, expandOffsetY, 0);
for (int i = 0; i < activeIcons.Count; i++)
{
float angle = i * Mathf.PI * 2f / activeIcons.Count;
// 오프셋을 적용하여 아이콘의 위치를 계산합니다.
Vector3 pos = centerOffset + new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * radius;
activeIcons[i].transform.localPosition = pos;
}
// 상세 보기가 활성화된 아이콘을 찾아 맨 위로 올립니다.
foreach (var icon in activeIcons)
{
if (icon.IsDetailViewActive)
{
icon.transform.SetAsLastSibling();
break;
}
}
//AnimateUISpace(0f); // 간격을 0으로 애니메이션
}
/// <summary>
/// 개별 알람 아이콘 중 하나가 상세 보기를 활성화했을 때 호출됩니다.
/// </summary>
/// <param name="alarmSingle">상세 보기를 활성화한 아이콘</param>
private void OnDetailSingle(AlarmSingleIconManager alarmSingle)
{
// 다른 모든 아이콘들의 상세 보기는 숨깁니다.
foreach (Transform child in expandedView.transform)
{
AlarmSingleIconManager existingIcon = child.GetComponent<AlarmSingleIconManager>();
if (existingIcon != null)
{
if (existingIcon != alarmSingle) existingIcon.HideDetail(); // 다른 아이콘은 숨김
}
}
}
/// <summary>
/// 펼쳐진 클러스터를 다시 원래의 묶음 아이콘 상태로 되돌립니다.
/// </summary>
private void CollapseCluster()
{
isExpanded = false;
// 모든 개별 아이콘의 상세 보기를 숨깁니다.
foreach (Transform child in expandedView.transform)
{
AlarmSingleIconManager existingIcon = child.GetComponent<AlarmSingleIconManager>();
if (existingIcon != null && existingIcon.IsDetailViewActive) existingIcon.HideDetail();
}
if (singleAlarmIcon1 != null && singleAlarmIcon1.IsDetailViewActive)
{
singleAlarmIcon1.HideDetail(); // 단일 아이콘도 숨김
}
UpdateView(); // 뷰를 다시 축소된 상태로 업데이트합니다.
}
/// <summary>
/// 현재 관리 중인 알람의 개수를 반환합니다.
/// </summary>
public int GetAlarmCount() => alarms.Count;
private void AnimateUISpace(float targetSpacing, float duration = 1.0f)
{
if (uiSpacingTweener != null && uiSpacingTweener.IsActive() && uiSpacingTweener.IsPlaying())
{
uiSpacingTweener.Kill();
}
uiSpacingTweener = DOVirtual.Float(objectYOffset, targetSpacing, duration, (value) =>
{
objectYOffset = value;
});
}
/// <summary>
/// 카메라 위치가 변경될 때 호출되는 이벤트 핸들러입니다.
/// 사용자가 줌 아웃(카메라가 멀어짐)하면 펼쳐진 클러스터를 자동으로 닫습니다.
/// </summary>
private void OnCameraPositionUpChangedHandler(float gap)
{
if (isZoomIn)
{
// 줌 인 상태에서 카메라 위치가 변경되면 클러스터를 해제
CollapseCluster();
isZoomIn = false; // 줌 인 상태를 해제합니다.
}
}
private void OnDestroy()
{
// DoTween 애니메이션이 실행 중이면 중지시킵니다.
if (uiSpacingTweener != null && uiSpacingTweener.IsActive() && uiSpacingTweener.IsPlaying())
{
uiSpacingTweener.Kill();
}
FactoryCameraController.Instance.OnCameraPositionUpChanged -= OnCameraPositionUpChangedHandler;
clusterView.OnClickHandler -= OnPointerClick;
targetObject = null;
alarms.Clear();
alarms = null;
canvas = null;
if (singleAlarmIcon1 != null)
{
Destroy(singleAlarmIcon1.gameObject);
singleAlarmIcon1 = null;
}
}
}
}