241 lines
11 KiB
C#
241 lines
11 KiB
C#
#nullable enable
|
|
using Cysharp.Threading.Tasks;
|
|
using SampleProject;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UVC.Core;
|
|
using UVC.Data;
|
|
|
|
namespace UVC.Factory.Component
|
|
{
|
|
/// <summary>
|
|
/// 씬에 존재하는 모든 AGV(Automated Guided Vehicle, 무인 운반차) 객체를 총괄 관리하는 싱글톤 클래스입니다.
|
|
/// 외부 데이터 소스(예: MQTT)로부터 AGV의 실시간 데이터를 수신하여, 씬에 AGV를 동적으로 생성, 업데이트, 제거하는 역할을 담당합니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 매니저는 `SingletonScene`을 상속받아 구현되었으므로, `AGVManager.Instance`를 통해 씬의 어디에서든 쉽게 접근할 수 있습니다.
|
|
/// 씬이 초기화될 때(`OnSceneInitialized`) MQTT 파이프라인에 연결하여 'AGV' 토픽의 데이터를 구독하기 시작합니다.
|
|
/// 수신된 데이터(`DataArray`)를 분석하여 새로 추가된 AGV, 제거된 AGV, 정보가 변경된 AGV를 감지하고 각각에 맞는 처리를 비동기적으로 수행합니다.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// 이 매니저는 자동으로 작동하므로 다른 스크립트에서 직접 호출할 일은 거의 없습니다.
|
|
/// 대신 이 매니저가 어떻게 AGV 객체를 생성하고 데이터를 전달하는지, 그리고 AGV 객체는 그 데이터를 어떻게 처리하는지를 이해하는 것이 중요합니다.
|
|
///
|
|
/// 1. **데이터 수신**: 서버로부터 다음과 같은 JSON 데이터 배열을 수신했다고 가정합니다.
|
|
/// (실제로는 DataArray 객체로 변환되어 전달됩니다.)
|
|
/// ```json
|
|
/// [
|
|
/// { "VHL_NAME": "AGV-01", "X": 105, "Y": 210, "DEGREE": 90.0, "VHL_STATE": "RUN" },
|
|
/// { "VHL_NAME": "AGV-02", "X": 150, "Y": 300, "DEGREE": 180.0, "VHL_STATE": "IDLE" }
|
|
/// ]
|
|
/// ```
|
|
///
|
|
/// 2. **매니저의 동작**: `OnUpdateDataAsync` 메서드가 이 데이터를 받아 처리합니다.
|
|
/// - `agvList`에 "AGV-01"이 없으면, `CreateAGV`를 호출하여 프리팹으로부터 새로운 AGV 게임 오브젝트를 생성합니다.
|
|
/// - 생성된 AGV 객체의 `UpdateData` 메서드를 호출하여 위치, 각도 등의 데이터를 전달합니다.
|
|
/// - 만약 기존에 있던 "AGV-01"의 데이터가 변경된 것이라면(ModifiedList), `agvList`에서 해당 AGV를 찾아 `UpdateData`만 호출합니다.
|
|
///
|
|
/// 3. **AGV 객체의 반응**: AGV.cs 에서는 `UpdateData` 메서드를 통해 받은 데이터로 자신의 상태를 갱신합니다.
|
|
/// (AGV.cs 파일에 포함될 수 있는 예시 코드)
|
|
/// <code>
|
|
/// public class AGV : FactoryObject
|
|
/// {
|
|
/// // ... (moveSpeed, rotationSpeed 등 변수 선언)
|
|
///
|
|
/// protected override void ProcessData(DataObject newData)
|
|
/// {
|
|
/// // AGVManager로부터 전달받은 데이터에 위치(X, Y)나 각도(DEGREE) 정보가 있는지 확인
|
|
/// if (newData.ContainsKey("X") && newData.ContainsKey("Y") && newData.ContainsKey("DEGREE"))
|
|
/// {
|
|
/// // 데이터로부터 목표 위치와 회전값 계산
|
|
/// float x = newData.GetFloat("X");
|
|
/// float y = newData.GetFloat("Y");
|
|
/// float degree = newData.GetFloat("DEGREE");
|
|
///
|
|
/// // Unity 월드 좌표계에 맞게 변환 (예시: Z를 Y로 사용, 스케일 조정)
|
|
/// Vector3 targetPosition = new Vector3(x * 0.01f, 0, y * 0.01f);
|
|
/// Quaternion targetRotation = Quaternion.Euler(0, degree, 0);
|
|
///
|
|
/// // 부드러운 이동 및 회전을 위해 Lerp 또는 다른 방법을 사용하여 현재 위치/회전을 업데이트
|
|
/// // (이 로직은 보통 Unity의 Update 메서드에서 처리됩니다)
|
|
/// this.targetPosition = targetPosition;
|
|
/// this.targetRotation = targetRotation;
|
|
/// }
|
|
///
|
|
/// // 상태(VHL_STATE)에 따라 AGV의 색상이나 애니메이션 변경
|
|
/// if (newData.ContainsKey("VHL_STATE"))
|
|
/// {
|
|
/// string status = newData.GetString("VHL_STATE");
|
|
/// // ... (상태에 따른 시각적 처리)
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
/// </example>
|
|
public class AGVManager : SingletonScene<AGVManager>
|
|
{
|
|
|
|
private readonly string prefabPath = "Prefabs/SampleProject/Factory/AGV";
|
|
private GameObject? prefab;
|
|
private List<AGV> agvList;
|
|
|
|
/// <summary>
|
|
/// AGVManager의 초기화 메서드입니다.
|
|
/// Awake 메서드에서 호출되며, MonoBehaviour가 생성될 때 한 번만 실행됩니다.
|
|
/// </summary>
|
|
protected override void Init()
|
|
{
|
|
agvList = new List<AGV>();
|
|
SceneMain.Instance.Initialized += OnSceneInitialized;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 씬이 완전히 초기화된 후 호출됩니다.
|
|
/// AGV 데이터를 수신하기 위한 MQTT 파이프라인을 설정합니다.
|
|
/// </summary>
|
|
private void OnSceneInitialized()
|
|
{
|
|
//데이터를 어떤 형식으로 받을지 정의합니다.
|
|
var dataMask = new DataMask();
|
|
dataMask["VHL_NAME"] = "";
|
|
dataMask["AGV_IDX"] = "";
|
|
dataMask["B_INSTALL"] = "";
|
|
dataMask["NODE_ID"] = "";
|
|
dataMask["REAL_ID"] = "";
|
|
dataMask["VHL_STATE"] = "";
|
|
dataMask["BAY_LIST"] = "";
|
|
dataMask["X"] = 0;
|
|
dataMask["Y"] = 0;
|
|
dataMask["DEGREE"] = 0.0;
|
|
dataMask["MODE"] = "";
|
|
dataMask["BATT"] = "";
|
|
dataMask["SUB_GOAL"] = "";
|
|
dataMask["FINAL_GOAL"] = "";
|
|
dataMask["JOB_ID"] = "";
|
|
dataMask["TIMESTAMP"] = DateTime.Now;
|
|
|
|
// MQTT 파이프라인 정보를 생성합니다.
|
|
// 'AGV' 토픽을 구독하고, 받은 데이터는 위에서 정의한 dataMask로 매핑하며,
|
|
// 데이터 유효성 검사를 위해 DataValidator를 설정합니다.
|
|
// 데이터가 업데이트되면 OnUpdateData 메서드를 호출하여 처리합니다.
|
|
|
|
DataValidator validator = new DataValidator();
|
|
validator.AddValidator("JOB_ID", value => value != null);
|
|
|
|
var pipelineInfo = new MQTTPipeLineInfo("AGV")
|
|
.setDataMapper(new DataMapper(dataMask))
|
|
.setValidator(validator)
|
|
.setHandler(OnUpdateData);
|
|
|
|
// 생성한 파이프라인 정보를 전역 MQTT 파이프라인에 추가합니다.
|
|
AppMain.Instance.MQTTPipeLine.Add(pipelineInfo);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 데이터 수신 시 호출되는 공개 핸들러입니다.
|
|
/// 비동기 처리 메서드인 OnUpdateDataAsync를 호출합니다.
|
|
/// </summary>
|
|
/// <param name="data">수신된 데이터 객체 (일반적으로 DataArray 형태)</param>
|
|
public void OnUpdateData(IDataObject? data)
|
|
{
|
|
OnUpdateDataAsync(data).Forget();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 수신된 AGV 데이터 배열을 비동기적으로 처리하여 씬에 반영합니다.
|
|
/// 추가, 제거, 수정된 AGV 데이터를 각각 구분하여 처리합니다.
|
|
/// </summary>
|
|
/// <param name="data">수신된 데이터 (DataArray)</param>
|
|
public async UniTask OnUpdateDataAsync(IDataObject? data)
|
|
{
|
|
if (data == null) return;
|
|
DataArray? arr = data as DataArray;
|
|
if (arr == null || arr.Count == 0) return;
|
|
|
|
// 데이터 배열에서 추가, 제거, 수정된 항목 리스트를 가져옵니다.
|
|
var AddedItems = arr.AddedItems;
|
|
var RemovedItems = arr.RemovedItems;
|
|
var ModifiedList = arr.ModifiedList;
|
|
|
|
// 새로 추가된 AGV 처리
|
|
foreach (var item in AddedItems.ToList())
|
|
{
|
|
AGV? agv = await CreateAGV(item);
|
|
if (agv != null)
|
|
{
|
|
agv.UpdateData(item);
|
|
agvList.Add(agv);
|
|
}
|
|
}
|
|
|
|
// 제거된 AGV 처리
|
|
foreach (var item in RemovedItems.ToList())
|
|
{
|
|
AGV agv = agvList.Find(x => x.Info != null && x.Info.Name == item.GetString("VHL_NAME"));
|
|
if (agv != null)
|
|
{
|
|
Destroy(agv.gameObject);
|
|
agvList.Remove(agv);
|
|
}
|
|
}
|
|
|
|
// 정보가 수정된 AGV 처리
|
|
foreach (var item in ModifiedList.ToList())
|
|
{
|
|
AGV agv = agvList.Find(x => x.Info != null && x.Info.Name == item.GetString("VHL_NAME"));
|
|
if (agv != null)
|
|
{
|
|
agv.UpdateData(item);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGV 프리팹을 사용하여 새로운 AGV 게임 오브젝트를 생성하고 초기화합니다.
|
|
/// </summary>
|
|
/// <param name="data">신규 AGV의 정보가 담긴 DataObject</param>
|
|
/// <returns>생성 및 초기화된 AGV 컴포넌트, 실패 시 null</returns>
|
|
private async UniTask<AGV?> CreateAGV(DataObject data)
|
|
{
|
|
if (prefab == null)
|
|
{
|
|
prefab = await Resources.LoadAsync<GameObject>(prefabPath) as GameObject;
|
|
if (prefab == null)
|
|
{
|
|
Debug.LogError($"Prefab not found at path: {prefabPath}");
|
|
return null;
|
|
}
|
|
}
|
|
GameObject prefabInstance = Instantiate(prefab, transform);
|
|
if (prefabInstance == null)
|
|
{
|
|
Debug.LogError("Failed to instantiate AGV prefab.");
|
|
return null;
|
|
}
|
|
var agv = prefabInstance.GetComponent<AGV>();
|
|
// AGV의 고정 정보(Info)를 설정합니다. 이 정보는 FactoryObjectManager 등에서 사용될 수 있습니다.
|
|
agv.Info = new FactoryObjectInfo(
|
|
data.GetString("VHL_NAME"),
|
|
data.GetString("NODE_ID"),
|
|
"",
|
|
"",
|
|
data.GetString("MODE")
|
|
);
|
|
return agv;
|
|
}
|
|
|
|
/// <summary>
|
|
/// AGVManager가 파괴될 때 호출됩니다.
|
|
/// MQTT 파이프라인에서 'AGV' 핸들러를 제거하여 메모리 누수를 방지합니다.
|
|
/// </summary>
|
|
protected override void OnDestroy()
|
|
{
|
|
base.OnDestroy();
|
|
AppMain.Instance.MQTTPipeLine.Remove("AGV");
|
|
}
|
|
}
|
|
}
|