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

335 lines
14 KiB
C#

#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
{
/// <summary>
/// 팩토리 내의 상호작용 가능한 3D 객체를 위한 추상 기본 클래스입니다.
/// 이 클래스는 Unity GameObject를 데이터(DataObject)와 연결하고, 사용자 상호작용(예: 클릭)을 처리하며,
/// 데이터 변경에 따라 객체의 상태(예: 색상, 애니메이션)를 업데이트하는 기능을 제공합니다.
/// </summary>
/// <remarks>
/// 팩토리 객체는 고유한 정보(Info)와 동적 데이터(data)를 가집니다.
/// 사용자가 객체를 클릭하면 OnPointerClick 이벤트가 발생하여 InfoWindow에 관련 데이터를 표시할 수 있습니다.
/// ProcessData 메서드를 재정의하여 데이터 변경 시 특정 로직을 수행하도록 구현해야 합니다.
/// </remarks>
/// <example>
/// 다음은 FactoryObject를 상속받아 'MachineObject'라는 구체적인 클래스를 만드는 예제입니다.
/// 이 예제에서는 데이터로 받은 'status' 값에 따라 머신의 색상을 변경합니다.
/// <code>
/// using UnityEngine;
/// using UVC.Data;
/// using UVC.Factory.Component;
///
/// // FactoryObject를 상속받는 MachineObject 클래스 정의
/// public class MachineObject : FactoryObject
/// {
/// private Renderer objectRenderer;
///
/// private void Awake()
/// {
/// // 색상을 변경할 렌더러 컴포넌트를 미리 찾아둡니다.
/// objectRenderer = GetComponent<Renderer>();
/// }
///
/// // 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<MeshFilter>(); // 렌더링을 위한 기본 컴포넌트
/// machineGo.AddComponent<MeshRenderer>();
/// machineGo.AddComponent<BoxCollider>(); // 클릭 이벤트를 위한 콜라이더
/// MachineObject machine = machineGo.AddComponent<MachineObject>();
///
/// // 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);
/// }
/// }
/// </code>
/// </example>
public abstract class FactoryObject : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
{
[Tooltip("3D 모델 객체")]
public InteractiveObject? modelObject;
protected FactoryObjectInfo? info;
/// <summary>
/// 팩토리 객체의 고유한 식별 정보(ID, 이름 등)를 가져오거나 설정합니다.
/// 이 정보는 객체를 관리하고 UI에 표시하는 데 사용됩니다.
/// </summary>
public FactoryObjectInfo? Info
{
get => info;
set
{
info = value;
if (info != null)
{
// 객체의 이름을 GameObject의 이름으로 설정합니다.
gameObject.name = info.Name;
RegisterFactoryObject();
}
}
}
protected DataObject? data;
protected UserSetting? dataDisplaySetting;
/// <summary>
/// 객체 클릭 시 정보창(InfoWindow)에 표시될 데이터의 순서와 항목을 지정하는 마스크입니다.
/// 이 리스트에 포함된 키의 데이터만 순서대로 표시됩니다. null이거나 비어있으면 모든 데이터를 표시합니다.
/// </summary>
public UserSetting? DataOrderedMask
{
get => dataDisplaySetting;
set
{
dataDisplaySetting = value;
}
}
private Renderer[]? objectRenderers;
/// <summary>
/// 객체와 모든 자식의 렌더링 상태를 제어합니다.
/// true이면 보이게, false이면 보이지 않게 설정합니다.
/// </summary>
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<Renderer>();
}
/// <summary>
/// FactoryObject를 FactoryObjectManager에 등록합니다.
/// </summary>
public virtual void RegisterFactoryObject()
{
if (Info != null) FactoryObjectManager.Instance.RegisterFactoryObject(this);
}
/// <summary>
/// FactoryObject를 FactoryObjectManager에서 등록 해제합니다.
/// </summary>
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();
}
/// <summary>
/// 포인터 클릭 이벤트를 처리하고 관련 데이터가 포함된 정보 창을 표시합니다.
/// </summary>
/// <remarks>
/// 이 메서드는 `InteractiveObject`로부터 상속받아 재정의되었습니다.
/// 객체에 유효한 데이터가 있을 경우, `InfoWindow`를 통해 사용자에게 데이터를 보여줍니다.
/// `DataOrderedMask`가 설정되어 있으면 해당 순서대로, 그렇지 않으면 모든 데이터를 표시합니다.
/// </remarks>
/// <param name="eventData">포인터 클릭과 관련된 이벤트 데이터입니다.</param>
public virtual void OnPointerClick(PointerEventData eventData)
{
if (data != null && data.Count > 0)
{
OrderedDictionary<string, object> info = new OrderedDictionary<string, object>();
// 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<string, object>(data!);
}
//순서 바꾸지 말것. FactoryObjectSelectionManager.Instance.Select 호출 후 InfoWindow.Instance.Show
FactoryObjectSelectionManager.Instance.Select(this);
InfoWindow.Instance.Show(transform, info);
}
}
/// <summary>
/// 포인터가 이 객체 위로 들어왔을 때 호출됩니다. 하이라이트 효과 등에 사용할 수 있습니다.
/// </summary>
/// <param name="eventData">포인터 이벤트와 관련된 데이터입니다.</param>
public void OnPointerEnter(PointerEventData eventData) { }
/// <summary>
/// 포인터가 이 객체에서 벗어났을 때 호출됩니다.
/// </summary>
/// <param name="eventData">포인터 이벤트와 관련된 데이터입니다.</param>
public void OnPointerExit(PointerEventData eventData) { }
/// <summary>
/// 외부로부터 받은 새로운 데이터로 객체의 상태를 업데이트합니다.
/// 이 메서드는 내부적으로 `ProcessData`를 호출하여 실제 데이터 처리 로직을 수행합니다.
/// MqttSubscriptionConfig.updatedDataOnly가 true인 경우, 데이터가 변경된 경우에만 호출됩니다.
/// </summary>
/// <param name="newData">업데이트할 새로운 데이터가 포함된 IDataObject 객체입니다.</param>
public void UpdateData(DataObject newData)
{
if (newData == null) return;
ProcessData(newData);
}
/// <summary>
/// 지정된 데이터 객체를 처리합니다. 이 메서드는 파생 클래스에서 재정의되어
/// 사용자 지정 데이터 처리 로직을 구현하도록 설계되었습니다.
/// </summary>
/// <remarks>
/// `UpdateData`가 호출될 때 실행됩니다. 파생 클래스에서는 이 메서드를 재정의하여
/// 데이터 값에 따라 객체의 색상, 애니메이션, 동작 등을 변경하는 코드를 작성해야 합니다.
/// </remarks>
/// <param name="newData">처리할 데이터 객체입니다. null일 수 없습니다.</param>
protected virtual void ProcessData(DataObject newData) { }
/// <summary>
/// 선택 된 효과로 외곽선을 표시합니다.
/// </summary>
public virtual void ShowOutLine() { }
/// <summary>
/// 외곽선을 숨깁니다.
/// </summary>
public virtual void HideOutLine() { }
/// <summary>
/// 즉시 객체의 상태를 업데이트합니다.
/// </summary>
/// <param name="count">업데이트 횟수. 기본값은 1입니다.</param>
public virtual void UpdateImmediately(int count = 1) { }
/// <summary>
/// 객체의 위치를 가져옵니다. 월드 좌표 또는 로컬 좌표로 반환할 수 있습니다.
/// </summary>
/// <param name="world">true이면 월드 좌표, false이면 부모 기준의 로컬 좌표를 반환합니다.</param>
/// <returns>객체의 Vector3 위치 값입니다.</returns>
public Vector3 GetPosition(bool world = false)
{
if (!world) return transform.position;
return transform.TransformPoint(transform.position);
}
/// <summary>
/// 객체의 진입점 위치를 가져옵니다. 주로 다른 객체(예: AGV)가 이 객체로 접근할 때 목표 지점으로 사용됩니다.
/// </summary>
/// <param name="world">true이면 월드 좌표, false이면 부모 기준의 로컬 좌표를 반환합니다.</param>
/// <returns>객체의 진입점 Vector3 위치 값입니다.</returns>
public Vector3 GetEntrancePosition(bool world = false)
{
if (!world) return transform.position;
return transform.TransformPoint(transform.position);
}
}
}