2025-08-11 18:30:13 +09:00
|
|
|
#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 클릭 시에는 선택/해제 로직을 무시합니다.
|
2025-08-13 18:35:11 +09:00
|
|
|
//Debug.Log($"IsPointerOverUIObject() : {IsPointerOverUIObject()}");
|
2025-08-11 18:30:13 +09:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|