Files
XRLib/Assets/Scripts/Factory/Component/FactoryObjectSelectionManager.cs
2025-12-08 21:06:05 +09:00

267 lines
11 KiB
C#

#nullable enable
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
using UVC.Core;
using UVC.Factory.Modal;
namespace UVC.Factory.Component
{
/// <summary>
/// 씬에 배치된 FactoryObject의 선택을 관리하는 싱글톤 클래스입니다.
/// 단일 선택, 다중 선택(추후 확장), 선택 해제 로직을 처리합니다.
/// </summary>
/// <remarks>
/// 이 관리자는 다음 기능을 제공합니다:
/// - FactoryObject 클릭 시 선택 및 외곽선 표시.
/// - 다른 객체 선택 시 이전에 선택된 객체의 외곽선 숨김.
/// - UI가 아닌 빈 공간 클릭 시 모든 선택 해제.
/// - 다중 선택을 위한 기반 제공.
///
/// 이 클래스가 올바르게 작동하려면 씬에 Unity의 EventSystem이 존재해야 합니다.
/// </remarks>
/// <example>
/// <code>
/// // FactoryObjectSelectionManager는 자동으로 씬에 생성되므로 별도의 인스턴스화가 필요 없습니다.
///
/// // FactoryObject에서 선택을 요청하는 방법:
/// public class MyFactoryObject : FactoryObject
/// {
/// public override void OnPointerClick(PointerEventData eventData)
/// {
/// // base.OnPointerClick(eventData); // InfoWindow를 표시하려면 기본 로직 호출
///
/// // 다중 선택을 원하면 (예: Shift 키 누름) isMultiSelect를 true로 설정
/// bool isMultiSelect = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
///
/// // FactoryObjectSelectionManager에 선택 요청
/// FactoryObjectSelectionManager.Instance.Select(this, isMultiSelect);
/// }
/// }
///
/// // 외부에서 선택된 객체를 가져오는 방법:
/// public class UIManager : MonoBehaviour
/// {
/// void Update()
/// {
/// if (Input.GetKeyDown(KeyCode.I))
/// {
/// var selectedObjects = FactoryObjectSelectionManager.Instance.GetSelectedObjects();
/// if (selectedObjects.Any())
/// {
/// Debug.Log($"현재 {selectedObjects.Count}개의 객체가 선택되었습니다.");
/// foreach (var obj in selectedObjects)
/// {
/// Debug.Log($"- {obj.Info?.Name}");
/// }
/// }
/// else
/// {
/// Debug.Log("선택된 객체가 없습니다.");
/// }
/// }
/// }
/// }
/// </code>
/// </example>
public class FactoryObjectSelectionManager : MonoBehaviour
{
private static FactoryObjectSelectionManager? _instance;
private static readonly object _lock = new object();
private static bool _applicationIsQuitting = false;
/// <summary>
/// SelectionManager의 싱글톤 인스턴스를 가져옵니다.
/// 씬에 인스턴스가 없으면 자동으로 생성합니다.
/// </summary>
public static FactoryObjectSelectionManager Instance
{
get
{
if (_applicationIsQuitting)
{
// 애플리케이션 종료 시점에 이미 파괴된 싱글톤에 접근하는 것을 방지합니다.
Debug.LogWarning("[Singleton] Instance 'FactoryObjectSelectionManager' already destroyed on application quit. Won't create again - returning null.");
return null!;
}
lock (_lock)
{
if (_instance == null)
{
// 씬에서 기존 인스턴스를 찾아봅니다.
_instance = FindFirstObjectByType<FactoryObjectSelectionManager>();
if (_instance == null)
{
// 씬에 인스턴스가 없으면 새로 생성합니다.
var singletonObject = new GameObject();
_instance = singletonObject.AddComponent<FactoryObjectSelectionManager>();
singletonObject.name = typeof(FactoryObjectSelectionManager).ToString() + " (Singleton)";
// 씬 전환 시 파괴되지 않도록 설정합니다.
DontDestroyOnLoad(singletonObject);
}
}
return _instance;
}
}
}
// 현재 선택된 객체들을 저장하는 리스트입니다.
private readonly List<FactoryObject> _selectedObjects = new List<FactoryObject>();
private void Awake()
{
// 싱글톤 인스턴스가 중복으로 생성되는 것을 방지합니다.
if (_instance != null && _instance != this)
{
Debug.LogWarning("Another instance of FactoryObjectSelectionManager exists, destroying this one.");
Destroy(gameObject);
}
}
private void Update()
{
// 마우스 왼쪽 버튼 클릭을 감지합니다.
if (Input.GetMouseButtonDown(0))
{
// 포인터가 UI 요소 위에 있는지 확인합니다. UI 클릭 시에는 선택/해제 로직을 무시합니다.
//Debug.Log($"IsPointerOverUIObject() : {IsPointerOverUIObject()}");
if (IsPointerOverUIObject())
{
return;
}
// 메인 카메라에서 마우스 위치로 레이를 쏩니다.
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
bool raycastHit = Physics.Raycast(ray, out RaycastHit hit);
Debug.Log($"Raycast : {raycastHit}");
// 레이캐스트로 무언가 감지되었는지 확인합니다.
if (raycastHit)
{
// 감지된 객체 또는 그 부모 중에 FactoryObject 컴포넌트가 있는지 확인합니다.
if (hit.collider.GetComponentInParent<FactoryObject>() == null)
{
// FactoryObject가 아닌 다른 객체를 클릭한 경우, 모든 선택을 해제합니다.
DeselectAll();
}
// FactoryObject를 클릭한 경우는 FactoryObject의 OnPointerClick에서 처리하므로 여기서는 별도 처리를 하지 않습니다.
}
else
{
// 레이캐스트로 아무것도 감지되지 않았을 경우 (빈 공간 클릭), 모든 선택을 해제합니다.
DeselectAll();
}
}
}
/// <summary>
/// 포인터가 UI 객체 위에 있는지 확인합니다.
/// </summary>
/// <returns>UI 객체 위에 있으면 true, 그렇지 않으면 false를 반환합니다.</returns>
private bool IsPointerOverUIObject()
{
// EventSystem이 없는 경우 false를 반환합니다.
if (EventSystem.current == null) return false;
// 현재 포인터 위치에 대한 이벤트 데이터를 생성합니다.
PointerEventData eventData = new PointerEventData(EventSystem.current)
{
position = Input.mousePosition
};
// 레이캐스트 결과를 저장할 리스트를 생성합니다.
List<RaycastResult> results = new List<RaycastResult>();
// 현재 포인터 위치에 있는 모든 UI 객체를 가져옵니다.
EventSystem.current.RaycastAll(eventData, results);
bool hasCanvas = false;
foreach (var result in results)
{
if(result.gameObject.GetComponentInParent<Canvas>() != null)
{
// UI 객체가 Canvas의 자식인 경우, UI 위에 있는 것으로 간주합니다.
return true;
}
}
// 결과가 하나라도 있으면 UI 위에 있는 것으로 간주합니다.
return hasCanvas;
}
/// <summary>
/// 지정된 FactoryObject를 선택합니다.
/// </summary>
/// <param name="factoryObject">선택할 객체입니다.</param>
/// <param name="isMultiSelect">다중 선택 모드 여부입니다. true이면 기존 선택에 추가하고, false이면 기존 선택을 해제하고 새로 선택합니다.</param>
public void Select(FactoryObject factoryObject, bool isMultiSelect = false)
{
if (!isMultiSelect)
{
// 단일 선택 모드일 경우, 기존에 선택된 모든 객체를 해제합니다.
DeselectAll();
}
Debug.Log($"Selecting FactoryObject: {factoryObject.Info?.Name}, MultiSelect: {isMultiSelect}");
if (!_selectedObjects.Contains(factoryObject))
{
// 객체가 아직 선택되지 않았다면 선택 목록에 추가하고 외곽선을 표시합니다.
_selectedObjects.Add(factoryObject);
factoryObject.ShowOutLine();
}
else if (isMultiSelect)
{
// 다중 선택 모드에서 이미 선택된 객체를 다시 클릭하면 선택을 해제합니다.
Deselect(factoryObject);
}
}
/// <summary>
/// 지정된 FactoryObject의 선택을 해제합니다.
/// </summary>
/// <param name="factoryObject">선택 해제할 객체입니다.</param>
public void Deselect(FactoryObject factoryObject)
{
if (_selectedObjects.Contains(factoryObject))
{
// 객체가 선택 목록에 있으면 외곽선을 숨기고 목록에서 제거합니다.
factoryObject.HideOutLine();
_selectedObjects.Remove(factoryObject);
}
}
/// <summary>
/// 현재 선택된 모든 FactoryObject의 선택을 해제합니다.
/// </summary>
public void DeselectAll()
{
// 성능을 위해 ToList()를 사용하여 반복 중 컬렉션 변경 문제를 방지합니다.
foreach (var selectedObject in _selectedObjects.ToList())
{
Deselect(selectedObject);
}
_selectedObjects.Clear();
if(InfoWindow.Instance.IsVisible) InfoWindow.Instance.Hide();
}
/// <summary>
/// 현재 선택된 모든 FactoryObject의 리스트를 가져옵니다.
/// </summary>
/// <returns>선택된 객체들의 읽기 전용 리스트입니다.</returns>
public IReadOnlyList<FactoryObject> GetSelectedObjects()
{
return _selectedObjects.AsReadOnly();
}
private void OnDestroy()
{
_applicationIsQuitting = true;
}
}
}