AGVManager에 pool적용. MQTTPipeLineInfo, updatedDataOnly=true 일때 값 검증해야 함

This commit is contained in:
logonkhi
2025-06-25 19:44:34 +09:00
parent 784238efbf
commit aa9caab761
7 changed files with 509 additions and 57 deletions

View File

@@ -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<AGV> agvList;
private MonoBehaviourPool<AGV>? agvPool;
/// <summary>
/// AGVManager의 초기화 메서드입니다.
@@ -87,16 +88,16 @@ namespace UVC.Factory.Component
/// </summary>
protected override void Init()
{
agvList = new List<AGV>();
SceneMain.Instance.Initialized += OnSceneInitialized;
SceneMain.Instance.Initialized += OnSceneInitializedAsync;
}
/// <summary>
/// 씬이 완전히 초기화된 후 호출됩니다.
/// AGV 데이터를 수신하기 위한 MQTT 파이프라인을 설정합니다.
/// </summary>
private void OnSceneInitialized()
private async void OnSceneInitializedAsync()
{
await InitializePoolAsync();
//데이터를 어떤 형식으로 받을지 정의합니다.
var dataMask = new DataMask();
dataMask["VHL_NAME"] = "";
@@ -134,23 +135,31 @@ namespace UVC.Factory.Component
}
/// <summary>
/// 데이터 수신 시 호출되는 공개 핸들러입니다.
/// 비동기 처리 메서드인 OnUpdateDataAsync를 호출합니다.
/// AGV 풀을 비동기적으로 초기화합니다.
/// </summary>
/// <param name="data">수신된 데이터 객체 (일반적으로 DataArray 형태)</param>
public void OnUpdateData(IDataObject? data)
private async UniTask InitializePoolAsync()
{
OnUpdateDataAsync(data).Forget();
if (agvPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(prefabPath) as GameObject;
if (prefab == null)
{
Debug.LogError($"Prefab not found at path: {prefabPath}");
return;
}
agvPool = new MonoBehaviourPool<AGV>(prefab, transform);
}
/// <summary>
/// 데이터 수신 시 호출되는 공개 핸들러입니다.
/// 수신된 AGV 데이터 배열을 비동기적으로 처리하여 씬에 반영합니다.
/// 추가, 제거, 수정된 AGV 데이터를 각각 구분하여 처리합니다.
/// </summary>
/// <param name="data">수신된 데이터 (DataArray)</param>
public async UniTask OnUpdateDataAsync(IDataObject? data)
/// <param name="data">수신된 데이터 객체 (DataArray 형태)</param>
public void OnUpdateData(IDataObject? data)
{
if (data == null) return;
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
}
/// <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' 핸들러를 제거하여 메모리 누수를 방지합니다.
@@ -235,6 +222,7 @@ namespace UVC.Factory.Component
{
base.OnDestroy();
AppMain.Instance.MQTTPipeLine.Remove("AGV");
agvPool?.ClearRecycledItems();
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0ac765159b11ecc459f67f2f5ddc491f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,228 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
namespace UVC.Pool
{
/// <summary>
/// MonoBehaviour를 상속받는 컴포넌트의 오브젝트 풀링을 관리하는 제네릭 클래스입니다.
/// 오브젝트 풀링은 오브젝트를 반복적으로 생성하고 파괴하는 데 드는 비용을 줄여 성능을 향상시키는 데 사용됩니다.
/// 사용하지 않는 오브젝트를 비활성화하여 보관했다가 필요할 때 재사용합니다.
///
/// 사용 예:
/// <code>
/// public class BulletManager : MonoBehaviour
/// {
/// public GameObject bulletPrefab;
/// public Transform bulletContainer;
/// private MonoBehaviourPool&lt;Bullet&gt; _bulletPool;
///
/// void Start()
/// {
/// // 총알 프리팹과 부모 컨테이너를 사용하여 풀을 초기화합니다.
/// _bulletPool = new MonoBehaviourPool&lt;Bullet&gt;(bulletPrefab, bulletContainer);
/// }
///
/// void SpawnBullet()
/// {
/// // 풀에서 총알을 가져와 사용합니다.
/// Bullet bullet = _bulletPool.GetItem();
/// bullet.transform.position = transform.position;
/// bullet.Fire();
/// }
///
/// public void ReturnBulletToPool(Bullet bullet)
/// {
/// // 사용이 끝난 총알을 풀에 반환합니다.
/// _bulletPool.ReturnItem(bullet);
/// }
/// }
/// </code>
/// </summary>
/// <typeparam name="T">풀링할 MonoBehaviour 타입입니다.</typeparam>
public class MonoBehaviourPool<T> where T : MonoBehaviour
{
/// <summary>
/// Resources 폴더에 있는 프리팹의 경로입니다.
/// </summary>
protected string _prefabsPath = "";
/// <summary>
/// 새로운 오브젝트를 생성할 때 사용할 원본 프리팹입니다.
/// </summary>
protected GameObject _originalPrefab;
/// <summary>
/// 활성화된(사용 중인) 오브젝트들을 담아둘 부모 Transform입니다.
/// </summary>
protected Transform _activeItemContainer;
/// <summary>
/// 재활용(비활성화된) 오브젝트들을 담아둘 부모 Transform입니다.
/// </summary>
protected Transform? _recycledItemContainer;
/// <summary>
/// 현재 활성화되어 사용 중인 아이템을 저장하는 딕셔너리입니다. (Key: 아이템 고유 키, Value: 아이템 인스턴스)
/// </summary>
protected Dictionary<string, T> _activeItems = new Dictionary<string, T>();
/// <summary>
/// 재활용을 위해 비활성화된 아이템 리스트입니다.
/// </summary>
protected List<T> _recycledItems = new List<T>();
/// <summary>
/// 현재 활성화된 아이템들의 딕셔너리를 읽기 전용으로 가져옵니다.
/// </summary>
public IReadOnlyDictionary<string, T> ActiveItems => _activeItems;
/// <summary>
/// 활성화된 아이템들의 부모 컨테이너를 가져옵니다.
/// </summary>
public Transform ActiveItemContainer { get { return _activeItemContainer; } }
/// <summary>
/// 재활용 아이템들의 부모 컨테이너를 가져옵니다.
/// </summary>
public Transform? RecycledItemContainer { get { return _recycledItemContainer; } }
/// <summary>
/// GameObject 프리팹을 사용하여 풀을 초기화합니다.
/// </summary>
/// <param name="originalPrefab">풀링할 오브젝트의 원본 프리팹입니다.</param>
/// <param name="activeItemContainer">활성화된 오브젝트가 위치할 부모 Transform입니다.</param>
/// <param name="recycledItemContainer">비활성화된 오브젝트가 위치할 부모 Transform입니다. 지정하지 않으면 activeItemContainer가 사용됩니다.</param>
public MonoBehaviourPool(GameObject originalPrefab, Transform activeItemContainer, Transform recycledItemContainer = null)
{
_originalPrefab = originalPrefab;
_activeItemContainer = activeItemContainer;
_recycledItemContainer = recycledItemContainer;
if (recycledItemContainer == null) _recycledItemContainer = _activeItemContainer;
}
/// <summary>
/// Resources 폴더 내의 프리팹 경로를 사용하여 풀을 초기화합니다.
/// </summary>
/// <param name="prefabsPath">Resources 폴더 기준의 프리팹 경로입니다.</param>
/// <param name="activeItemContainer">활성화된 오브젝트가 위치할 부모 Transform입니다.</param>
/// <param name="recycledItemContainer">비활성화된 오브젝트가 위치할 부모 Transform입니다. 지정하지 않으면 activeItemContainer가 사용됩니다.</param>
public MonoBehaviourPool(string prefabsPath, Transform activeItemContainer, Transform recycledItemContainer)
{
_prefabsPath = prefabsPath;
_originalPrefab = Resources.Load<GameObject>(prefabsPath);
_activeItemContainer = activeItemContainer;
_recycledItemContainer = recycledItemContainer;
if (recycledItemContainer == null) _recycledItemContainer = _activeItemContainer;
}
/// <summary>
/// 풀에서 아이템을 가져와 지정된 키와 연결합니다.
/// </summary>
/// <param name="key">아이템을 식별할 고유 키입니다.</param>
/// <param name="autoSetParent">true일 경우, 아이템의 부모를 자동으로 ActiveItemContainer로 설정합니다.</param>
/// <param name="parent">autoSetParent가 true일 때, 아이템의 부모를 지정된 Transform으로 설정합니다.</param>
/// <returns>활성화된 아이템의 T 컴포넌트입니다. 키가 이미 존재하면 null을 반환합니다.</returns>
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<T>();
}
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;
}
/// <summary>
/// 키를 사용하여 활성화된 아이템을 풀에 반환합니다.
/// </summary>
/// <param name="key">반환할 아이템의 고유 키입니다.</param>
/// <param name="clearParent">true일 경우, 아이템의 부모를 RecycledItemContainer로 설정합니다.</param>
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);
}
/// <summary>
/// 키를 사용하여 활성화된 아이템을 찾습니다.
/// </summary>
/// <param name="key">찾을 아이템의 고유 키입니다.</param>
/// <returns>키에 해당하는 아이템. 없으면 null을 반환합니다.</returns>
public T? FindActiveItem(string key)
{
_activeItems.TryGetValue(key, out T item);
return item;
}
/// <summary>
/// 현재 활성화된 모든 아이템을 풀에 반환합니다.
/// </summary>
public void ReturnAll()
{
// ToList()를 사용하여 키 컬렉션의 복사본을 만들어 순회 중 변경 문제를 방지합니다.
foreach (var key in _activeItems.Keys.ToList())
{
ReturnItem(key);
}
}
/// <summary>
/// 재활용 중인 모든 아이템을 파괴하여 메모리에서 완전히 제거합니다.
/// </summary>
public void ClearRecycledItems()
{
if (_recycledItems == null)
return;
foreach (var item in _recycledItems)
{
if (item != null)
UnityEngine.Object.Destroy(item.gameObject);
}
_recycledItems.Clear();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e9be41afc37616649924d7190be6215a

View File

@@ -0,0 +1,224 @@
using UnityEngine;
namespace UVC.Pool
{
/// <summary>
/// List와 유사하게 작동하는 경량 배열 구현입니다.
/// 필요할 때 자동으로 메모리를 할당하지만, 가비지 컬렉션을 위해 메모리를 해제하지는 않습니다.
/// 이는 특히 게임 개발과 같이 성능이 중요한 상황에서 가비지 컬렉션으로 인한 멈춤 현상을 방지하는 데 유용합니다.
///
/// 사용 예:
/// <code>
/// SmallList&lt;int&gt; numbers = new SmallList&lt;int&gt;();
/// numbers.Add(10);
/// numbers.Add(20);
/// int firstNumber = numbers[0]; // 10
/// </code>
/// </summary>
/// <typeparam name="T">리스트에 저장할 요소의 타입입니다.</typeparam>
public class SmallList<T>
{
/// <summary>
/// 리스트 데이터를 저장하는 내부 배열입니다.
/// </summary>
public T[] data;
/// <summary>
/// 리스트에 있는 요소의 수입니다.
/// </summary>
public int Count = 0;
/// <summary>
/// 인덱스를 통해 리스트 항목에 접근합니다.
/// </summary>
/// <param name="i">접근할 요소의 인덱스입니다.</param>
/// <returns>지정된 인덱스의 요소입니다.</returns>
public T this[int i]
{
get { return data[i]; }
set { data[i] = value; }
}
/// <summary>
/// 더 많은 메모리가 필요할 때 배열의 크기를 조정합니다.
/// </summary>
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;
}
/// <summary>
/// 가비지 컬렉션에 메모리를 해제하는 대신 리스트 크기를 0으로 재설정하여 모든 요소를 지웁니다.
/// </summary>
public void Clear()
{
Count = 0;
}
/// <summary>
/// 리스트의 첫 번째 요소를 반환합니다.
/// </summary>
/// <returns>리스트의 첫 번째 요소입니다. 리스트가 비어 있으면 default(T)를 반환합니다.</returns>
public T First()
{
if (data == null || Count == 0) return default(T);
return data[0];
}
/// <summary>
/// 리스트의 마지막 요소를 반환합니다.
/// </summary>
/// <returns>리스트의 마지막 요소입니다. 리스트가 비어 있으면 default(T)를 반환합니다.</returns>
public T Last()
{
if (data == null || Count == 0) return default(T);
return data[Count - 1];
}
/// <summary>
/// 배열 끝에 새 요소를 추가하고 필요한 경우 메모리를 더 할당합니다.
/// </summary>
/// <param name="item">추가할 요소입니다.</param>
public void Add(T item)
{
if (data == null || Count == data.Length)
ResizeArray();
data[Count] = item;
Count++;
}
/// <summary>
/// 배열 시작 부분에 새 요소를 추가하고 필요한 경우 메모리를 더 할당합니다.
/// </summary>
/// <param name="item">추가할 요소입니다.</param>
public void AddStart(T item)
{
Insert(item, 0);
}
/// <summary>
/// 지정된 인덱스에 새 요소를 삽입하고 필요한 경우 메모리를 더 할당합니다.
/// </summary>
/// <param name="item">삽입할 요소입니다.</param>
/// <param name="index">요소를 삽입할 인덱스입니다.</param>
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++;
}
/// <summary>
/// 데이터 시작 부분에서 항목을 제거합니다.
/// </summary>
/// <returns>제거된 요소입니다.</returns>
public T RemoveStart()
{
return RemoveAt(0);
}
/// <summary>
/// 지정된 인덱스에서 항목을 제거합니다.
/// </summary>
/// <param name="index">제거할 요소의 인덱스입니다.</param>
/// <returns>제거된 요소입니다.</returns>
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);
}
}
/// <summary>
/// 데이터에서 특정 항목을 제거합니다.
/// </summary>
/// <param name="item">제거할 요소입니다.</param>
/// <returns>제거된 요소입니다. 항목을 찾지 못하면 default(T)를 반환합니다.</returns>
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);
}
/// <summary>
/// 데이터 끝에서 항목을 제거합니다.
/// </summary>
/// <returns>제거된 요소입니다.</returns>
public T RemoveEnd()
{
if (data != null && Count != 0)
{
Count--;
T val = data[Count];
data[Count] = default(T);
return val;
}
else
{
return default(T);
}
}
/// <summary>
/// 데이터에 특정 항목이 포함되어 있는지 확인합니다.
/// </summary>
/// <param name="item">비교할 항목입니다.</param>
/// <returns>항목이 데이터에 있으면 true, 그렇지 않으면 false를 반환합니다.</returns>
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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 641f319d4eeb0ad408f5f79e67363328

View File

@@ -154,7 +154,7 @@ namespace UVC.UI.Info
foreach (var kvp in information)
{
// <indent> 태그를 사용하여 줄바꿈 시에도 정렬이 유지되도록 합니다.
combinedString += $"{kvp.Key}:<pos=40%><indent=40%>{kvp.Value ?? "null"}</indent>\n";
combinedString += $"{kvp.Key}<pos=40%><indent=40%>{kvp.Value ?? "null"}</indent>\n";
}
combinedString = combinedString.TrimEnd('\n'); // 마지막 줄바꿈 제거
text.text = combinedString;