diff --git a/Assets/Scripts/UVC/Factory/Component/AGVManager.cs b/Assets/Scripts/UVC/Factory/Component/AGVManager.cs index 9ff80ae5..1e895858 100644 --- a/Assets/Scripts/UVC/Factory/Component/AGVManager.cs +++ b/Assets/Scripts/UVC/Factory/Component/AGVManager.cs @@ -4,9 +4,11 @@ using SampleProject; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using UnityEngine; using UVC.Core; using UVC.Data; +using UVC.Pool; namespace UVC.Factory.Component { @@ -78,8 +80,7 @@ namespace UVC.Factory.Component { private readonly string prefabPath = "Prefabs/SampleProject/Factory/AGV"; - private GameObject? prefab; - private List agvList; + private MonoBehaviourPool? agvPool; /// /// AGVManager의 초기화 메서드입니다. @@ -87,16 +88,16 @@ namespace UVC.Factory.Component /// protected override void Init() { - agvList = new List(); - SceneMain.Instance.Initialized += OnSceneInitialized; + SceneMain.Instance.Initialized += OnSceneInitializedAsync; } /// /// 씬이 완전히 초기화된 후 호출됩니다. /// AGV 데이터를 수신하기 위한 MQTT 파이프라인을 설정합니다. /// - private void OnSceneInitialized() + private async void OnSceneInitializedAsync() { + await InitializePoolAsync(); //데이터를 어떤 형식으로 받을지 정의합니다. var dataMask = new DataMask(); dataMask["VHL_NAME"] = ""; @@ -134,23 +135,31 @@ namespace UVC.Factory.Component } /// - /// 데이터 수신 시 호출되는 공개 핸들러입니다. - /// 비동기 처리 메서드인 OnUpdateDataAsync를 호출합니다. + /// AGV 풀을 비동기적으로 초기화합니다. /// - /// 수신된 데이터 객체 (일반적으로 DataArray 형태) - public void OnUpdateData(IDataObject? data) + private async UniTask InitializePoolAsync() { - OnUpdateDataAsync(data).Forget(); + 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 MonoBehaviourPool(prefab, transform); } /// + /// 데이터 수신 시 호출되는 공개 핸들러입니다. /// 수신된 AGV 데이터 배열을 비동기적으로 처리하여 씬에 반영합니다. /// 추가, 제거, 수정된 AGV 데이터를 각각 구분하여 처리합니다. /// - /// 수신된 데이터 (DataArray) - public async UniTask OnUpdateDataAsync(IDataObject? data) - { - if (data == null) return; + /// 수신된 데이터 객체 (DataArray 형태) + public void OnUpdateData(IDataObject? data) + { + if (data == null || agvPool == null) return; + DataArray? arr = data as DataArray; if (arr == null || arr.Count == 0) return; @@ -159,32 +168,44 @@ namespace UVC.Factory.Component var RemovedItems = arr.RemovedItems; var ModifiedList = arr.ModifiedList; + //Debug.Log($"AGVManager received data: Added={AddedItems.Count}, Removed={RemovedItems.Count}, Modified={ModifiedList.Count}"); + // 새로 추가된 AGV 처리 foreach (var item in AddedItems.ToList()) { - AGV? agv = await CreateAGV(item); - if (agv != null) + AGV? agv = agvPool.GetItem(item.GetString("VHL_NAME")!); + if (agv == null) { - agv.UpdateData(item); - agvList.Add(agv); + Debug.LogError($"Failed to create AGV for {item.GetString("VHL_NAME")}"); + continue; } + agv.Info = new FactoryObjectInfo( + item.GetString("VHL_NAME"), + item.GetString("NODE_ID"), + "", + "", + item.GetString("MODE") + ); + agv.UpdateData(item); } // 제거된 AGV 처리 foreach (var item in RemovedItems.ToList()) { - AGV agv = agvList.Find(x => x.Info != null && x.Info.Name == item.GetString("VHL_NAME")); + string vhlName = item.GetString("VHL_NAME")!; + AGV? agv = agvPool.FindActiveItem(vhlName); if (agv != null) { - Destroy(agv.gameObject); - agvList.Remove(agv); + agvPool.ReturnItem(vhlName); } } // 정보가 수정된 AGV 처리 foreach (var item in ModifiedList.ToList()) { - AGV agv = agvList.Find(x => x.Info != null && x.Info.Name == item.GetString("VHL_NAME")); + Debug.Log($"AGVManager modified data: {item.ToString()}"); + string vhlName = item.GetString("VHL_NAME")!; + AGV? agv = agvPool.FindActiveItem(vhlName); if (agv != null) { agv.UpdateData(item); @@ -193,40 +214,6 @@ namespace UVC.Factory.Component } - /// - /// AGV 프리팹을 사용하여 새로운 AGV 게임 오브젝트를 생성하고 초기화합니다. - /// - /// 신규 AGV의 정보가 담긴 DataObject - /// 생성 및 초기화된 AGV 컴포넌트, 실패 시 null - private async UniTask CreateAGV(DataObject data) - { - if (prefab == null) - { - prefab = await Resources.LoadAsync(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의 고정 정보(Info)를 설정합니다. 이 정보는 FactoryObjectManager 등에서 사용될 수 있습니다. - agv.Info = new FactoryObjectInfo( - data.GetString("VHL_NAME"), - data.GetString("NODE_ID"), - "", - "", - data.GetString("MODE") - ); - return agv; - } - /// /// AGVManager가 파괴될 때 호출됩니다. /// MQTT 파이프라인에서 'AGV' 핸들러를 제거하여 메모리 누수를 방지합니다. @@ -235,6 +222,7 @@ namespace UVC.Factory.Component { base.OnDestroy(); AppMain.Instance.MQTTPipeLine.Remove("AGV"); + agvPool?.ClearRecycledItems(); } } } diff --git a/Assets/Scripts/UVC/Pool.meta b/Assets/Scripts/UVC/Pool.meta new file mode 100644 index 00000000..73fc4f3c --- /dev/null +++ b/Assets/Scripts/UVC/Pool.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0ac765159b11ecc459f67f2f5ddc491f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UVC/Pool/MonoBehaviourPool.cs b/Assets/Scripts/UVC/Pool/MonoBehaviourPool.cs new file mode 100644 index 00000000..507a49bc --- /dev/null +++ b/Assets/Scripts/UVC/Pool/MonoBehaviourPool.cs @@ -0,0 +1,228 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.InputSystem; + +namespace UVC.Pool +{ + /// + /// MonoBehaviour를 상속받는 컴포넌트의 오브젝트 풀링을 관리하는 제네릭 클래스입니다. + /// 오브젝트 풀링은 오브젝트를 반복적으로 생성하고 파괴하는 데 드는 비용을 줄여 성능을 향상시키는 데 사용됩니다. + /// 사용하지 않는 오브젝트를 비활성화하여 보관했다가 필요할 때 재사용합니다. + /// + /// 사용 예: + /// + /// public class BulletManager : MonoBehaviour + /// { + /// public GameObject bulletPrefab; + /// public Transform bulletContainer; + /// private MonoBehaviourPool<Bullet> _bulletPool; + /// + /// void Start() + /// { + /// // 총알 프리팹과 부모 컨테이너를 사용하여 풀을 초기화합니다. + /// _bulletPool = new MonoBehaviourPool<Bullet>(bulletPrefab, bulletContainer); + /// } + /// + /// void SpawnBullet() + /// { + /// // 풀에서 총알을 가져와 사용합니다. + /// Bullet bullet = _bulletPool.GetItem(); + /// bullet.transform.position = transform.position; + /// bullet.Fire(); + /// } + /// + /// public void ReturnBulletToPool(Bullet bullet) + /// { + /// // 사용이 끝난 총알을 풀에 반환합니다. + /// _bulletPool.ReturnItem(bullet); + /// } + /// } + /// + /// + /// 풀링할 MonoBehaviour 타입입니다. + public class MonoBehaviourPool where T : MonoBehaviour + { + /// + /// Resources 폴더에 있는 프리팹의 경로입니다. + /// + protected string _prefabsPath = ""; + /// + /// 새로운 오브젝트를 생성할 때 사용할 원본 프리팹입니다. + /// + protected GameObject _originalPrefab; + + /// + /// 활성화된(사용 중인) 오브젝트들을 담아둘 부모 Transform입니다. + /// + protected Transform _activeItemContainer; + + /// + /// 재활용(비활성화된) 오브젝트들을 담아둘 부모 Transform입니다. + /// + protected Transform? _recycledItemContainer; + + /// + /// 현재 활성화되어 사용 중인 아이템을 저장하는 딕셔너리입니다. (Key: 아이템 고유 키, Value: 아이템 인스턴스) + /// + protected Dictionary _activeItems = new Dictionary(); + + /// + /// 재활용을 위해 비활성화된 아이템 리스트입니다. + /// + protected List _recycledItems = new List(); + + /// + /// 현재 활성화된 아이템들의 딕셔너리를 읽기 전용으로 가져옵니다. + /// + public IReadOnlyDictionary ActiveItems => _activeItems; + + /// + /// 활성화된 아이템들의 부모 컨테이너를 가져옵니다. + /// + public Transform ActiveItemContainer { get { return _activeItemContainer; } } + + /// + /// 재활용 아이템들의 부모 컨테이너를 가져옵니다. + /// + public Transform? RecycledItemContainer { get { return _recycledItemContainer; } } + + /// + /// GameObject 프리팹을 사용하여 풀을 초기화합니다. + /// + /// 풀링할 오브젝트의 원본 프리팹입니다. + /// 활성화된 오브젝트가 위치할 부모 Transform입니다. + /// 비활성화된 오브젝트가 위치할 부모 Transform입니다. 지정하지 않으면 activeItemContainer가 사용됩니다. + public MonoBehaviourPool(GameObject originalPrefab, Transform activeItemContainer, Transform recycledItemContainer = null) + { + _originalPrefab = originalPrefab; + _activeItemContainer = activeItemContainer; + _recycledItemContainer = recycledItemContainer; + + if (recycledItemContainer == null) _recycledItemContainer = _activeItemContainer; + } + + /// + /// Resources 폴더 내의 프리팹 경로를 사용하여 풀을 초기화합니다. + /// + /// Resources 폴더 기준의 프리팹 경로입니다. + /// 활성화된 오브젝트가 위치할 부모 Transform입니다. + /// 비활성화된 오브젝트가 위치할 부모 Transform입니다. 지정하지 않으면 activeItemContainer가 사용됩니다. + public MonoBehaviourPool(string prefabsPath, Transform activeItemContainer, Transform recycledItemContainer) + { + _prefabsPath = prefabsPath; + _originalPrefab = Resources.Load(prefabsPath); + _activeItemContainer = activeItemContainer; + _recycledItemContainer = recycledItemContainer; + if (recycledItemContainer == null) _recycledItemContainer = _activeItemContainer; + } + + /// + /// 풀에서 아이템을 가져와 지정된 키와 연결합니다. + /// + /// 아이템을 식별할 고유 키입니다. + /// true일 경우, 아이템의 부모를 자동으로 ActiveItemContainer로 설정합니다. + /// autoSetParent가 true일 때, 아이템의 부모를 지정된 Transform으로 설정합니다. + /// 활성화된 아이템의 T 컴포넌트입니다. 키가 이미 존재하면 null을 반환합니다. + public T? GetItem(string key, bool autoSetParent = true, Transform? parent = null) + { + if (_activeItems.ContainsKey(key)) + { + return _activeItems[key]; + } + + T? item = null; + if (_recycledItems.Count > 0) + { + item = _recycledItems[0]; + _recycledItems.RemoveAt(0); + } + else + { + GameObject go = UnityEngine.Object.Instantiate(_originalPrefab); + item = go.GetComponent(); + } + + item.gameObject.SetActive(true); + if (autoSetParent) + { + if (parent != null) + { + item.transform.SetParent(parent, false); + } + else if (_activeItemContainer != null) + { + item.transform.SetParent(_activeItemContainer, false); + } + } + + _activeItems.Add(key, item); + return item; + } + + /// + /// 키를 사용하여 활성화된 아이템을 풀에 반환합니다. + /// + /// 반환할 아이템의 고유 키입니다. + /// true일 경우, 아이템의 부모를 RecycledItemContainer로 설정합니다. + public void ReturnItem(string key, bool clearParent = false) + { + if (!_activeItems.TryGetValue(key, out T item)) + { + return; // 키에 해당하는 아이템이 없으면 반환 + } + + _activeItems.Remove(key); + _recycledItems.Add(item); + + if (clearParent) + { + if (item.gameObject.activeInHierarchy && _recycledItemContainer != null) + { + item.transform.SetParent(_recycledItemContainer, false); + } + } + item.gameObject.SetActive(false); + } + + /// + /// 키를 사용하여 활성화된 아이템을 찾습니다. + /// + /// 찾을 아이템의 고유 키입니다. + /// 키에 해당하는 아이템. 없으면 null을 반환합니다. + public T? FindActiveItem(string key) + { + _activeItems.TryGetValue(key, out T item); + return item; + } + + /// + /// 현재 활성화된 모든 아이템을 풀에 반환합니다. + /// + public void ReturnAll() + { + // ToList()를 사용하여 키 컬렉션의 복사본을 만들어 순회 중 변경 문제를 방지합니다. + foreach (var key in _activeItems.Keys.ToList()) + { + ReturnItem(key); + } + } + + /// + /// 재활용 중인 모든 아이템을 파괴하여 메모리에서 완전히 제거합니다. + /// + public void ClearRecycledItems() + { + if (_recycledItems == null) + return; + + foreach (var item in _recycledItems) + { + if (item != null) + UnityEngine.Object.Destroy(item.gameObject); + } + _recycledItems.Clear(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/Pool/MonoBehaviourPool.cs.meta b/Assets/Scripts/UVC/Pool/MonoBehaviourPool.cs.meta new file mode 100644 index 00000000..e6061515 --- /dev/null +++ b/Assets/Scripts/UVC/Pool/MonoBehaviourPool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e9be41afc37616649924d7190be6215a \ No newline at end of file diff --git a/Assets/Scripts/UVC/Pool/SmallList.cs b/Assets/Scripts/UVC/Pool/SmallList.cs new file mode 100644 index 00000000..fd09b893 --- /dev/null +++ b/Assets/Scripts/UVC/Pool/SmallList.cs @@ -0,0 +1,224 @@ +using UnityEngine; + +namespace UVC.Pool +{ + /// + /// List와 유사하게 작동하는 경량 배열 구현입니다. + /// 필요할 때 자동으로 메모리를 할당하지만, 가비지 컬렉션을 위해 메모리를 해제하지는 않습니다. + /// 이는 특히 게임 개발과 같이 성능이 중요한 상황에서 가비지 컬렉션으로 인한 멈춤 현상을 방지하는 데 유용합니다. + /// + /// 사용 예: + /// + /// SmallList<int> numbers = new SmallList<int>(); + /// numbers.Add(10); + /// numbers.Add(20); + /// int firstNumber = numbers[0]; // 10 + /// + /// + /// 리스트에 저장할 요소의 타입입니다. + public class SmallList + { + /// + /// 리스트 데이터를 저장하는 내부 배열입니다. + /// + public T[] data; + + /// + /// 리스트에 있는 요소의 수입니다. + /// + public int Count = 0; + + /// + /// 인덱스를 통해 리스트 항목에 접근합니다. + /// + /// 접근할 요소의 인덱스입니다. + /// 지정된 인덱스의 요소입니다. + public T this[int i] + { + get { return data[i]; } + set { data[i] = value; } + } + + /// + /// 더 많은 메모리가 필요할 때 배열의 크기를 조정합니다. + /// + private void ResizeArray() + { + T[] newData; + + if (data != null) + newData = new T[Mathf.Max(data.Length << 1, 64)]; + else + newData = new T[64]; + + if (data != null && Count > 0) + data.CopyTo(newData, 0); + + data = newData; + } + + /// + /// 가비지 컬렉션에 메모리를 해제하는 대신 리스트 크기를 0으로 재설정하여 모든 요소를 지웁니다. + /// + public void Clear() + { + Count = 0; + } + + /// + /// 리스트의 첫 번째 요소를 반환합니다. + /// + /// 리스트의 첫 번째 요소입니다. 리스트가 비어 있으면 default(T)를 반환합니다. + public T First() + { + if (data == null || Count == 0) return default(T); + return data[0]; + } + + /// + /// 리스트의 마지막 요소를 반환합니다. + /// + /// 리스트의 마지막 요소입니다. 리스트가 비어 있으면 default(T)를 반환합니다. + public T Last() + { + if (data == null || Count == 0) return default(T); + return data[Count - 1]; + } + + /// + /// 배열 끝에 새 요소를 추가하고 필요한 경우 메모리를 더 할당합니다. + /// + /// 추가할 요소입니다. + public void Add(T item) + { + if (data == null || Count == data.Length) + ResizeArray(); + + data[Count] = item; + Count++; + } + + /// + /// 배열 시작 부분에 새 요소를 추가하고 필요한 경우 메모리를 더 할당합니다. + /// + /// 추가할 요소입니다. + public void AddStart(T item) + { + Insert(item, 0); + } + + /// + /// 지정된 인덱스에 새 요소를 삽입하고 필요한 경우 메모리를 더 할당합니다. + /// + /// 삽입할 요소입니다. + /// 요소를 삽입할 인덱스입니다. + public void Insert(T item, int index) + { + if (data == null || Count == data.Length) + ResizeArray(); + + for (var i = Count; i > index; i--) + { + data[i] = data[i - 1]; + } + + data[index] = item; + Count++; + } + + /// + /// 데이터 시작 부분에서 항목을 제거합니다. + /// + /// 제거된 요소입니다. + public T RemoveStart() + { + return RemoveAt(0); + } + + /// + /// 지정된 인덱스에서 항목을 제거합니다. + /// + /// 제거할 요소의 인덱스입니다. + /// 제거된 요소입니다. + public T RemoveAt(int index) + { + if (data != null && Count != 0) + { + T val = data[index]; + + for (var i = index; i < Count - 1; i++) + { + data[i] = data[i + 1]; + } + + Count--; + data[Count] = default(T); + return val; + } + else + { + return default(T); + } + } + + /// + /// 데이터에서 특정 항목을 제거합니다. + /// + /// 제거할 요소입니다. + /// 제거된 요소입니다. 항목을 찾지 못하면 default(T)를 반환합니다. + public T Remove(T item) + { + if (data != null && Count != 0) + { + for (var i = 0; i < Count; i++) + { + if (data[i].Equals(item)) + { + return RemoveAt(i); + } + } + } + + return default(T); + } + + /// + /// 데이터 끝에서 항목을 제거합니다. + /// + /// 제거된 요소입니다. + public T RemoveEnd() + { + if (data != null && Count != 0) + { + Count--; + T val = data[Count]; + data[Count] = default(T); + + return val; + } + else + { + return default(T); + } + } + + /// + /// 데이터에 특정 항목이 포함되어 있는지 확인합니다. + /// + /// 비교할 항목입니다. + /// 항목이 데이터에 있으면 true, 그렇지 않으면 false를 반환합니다. + public bool Contains(T item) + { + if (data == null) + return false; + + for (var i = 0; i < Count; i++) + { + if (data[i].Equals(item)) + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/UVC/Pool/SmallList.cs.meta b/Assets/Scripts/UVC/Pool/SmallList.cs.meta new file mode 100644 index 00000000..97cd5ff2 --- /dev/null +++ b/Assets/Scripts/UVC/Pool/SmallList.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 641f319d4eeb0ad408f5f79e67363328 \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/Info/InfoWindow.cs b/Assets/Scripts/UVC/UI/Info/InfoWindow.cs index 1bcda0bf..63907495 100644 --- a/Assets/Scripts/UVC/UI/Info/InfoWindow.cs +++ b/Assets/Scripts/UVC/UI/Info/InfoWindow.cs @@ -154,7 +154,7 @@ namespace UVC.UI.Info foreach (var kvp in information) { // 태그를 사용하여 줄바꿈 시에도 정렬이 유지되도록 합니다. - combinedString += $"{kvp.Key}:{kvp.Value ?? "null"}\n"; + combinedString += $"{kvp.Key}{kvp.Value ?? "null"}\n"; } combinedString = combinedString.TrimEnd('\n'); // 마지막 줄바꿈 제거 text.text = combinedString;