#nullable enable using Cysharp.Threading.Tasks; using SampleProject; using System; using System.Linq; using Unity.Burst.Intrinsics; using UnityEngine; using UVC.Core; using UVC.Data; using UVC.Data.Core; using UVC.Data.Mqtt; using UVC.Pool; namespace UVC.Factory.Component { /// /// 씬에 존재하는 모든 AGV(Automated Guided Vehicle, 무인 운반차) 객체를 총괄 관리하는 싱글톤 클래스입니다. /// 외부 데이터 소스(예: MQTT)로부터 AGV의 실시간 데이터를 수신하여, 씬에 AGV를 동적으로 생성, 업데이트, 제거하는 역할을 담당합니다. /// /// /// 이 매니저는 `SingletonScene`을 상속받아 구현되었으므로, `AGVManager.Instance`를 통해 씬의 어디에서든 쉽게 접근할 수 있습니다. /// 씬이 초기화될 때(`OnSceneInitialized`) MQTT 파이프라인에 연결하여 'AGV' 토픽의 데이터를 구독하기 시작합니다. /// 수신된 데이터(`DataArray`)를 분석하여 새로 추가된 AGV, 제거된 AGV, 정보가 변경된 AGV를 감지하고 각각에 맞는 처리를 비동기적으로 수행합니다. /// /// /// 이 매니저는 자동으로 작동하므로 다른 스크립트에서 직접 호출할 일은 거의 없습니다. /// 대신 이 매니저가 어떻게 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 파일에 포함될 수 있는 예시 코드) /// /// 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"); /// // ... (상태에 따른 시각적 처리) /// } /// } /// } /// /// public class AGVManager : SingletonScene { private readonly string prefabPath = "Prefabs/Factory/AGV"; private GameObjectPool? agvPool; public GameObjectPool AGVPool { get { if (agvPool == null) { Debug.LogError("AGVPool is not initialized. Please call InitializePoolAsync first."); } return agvPool!; } } public Action? OnAGVCreated; private bool created = false; public bool Created => created; /// /// AGVManager의 초기화 메서드입니다. /// Awake 메서드에서 호출되며, MonoBehaviour가 생성될 때 한 번만 실행됩니다. /// protected override void Init() { InitializePoolAsync().ContinueWith(() => { //playback에서도 데이터를 업데이트 하기에 DataRepository에 핸들러를 추가합니다. DataRepository.Instance.AddDataUpdateHandler("AGV", OnUpdateData); var pipelineInfo = new MqttSubscriptionConfig("AGV"); var dataMapperValidator = DataMapperValidator.Get("AGV"); if (dataMapperValidator?.DataMapper != null) pipelineInfo.setDataMapper(dataMapperValidator.DataMapper); if (dataMapperValidator?.Validator != null) pipelineInfo.setValidator(dataMapperValidator.Validator); // 생성한 파이프라인 정보를 전역 MQTT 파이프라인에 추가합니다. DataRepository.Instance.MqttReceiver.Add(pipelineInfo); SceneMain.Instance.Initialized += OnSceneInitialized; }); } /// /// 씬이 완전히 초기화된 후 호출됩니다. /// AGV 데이터를 수신하기 위한 MQTT 파이프라인을 설정합니다. /// private void OnSceneInitialized() { } /// /// AGV 풀을 비동기적으로 초기화합니다. /// private async UniTask InitializePoolAsync() { if (agvPool != null) return; var prefab = await Resources.LoadAsync(prefabPath) as GameObject; if (prefab == null) { Debug.LogError($"Prefab not found at path: {prefabPath}"); return; } agvPool = new GameObjectPool(prefab, transform); } /// /// 데이터 수신 시 호출되는 공개 핸들러입니다. /// 수신된 AGV 데이터 배열을 비동기적으로 처리하여 씬에 반영합니다. /// 추가, 제거, 수정된 AGV 데이터를 각각 구분하여 처리합니다. /// /// 수신된 데이터 객체 (DataArray 형태) public void OnUpdateData(IDataObject data) { if (data == null || agvPool == 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; //Debug.Log($"AGVManager received data: count:{arr.Count}, Added={AddedItems.Count}, Removed={RemovedItems.Count}, Modified={ModifiedList.Count}"); // 새로 추가된 AGV 처리 foreach (var item in AddedItems.ToList()) { AGV? agv = agvPool.GetItem(item.GetString("VHL_NAME")!); if (agv == null) { Debug.LogError($"Failed to create AGV for {item.GetString("VHL_NAME")}"); continue; } agv.Info = new FactoryObjectInfo( item.GetString("VHL_NAME"), item.GetString("VHL_NAME"), "", "", item.GetString("MODE") ); agv.UpdateData(item); } // 제거된 AGV 처리 foreach (var item in RemovedItems.ToList()) { string vhlName = item.GetString("VHL_NAME")!; AGV? agv = agvPool.FindActiveItem(vhlName); if (agv != null) { agvPool.ReturnItem(vhlName); } } // 정보가 수정된 AGV 처리 foreach (var item in ModifiedList.ToList()) { string vhlName = item.GetString("VHL_NAME")!; AGV? agv = agvPool.FindActiveItem(vhlName); if (agv != null) { agv.UpdateData(item); } } if (created == false) { created = true; // 씬이 처음 초기화될 때 AGVManager가 생성되었음을 알립니다. Debug.Log("AGVManager created and initialized."); OnAGVCreated?.Invoke(); } } /// /// AGVManager가 파괴될 때 호출됩니다. /// MQTT 파이프라인에서 'AGV' 핸들러를 제거하여 메모리 누수를 방지합니다. /// protected override void OnDestroy() { base.OnDestroy(); SceneMain.Instance.Initialized -= OnSceneInitialized; DataRepository.Instance.RemoveDataUpdateHandler("AGV", OnUpdateData); agvPool?.ClearRecycledItems(); OnAGVCreated = null; // 이벤트 핸들러 초기화 } } }