Files
XRLib/Assets/Scripts/UVC/Factory/Component/FactoryObject.cs
2025-06-25 18:50:19 +09:00

242 lines
11 KiB
C#

#nullable enable
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UVC.Data;
using UVC.Object3d;
using UVC.UI.Info;
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 : InteractiveObject
{
protected FactoryObjectInfo? info;
/// <summary>
/// 팩토리 객체의 고유한 식별 정보(ID, 이름 등)를 가져오거나 설정합니다.
/// 이 정보는 객체를 관리하고 UI에 표시하는 데 사용됩니다.
/// </summary>
public FactoryObjectInfo? Info
{
get => info;
set
{
info = value;
if (value != null)
{
// 객체의 이름을 GameObject의 이름으로 설정합니다.
gameObject.name = value.Name;
FactoryObjectManager.Instance.RegisterFactoryObject(this);
}
}
}
protected DataObject? data;
protected List<string>? dataOrderedMask;
/// <summary>
/// 객체 클릭 시 정보창(InfoWindow)에 표시될 데이터의 순서와 항목을 지정하는 마스크입니다.
/// 이 리스트에 포함된 키의 데이터만 순서대로 표시됩니다. null이거나 비어있으면 모든 데이터를 표시합니다.
/// </summary>
public List<string>? DataOrderedMask
{
get => dataOrderedMask;
set
{
dataOrderedMask = value;
}
}
private void OnDestroy()
{
FactoryObjectManager.Instance.UnregisterFactoryObject(Info!);
}
/// <summary>
/// 포인터 클릭 이벤트를 처리하고 관련 데이터가 포함된 정보 창을 표시합니다.
/// </summary>
/// <remarks>
/// 이 메서드는 `InteractiveObject`로부터 상속받아 재정의되었습니다.
/// 객체에 유효한 데이터가 있을 경우, `InfoWindow`를 통해 사용자에게 데이터를 보여줍니다.
/// `DataOrderedMask`가 설정되어 있으면 해당 순서대로, 그렇지 않으면 모든 데이터를 표시합니다.
/// </remarks>
/// <param name="eventData">포인터 클릭과 관련된 이벤트 데이터입니다.</param>
public override void OnPointerClick(PointerEventData eventData)
{
if (data != null && data.Count > 0)
{
Dictionary<string, object> info = new Dictionary<string, object>();
// dataOrderedMask가 설정되어 있으면 해당 순서대로 정보를 가져옵니다.
if (dataOrderedMask != null && dataOrderedMask.Count > 0)
{
foreach (var key in dataOrderedMask)
{
if (data.ContainsKey(key))
{
info[key] = data[key];
}
}
}
else
{
// dataOrderedMask가 설정되어 있지 않으면 모든 데이터를 사용합니다.
info = new Dictionary<string, object>(data);
}
InfoWindow.Instance.Show(transform, info);
}
}
/// <summary>
/// 외부로부터 받은 새로운 데이터로 객체의 상태를 업데이트합니다.
/// 이 메서드는 내부적으로 `ProcessData`를 호출하여 실제 데이터 처리 로직을 수행합니다.
/// MQTTPipeLineInfo.updatedDataOnly가 true인 경우, 데이터가 변경된 경우에만 호출됩니다.
/// </summary>
/// <param name="newData">업데이트할 새로운 데이터가 포함된 IDataObject 객체입니다.</param>
public void UpdateData(IDataObject? newData)
{
if(newData == null) return;
DataObject? dataObject = newData as DataObject;
if (dataObject == null || dataObject.Count == 0) return;
ProcessData(dataObject);
}
/// <summary>
/// 지정된 데이터 객체를 처리합니다. 이 메서드는 파생 클래스에서 재정의되어
/// 사용자 지정 데이터 처리 로직을 구현하도록 설계되었습니다.
/// </summary>
/// <remarks>
/// `UpdateData`가 호출될 때 실행됩니다. 파생 클래스에서는 이 메서드를 재정의하여
/// 데이터 값에 따라 객체의 색상, 애니메이션, 동작 등을 변경하는 코드를 작성해야 합니다.
/// </remarks>
/// <param name="newData">처리할 데이터 객체입니다. null일 수 없습니다.</param>
protected virtual void ProcessData(DataObject newData) {}
/// <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);
}
}
}