#nullable enable using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UVC.Data; using UVC.Data.Core; using UVC.Extention; using UVC.Factory.Modal; using UVC.Object3d; namespace UVC.Factory.Component { /// /// 팩토리 내의 상호작용 가능한 3D 객체를 위한 추상 기본 클래스입니다. /// 이 클래스는 Unity GameObject를 데이터(DataObject)와 연결하고, 사용자 상호작용(예: 클릭)을 처리하며, /// 데이터 변경에 따라 객체의 상태(예: 색상, 애니메이션)를 업데이트하는 기능을 제공합니다. /// /// /// 팩토리 객체는 고유한 정보(Info)와 동적 데이터(data)를 가집니다. /// 사용자가 객체를 클릭하면 OnPointerClick 이벤트가 발생하여 InfoWindow에 관련 데이터를 표시할 수 있습니다. /// ProcessData 메서드를 재정의하여 데이터 변경 시 특정 로직을 수행하도록 구현해야 합니다. /// /// /// 다음은 FactoryObject를 상속받아 'MachineObject'라는 구체적인 클래스를 만드는 예제입니다. /// 이 예제에서는 데이터로 받은 'status' 값에 따라 머신의 색상을 변경합니다. /// /// using UnityEngine; /// using UVC.Data; /// using UVC.Factory.Component; /// /// // FactoryObject를 상속받는 MachineObject 클래스 정의 /// public class MachineObject : FactoryObject /// { /// private Renderer objectRenderer; /// /// private void Awake() /// { /// // 색상을 변경할 렌더러 컴포넌트를 미리 찾아둡니다. /// objectRenderer = GetComponent(); /// } /// /// // ProcessData 메서드를 재정의하여 데이터 처리 로직을 구현합니다. /// protected override void ProcessData(DataObject newData) /// { /// // 'status' 키가 데이터에 포함되어 있는지 확인합니다. /// if (newData.ContainsKey("status")) /// { /// // 'status' 값을 문자열로 가져옵니다. /// string status = newData.GetString("status", "off"); /// /// // 상태 값에 따라 머티리얼의 색상을 변경합니다. /// switch (status) /// { /// case "running": /// objectRenderer.material.color = Color.green; /// break; /// case "warning": /// objectRenderer.material.color = Color.yellow; /// break; /// case "error": /// objectRenderer.material.color = Color.red; /// break; /// default: /// objectRenderer.material.color = Color.gray; /// break; /// } /// } /// } /// } /// /// // 아래는 MachineObject를 생성하고 데이터를 업데이트하는 예시입니다. /// public class MachineManager : MonoBehaviour /// { /// void Start() /// { /// // 1. 게임 오브젝트를 생성하고 MachineObject 컴포넌트를 추가합니다. /// GameObject machineGo = new GameObject("Drilling Machine"); /// machineGo.AddComponent(); // 렌더링을 위한 기본 컴포넌트 /// machineGo.AddComponent(); /// machineGo.AddComponent(); // 클릭 이벤트를 위한 콜라이더 /// MachineObject machine = machineGo.AddComponent(); /// /// // 2. 객체 정보(Info)를 설정합니다. /// machine.Info = new FactoryObjectInfo /// { /// Id = "MC-001", /// Name = "Drilling Machine" /// }; /// /// // 3. 초기 데이터를 생성하고 UpdateData를 통해 전달합니다. /// // 이 시점에 ProcessData가 호출되어 머신 색상이 녹색으로 변경됩니다. /// var initialData = new DataObject /// { /// { "status", "running" }, /// { "temperature", 85.5f }, /// { "operator", "Admin" } /// }; /// machine.UpdateData(initialData); /// /// // 4. 5초 후 데이터 변경을 시뮬레이션합니다. /// // 상태가 'error'로 변경되면 ProcessData가 다시 호출되어 색상이 빨간색으로 바뀝니다. /// StartCoroutine(SimulateError(machine)); /// } /// /// private System.Collections.IEnumerator SimulateError(MachineObject machine) /// { /// yield return new WaitForSeconds(5); /// /// var errorData = new DataObject { { "status", "error" } }; /// machine.UpdateData(errorData); /// } /// } /// /// public abstract class FactoryObject : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler { [Tooltip("3D 모델 객체")] public InteractiveObject? modelObject; protected FactoryObjectInfo? info; /// /// 팩토리 객체의 고유한 식별 정보(ID, 이름 등)를 가져오거나 설정합니다. /// 이 정보는 객체를 관리하고 UI에 표시하는 데 사용됩니다. /// public FactoryObjectInfo? Info { get => info; set { info = value; if (info != null) { // 객체의 이름을 GameObject의 이름으로 설정합니다. gameObject.name = info.Name; RegisterFactoryObject(); } } } protected DataObject? data; protected UserSetting? dataDisplaySetting; /// /// 객체 클릭 시 정보창(InfoWindow)에 표시될 데이터의 순서와 항목을 지정하는 마스크입니다. /// 이 리스트에 포함된 키의 데이터만 순서대로 표시됩니다. null이거나 비어있으면 모든 데이터를 표시합니다. /// public UserSetting? DataOrderedMask { get => dataDisplaySetting; set { dataDisplaySetting = value; } } private Renderer[]? objectRenderers; /// /// 객체와 모든 자식의 렌더링 상태를 제어합니다. /// true이면 보이게, false이면 보이지 않게 설정합니다. /// public bool IsVisible { get => objectRenderers?.Length > 0 && objectRenderers[0] != null && objectRenderers[0].enabled; set { if (objectRenderers != null) { foreach (var r in objectRenderers) { if (r != null) r.enabled = value; } } } } protected virtual void Awake() { // 초기화 작업을 수행합니다. if (modelObject == null) { Debug.LogWarning("FactoryObject requires an InteractiveObject component."); } else { modelObject.OnPointerClickHandler += OnPointerClick; modelObject.OnPointerEnterHandler += OnPointerEnter; modelObject.OnPointerExitHandler += OnPointerExit; } // 자신과 모든 자식 객체에서 Renderer 컴포넌트를 찾아 캐시합니다. objectRenderers = GetComponentsInChildren(); } /// /// FactoryObject를 FactoryObjectManager에 등록합니다. /// public virtual void RegisterFactoryObject() { if (Info != null) FactoryObjectManager.Instance.RegisterFactoryObject(this); } /// /// FactoryObject를 FactoryObjectManager에서 등록 해제합니다. /// public virtual void UnregisterFactoryObject() { if(Info != null) FactoryObjectManager.Instance.UnregisterFactoryObject(Info); } protected virtual void OnDestroy() { if (modelObject != null) { modelObject.OnPointerClickHandler -= OnPointerClick; modelObject.OnPointerEnterHandler -= OnPointerEnter; modelObject.OnPointerExitHandler -= OnPointerExit; } UnregisterFactoryObject(); } /// /// 포인터 클릭 이벤트를 처리하고 관련 데이터가 포함된 정보 창을 표시합니다. /// /// /// 이 메서드는 `InteractiveObject`로부터 상속받아 재정의되었습니다. /// 객체에 유효한 데이터가 있을 경우, `InfoWindow`를 통해 사용자에게 데이터를 보여줍니다. /// `DataOrderedMask`가 설정되어 있으면 해당 순서대로, 그렇지 않으면 모든 데이터를 표시합니다. /// /// 포인터 클릭과 관련된 이벤트 데이터입니다. public virtual void OnPointerClick(PointerEventData eventData) { if (data != null && data.Count > 0) { OrderedDictionary info = new OrderedDictionary(); // dataOrderedMask가 설정되어 있으면 해당 순서대로 정보를 가져옵니다. if (dataDisplaySetting != null && dataDisplaySetting.Count > 0) { foreach (var keyValue in dataDisplaySetting) { if (data.ContainsKey(keyValue.Key)) { info[keyValue.Value.ToString()] = data[keyValue.Key]!; } } } else { // dataOrderedMask가 설정되어 있지 않으면 모든 데이터를 사용합니다. info = new OrderedDictionary(data!); } //순서 바꾸지 말것. FactoryObjectSelectionManager.Instance.Select 호출 후 InfoWindow.Instance.Show FactoryObjectSelectionManager.Instance.Select(this); InfoWindow.Instance.Show(transform, info); } } /// /// 포인터가 이 객체 위로 들어왔을 때 호출됩니다. 하이라이트 효과 등에 사용할 수 있습니다. /// /// 포인터 이벤트와 관련된 데이터입니다. public void OnPointerEnter(PointerEventData eventData) { } /// /// 포인터가 이 객체에서 벗어났을 때 호출됩니다. /// /// 포인터 이벤트와 관련된 데이터입니다. public void OnPointerExit(PointerEventData eventData) { } /// /// 외부로부터 받은 새로운 데이터로 객체의 상태를 업데이트합니다. /// 이 메서드는 내부적으로 `ProcessData`를 호출하여 실제 데이터 처리 로직을 수행합니다. /// MqttSubscriptionConfig.updatedDataOnly가 true인 경우, 데이터가 변경된 경우에만 호출됩니다. /// /// 업데이트할 새로운 데이터가 포함된 IDataObject 객체입니다. public void UpdateData(DataObject newData) { if (newData == null) return; ProcessData(newData); } /// /// 지정된 데이터 객체를 처리합니다. 이 메서드는 파생 클래스에서 재정의되어 /// 사용자 지정 데이터 처리 로직을 구현하도록 설계되었습니다. /// /// /// `UpdateData`가 호출될 때 실행됩니다. 파생 클래스에서는 이 메서드를 재정의하여 /// 데이터 값에 따라 객체의 색상, 애니메이션, 동작 등을 변경하는 코드를 작성해야 합니다. /// /// 처리할 데이터 객체입니다. null일 수 없습니다. protected virtual void ProcessData(DataObject newData) { } /// /// 선택 된 효과로 외곽선을 표시합니다. /// public virtual void ShowOutLine() { } /// /// 외곽선을 숨깁니다. /// public virtual void HideOutLine() { } /// /// 즉시 객체의 상태를 업데이트합니다. /// /// 업데이트 횟수. 기본값은 1입니다. public virtual void UpdateImmediately(int count = 1) { } /// /// 객체의 위치를 가져옵니다. 월드 좌표 또는 로컬 좌표로 반환할 수 있습니다. /// /// true이면 월드 좌표, false이면 부모 기준의 로컬 좌표를 반환합니다. /// 객체의 Vector3 위치 값입니다. public Vector3 GetPosition(bool world = false) { if (!world) return transform.position; return transform.TransformPoint(transform.position); } /// /// 객체의 진입점 위치를 가져옵니다. 주로 다른 객체(예: AGV)가 이 객체로 접근할 때 목표 지점으로 사용됩니다. /// /// true이면 월드 좌표, false이면 부모 기준의 로컬 좌표를 반환합니다. /// 객체의 진입점 Vector3 위치 값입니다. public Vector3 GetEntrancePosition(bool world = false) { if (!world) return transform.position; return transform.TransformPoint(transform.position); } } }