335 lines
14 KiB
C#
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);
|
|
|
|
}
|
|
}
|
|
}
|