#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 { /// /// 씬에 배치된 FactoryObject의 선택을 관리하는 싱글톤 클래스입니다. /// 단일 선택, 다중 선택(추후 확장), 선택 해제 로직을 처리합니다. /// /// /// 이 관리자는 다음 기능을 제공합니다: /// - FactoryObject 클릭 시 선택 및 외곽선 표시. /// - 다른 객체 선택 시 이전에 선택된 객체의 외곽선 숨김. /// - UI가 아닌 빈 공간 클릭 시 모든 선택 해제. /// - 다중 선택을 위한 기반 제공. /// /// 이 클래스가 올바르게 작동하려면 씬에 Unity의 EventSystem이 존재해야 합니다. /// /// /// /// // 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("선택된 객체가 없습니다."); /// } /// } /// } /// } /// /// public class FactoryObjectSelectionManager : MonoBehaviour { private static FactoryObjectSelectionManager? _instance; private static readonly object _lock = new object(); private static bool _applicationIsQuitting = false; /// /// SelectionManager의 싱글톤 인스턴스를 가져옵니다. /// 씬에 인스턴스가 없으면 자동으로 생성합니다. /// 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(); if (_instance == null) { // 씬에 인스턴스가 없으면 새로 생성합니다. var singletonObject = new GameObject(); _instance = singletonObject.AddComponent(); singletonObject.name = typeof(FactoryObjectSelectionManager).ToString() + " (Singleton)"; // 씬 전환 시 파괴되지 않도록 설정합니다. DontDestroyOnLoad(singletonObject); } } return _instance; } } } // 현재 선택된 객체들을 저장하는 리스트입니다. private readonly List _selectedObjects = new List(); 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() == null) { // FactoryObject가 아닌 다른 객체를 클릭한 경우, 모든 선택을 해제합니다. DeselectAll(); } // FactoryObject를 클릭한 경우는 FactoryObject의 OnPointerClick에서 처리하므로 여기서는 별도 처리를 하지 않습니다. } else { // 레이캐스트로 아무것도 감지되지 않았을 경우 (빈 공간 클릭), 모든 선택을 해제합니다. DeselectAll(); } } } /// /// 포인터가 UI 객체 위에 있는지 확인합니다. /// /// UI 객체 위에 있으면 true, 그렇지 않으면 false를 반환합니다. private bool IsPointerOverUIObject() { // EventSystem이 없는 경우 false를 반환합니다. if (EventSystem.current == null) return false; // 현재 포인터 위치에 대한 이벤트 데이터를 생성합니다. PointerEventData eventData = new PointerEventData(EventSystem.current) { position = Input.mousePosition }; // 레이캐스트 결과를 저장할 리스트를 생성합니다. List results = new List(); // 현재 포인터 위치에 있는 모든 UI 객체를 가져옵니다. EventSystem.current.RaycastAll(eventData, results); bool hasCanvas = false; foreach (var result in results) { if(result.gameObject.GetComponentInParent() != null) { // UI 객체가 Canvas의 자식인 경우, UI 위에 있는 것으로 간주합니다. return true; } } // 결과가 하나라도 있으면 UI 위에 있는 것으로 간주합니다. return hasCanvas; } /// /// 지정된 FactoryObject를 선택합니다. /// /// 선택할 객체입니다. /// 다중 선택 모드 여부입니다. true이면 기존 선택에 추가하고, false이면 기존 선택을 해제하고 새로 선택합니다. 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); } } /// /// 지정된 FactoryObject의 선택을 해제합니다. /// /// 선택 해제할 객체입니다. public void Deselect(FactoryObject factoryObject) { if (_selectedObjects.Contains(factoryObject)) { // 객체가 선택 목록에 있으면 외곽선을 숨기고 목록에서 제거합니다. factoryObject.HideOutLine(); _selectedObjects.Remove(factoryObject); } } /// /// 현재 선택된 모든 FactoryObject의 선택을 해제합니다. /// public void DeselectAll() { // 성능을 위해 ToList()를 사용하여 반복 중 컬렉션 변경 문제를 방지합니다. foreach (var selectedObject in _selectedObjects.ToList()) { Deselect(selectedObject); } _selectedObjects.Clear(); if(InfoWindow.Instance.IsVisible) InfoWindow.Instance.Hide(); } /// /// 현재 선택된 모든 FactoryObject의 리스트를 가져옵니다. /// /// 선택된 객체들의 읽기 전용 리스트입니다. public IReadOnlyList GetSelectedObjects() { return _selectedObjects.AsReadOnly(); } private void OnDestroy() { _applicationIsQuitting = true; } } }