This commit is contained in:
2026-03-17 10:52:33 +09:00
parent 28cd9b1582
commit 43772daa55
16 changed files with 358 additions and 448 deletions

View File

@@ -44,12 +44,8 @@ namespace Simulator.Data.Transport
protected override void Init() protected override void Init()
{ {
InitializeNodePoolAsync().ContinueWith(() => InitializePoolAsync<AGVNode>(nodePool, "node").ContinueWith(pool => { nodePool = pool; });
{ InitializePoolAsync<AGVPath>(pathPool, "path").ContinueWith(pool => { pathPool = pool; });
});
InitializePathPoolAsync().ContinueWith(() =>
{
});
} }
public Vector3 GetNodePosition(string name) public Vector3 GetNodePosition(string name)
@@ -81,29 +77,16 @@ namespace Simulator.Data.Transport
} }
} }
private async UniTask InitializeNodePoolAsync() private async UniTask<GameObjectPool<T>> InitializePoolAsync<T>(GameObjectPool<T> existingPool, string key) where T : MonoBehaviour
{ {
if (nodePool != null) return; if (existingPool != null) return existingPool;
var prefab = await Resources.LoadAsync<GameObject>(prefabPaths[key]) as GameObject;
var prefab = await Resources.LoadAsync<GameObject>(prefabPaths["node"]) as GameObject;
if (prefab == null) if (prefab == null)
{ {
Debug.LogError($"Prefab not found at path: {prefabPaths["node"]}"); Debug.LogError($"Prefab not found at path: {prefabPaths[key]}");
return; return null;
} }
nodePool = new GameObjectPool<AGVNode>(prefab, transform); return new GameObjectPool<T>(prefab, transform);
}
private async UniTask InitializePathPoolAsync()
{
if (pathPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(prefabPaths["path"]) as GameObject;
if (prefab == null)
{
Debug.LogError($"Prefab not found at path: {prefabPaths["path"]}");
return;
}
pathPool = new GameObjectPool<AGVPath>(prefab, transform);
} }
} }
} }

View File

@@ -39,12 +39,13 @@ namespace Simulator.Data.Transport
public void InitializeAllPools() public void InitializeAllPools()
{ {
InitializeAGVPoolAsync().ContinueWith(() => InitializePoolAsync<AGV>(agvPool, "agv").ContinueWith(pool =>
{ {
agvPool = pool;
InitializeDataMappers(); InitializeDataMappers();
}); });
InitializeAFLPoolAsync().ContinueWith(() => { }); InitializePoolAsync<AGV>(aflPool, "afl").ContinueWith(pool => { aflPool = pool; });
InitializeForkLiftPoolAsync().ContinueWith(() => { }); InitializePoolAsync<AGV>(forkLiftPool, "forkLift").ContinueWith(pool => { forkLiftPool = pool; });
} }
private void InitializeDataMappers() private void InitializeDataMappers()
@@ -135,43 +136,16 @@ namespace Simulator.Data.Transport
return agvMap.TryGetValue(name, out var agv) ? agv : null; return agvMap.TryGetValue(name, out var agv) ? agv : null;
} }
private async UniTask InitializeAGVPoolAsync() private async UniTask<GameObjectPool<T>> InitializePoolAsync<T>(GameObjectPool<T> existingPool, string key) where T : MonoBehaviour
{ {
if (agvPool != null) return; if (existingPool != null) return existingPool;
var prefab = await Resources.LoadAsync<GameObject>(prefabPaths[key]) as GameObject;
var prefab = await Resources.LoadAsync<GameObject>(prefabPaths["agv"]) as GameObject;
if (prefab == null) if (prefab == null)
{ {
Debug.LogError($"Prefab not found at path: {prefabPaths["agv"]}"); Debug.LogError($"Prefab not found at path: {prefabPaths[key]}");
return; return null;
} }
agvPool = new GameObjectPool<AGV>(prefab, _parent); return new GameObjectPool<T>(prefab, _parent);
}
private async UniTask InitializeAFLPoolAsync()
{
if (aflPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(prefabPaths["afl"]) as GameObject;
if (prefab == null)
{
Debug.LogError($"Prefab not found at path: {prefabPaths["afl"]}");
return;
}
aflPool = new GameObjectPool<AGV>(prefab, _parent);
}
private async UniTask InitializeForkLiftPoolAsync()
{
if (forkLiftPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(prefabPaths["forkLift"]) as GameObject;
if (prefab == null)
{
Debug.LogError($"Prefab not found at path: {prefabPaths["forkLift"]}");
return;
}
forkLiftPool = new GameObjectPool<AGV>(prefab, _parent);
} }
} }
} }

View File

@@ -55,14 +55,14 @@ namespace Simulator.Data
public void InitializeAllPools() public void InitializeAllPools()
{ {
InitializeSourcePoolAsync().ContinueWith(() => _dataMapperDict.Add(ComponentType.Source, CreateDataMapper(ComponentType.Source))); InitializePoolAsync<SourceComponent>(_sourcePool, "source").ContinueWith(pool => { _sourcePool = pool; _dataMapperDict.Add(ComponentType.Source, CreateDataMapper(ComponentType.Source)); });
InitializeSinkPoolAsync().ContinueWith(() => _dataMapperDict.Add(ComponentType.Sink, CreateDataMapper(ComponentType.Sink))); InitializePoolAsync<SinkComponent>(_sinkPool, "sink").ContinueWith(pool => { _sinkPool = pool; _dataMapperDict.Add(ComponentType.Sink, CreateDataMapper(ComponentType.Sink)); });
InitializeQueuePoolAsync().ContinueWith(() => _dataMapperDict.Add(ComponentType.Queue, null)); InitializePoolAsync<QueueComponent>(_queuePool, "queue").ContinueWith(pool => { _queuePool = pool; _dataMapperDict.Add(ComponentType.Queue, null); });
InitializeRackPoolAsync().ContinueWith(() => _dataMapperDict.Add(ComponentType.Rack, CreateDataMapper(ComponentType.Rack))); InitializePoolAsync<RackComponent>(_rackPool, "rack").ContinueWith(pool => { _rackPool = pool; _dataMapperDict.Add(ComponentType.Rack, CreateDataMapper(ComponentType.Rack)); });
InitializeAsrsPoolAsync().ContinueWith(() => _dataMapperDict.Add(ComponentType.ASRS, CreateDataMapper(ComponentType.ASRS))); InitializePoolAsync<ASRSComponent>(_asrsPool, "asrs").ContinueWith(pool => { _asrsPool = pool; _dataMapperDict.Add(ComponentType.ASRS, CreateDataMapper(ComponentType.ASRS)); });
InitializeRobotArmPoolAsync().ContinueWith(() => _dataMapperDict.Add(ComponentType.RobotArm, CreateDataMapper(ComponentType.RobotArm))); InitializePoolAsync<RobotArmComponent>(_robotArmPool, "robotArm").ContinueWith(pool => { _robotArmPool = pool; _dataMapperDict.Add(ComponentType.RobotArm, CreateDataMapper(ComponentType.RobotArm)); });
InitializeProcessorPoolAsync().ContinueWith(() => _dataMapperDict.Add(ComponentType.Processor, CreateDataMapper(ComponentType.Processor))); InitializePoolAsync<ProcessorComponent>(_processorPool, "processor").ContinueWith(pool => { _processorPool = pool; _dataMapperDict.Add(ComponentType.Processor, CreateDataMapper(ComponentType.Processor)); });
InitializeWorkerPoolAsync().ContinueWith(() => _dataMapperDict.Add(ComponentType.Worker, CreateDataMapper(ComponentType.Worker))); InitializePoolAsync<WorkerComponent>(_workerPool, "worker").ContinueWith(pool => { _workerPool = pool; _dataMapperDict.Add(ComponentType.Worker, CreateDataMapper(ComponentType.Worker)); });
} }
private DataMapper CreateDataMapper(ComponentType type) private DataMapper CreateDataMapper(ComponentType type)
@@ -113,68 +113,12 @@ namespace Simulator.Data
#region Pool Initialization #region Pool Initialization
private async UniTask InitializeSourcePoolAsync() private async UniTask<GameObjectPool<T>> InitializePoolAsync<T>(GameObjectPool<T> existingPool, string key) where T : MonoBehaviour
{ {
if (_sourcePool != null) return; if (existingPool != null) return existingPool;
var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths["source"]) as GameObject; var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths[key]) as GameObject;
if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths["source"]}"); return; } if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths[key]}"); return null; }
_sourcePool = new GameObjectPool<SourceComponent>(prefab, _parentTransform); return new GameObjectPool<T>(prefab, _parentTransform);
}
private async UniTask InitializeSinkPoolAsync()
{
if (_sinkPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths["sink"]) as GameObject;
if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths["sink"]}"); return; }
_sinkPool = new GameObjectPool<SinkComponent>(prefab, _parentTransform);
}
private async UniTask InitializeQueuePoolAsync()
{
if (_queuePool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths["queue"]) as GameObject;
if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths["queue"]}"); return; }
_queuePool = new GameObjectPool<QueueComponent>(prefab, _parentTransform);
}
private async UniTask InitializeRackPoolAsync()
{
if (_rackPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths["rack"]) as GameObject;
if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths["rack"]}"); return; }
_rackPool = new GameObjectPool<RackComponent>(prefab, _parentTransform);
}
private async UniTask InitializeAsrsPoolAsync()
{
if (_asrsPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths["asrs"]) as GameObject;
if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths["asrs"]}"); return; }
_asrsPool = new GameObjectPool<ASRSComponent>(prefab, _parentTransform);
}
private async UniTask InitializeRobotArmPoolAsync()
{
if (_robotArmPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths["robotArm"]) as GameObject;
if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths["robotArm"]}"); return; }
_robotArmPool = new GameObjectPool<RobotArmComponent>(prefab, _parentTransform);
}
private async UniTask InitializeProcessorPoolAsync()
{
if (_processorPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths["processor"]) as GameObject;
if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths["processor"]}"); return; }
_processorPool = new GameObjectPool<ProcessorComponent>(prefab, _parentTransform);
}
private async UniTask InitializeWorkerPoolAsync()
{
if (_workerPool != null) return;
var prefab = await Resources.LoadAsync<GameObject>(_prefabPaths["worker"]) as GameObject;
if (prefab == null) { Debug.LogError($"Prefab not found at path: {_prefabPaths["worker"]}"); return; }
_workerPool = new GameObjectPool<WorkerComponent>(prefab, _parentTransform);
} }
#endregion #endregion

View File

@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace Simulator.Data namespace Simulator.Data
@@ -6,127 +8,64 @@ namespace Simulator.Data
{ {
private ProgressPopupView _progressPopupView; private ProgressPopupView _progressPopupView;
private SourceProperty _sourceProperty;
private SinkProperty _sinkProperty;
private RackProperty _rackProperty;
private QueueProperty _queueProperty;
private ASRSProperty _asrsProperty;
private RobotArmProperty _robotArmProperty;
private ProcessorProperty _processorProperty;
private ConveyorProperty _conveyorProperty; private ConveyorProperty _conveyorProperty;
private NodeProperty _nodeProperty; private NodeProperty _nodeProperty;
private struct BindingEntry
{
public Action<ComponentType, ComponentDataBase> PropertyHandler;
public bool ShowProgress;
}
private readonly Dictionary<ComponentType, BindingEntry> _bindings = new Dictionary<ComponentType, BindingEntry>();
public void Initialize() public void Initialize()
{ {
_progressPopupView = Object.FindAnyObjectByType<ProgressPopupView>(FindObjectsInactive.Include); _progressPopupView = GameObject.FindAnyObjectByType<ProgressPopupView>(FindObjectsInactive.Include);
_conveyorProperty = GameObject.FindAnyObjectByType<ConveyorProperty>();
_nodeProperty = GameObject.FindAnyObjectByType<NodeProperty>();
_sourceProperty = Object.FindAnyObjectByType<SourceProperty>(); Register<SourceProperty>(ComponentType.Source, (p, t, d) => p.SetPropertyWindow(t, d), showProgress: true);
_sinkProperty = Object.FindAnyObjectByType<SinkProperty>(); Register<SinkProperty>(ComponentType.Sink, (p, t, d) => p.SetPropertyWindow(t, d), showProgress: true);
_rackProperty = Object.FindAnyObjectByType<RackProperty>(); Register<ProcessorProperty>(ComponentType.Processor, (p, t, d) => p.SetPropertyWindow(t, d), showProgress: true);
_queueProperty = Object.FindAnyObjectByType<QueueProperty>(); Register<RackProperty>(ComponentType.Rack, (p, t, d) => p.SetPropertyWindow(t, d));
_asrsProperty = Object.FindAnyObjectByType<ASRSProperty>(); Register<QueueProperty>(ComponentType.Queue, (p, t, d) => p.SetPropertyWindow(t, d));
_robotArmProperty = Object.FindAnyObjectByType<RobotArmProperty>(); Register<ASRSProperty>(ComponentType.ASRS, (p, t, d) => p.SetPropertyWindow(t, d));
_processorProperty = Object.FindAnyObjectByType<ProcessorProperty>(); Register<RobotArmProperty>(ComponentType.RobotArm, (p, t, d) => p.SetPropertyWindow(t, d));
_conveyorProperty = Object.FindAnyObjectByType<ConveyorProperty>(); }
_nodeProperty = Object.FindAnyObjectByType<NodeProperty>();
private void Register<T>(ComponentType type, Action<T, ComponentType, ComponentDataBase> setup, bool showProgress = false) where T : MonoBehaviour
{
var property = GameObject.FindAnyObjectByType<T>();
if (property == null) return;
_bindings[type] = new BindingEntry
{
PropertyHandler = (t, d) => setup(property, t, d),
ShowProgress = showProgress
};
} }
public void BindStandardComponent(ComponentBase component) public void BindStandardComponent(ComponentBase component)
{ {
switch (component.componentType) if (!_bindings.TryGetValue(component.componentType, out var entry))
{ return;
case ComponentType.Source:
if (_sourceProperty != null) component.onComponentClicked += entry.PropertyHandler;
component.onComponentClicked += _sourceProperty.SetPropertyWindow;
if (_progressPopupView != null) if (entry.ShowProgress && _progressPopupView != null)
component.onComponentClicked += _progressPopupView.Show; component.onComponentClicked += _progressPopupView.Show;
break;
case ComponentType.Sink:
if (_sinkProperty != null)
component.onComponentClicked += _sinkProperty.SetPropertyWindow;
if (_progressPopupView != null)
component.onComponentClicked += _progressPopupView.Show;
break;
case ComponentType.Processor:
if (_processorProperty != null)
component.onComponentClicked += _processorProperty.SetPropertyWindow;
if (_progressPopupView != null)
component.onComponentClicked += _progressPopupView.Show;
break;
case ComponentType.Rack:
if (_rackProperty != null)
component.onComponentClicked += _rackProperty.SetPropertyWindow;
break;
case ComponentType.Queue:
if (_queueProperty != null)
component.onComponentClicked += _queueProperty.SetPropertyWindow;
break;
case ComponentType.ASRS:
if (_asrsProperty != null)
component.onComponentClicked += _asrsProperty.SetPropertyWindow;
break;
case ComponentType.RobotArm:
if (_robotArmProperty != null)
component.onComponentClicked += _robotArmProperty.SetPropertyWindow;
break;
case ComponentType.Worker:
break;
}
} }
public void UnbindStandardComponent(ComponentBase component) public void UnbindStandardComponent(ComponentBase component)
{ {
switch (component.componentType) if (!_bindings.TryGetValue(component.componentType, out var entry))
{ return;
case ComponentType.Source:
if (_sourceProperty != null)
component.onComponentClicked -= _sourceProperty.SetPropertyWindow;
if (_progressPopupView != null)
component.onComponentClicked -= _progressPopupView.Show;
break;
case ComponentType.Sink:
if (_sinkProperty != null)
component.onComponentClicked -= _sinkProperty.SetPropertyWindow;
if (_progressPopupView != null)
component.onComponentClicked -= _progressPopupView.Show;
break;
case ComponentType.Processor:
if (_processorProperty != null)
component.onComponentClicked -= _processorProperty.SetPropertyWindow;
if (_progressPopupView != null)
component.onComponentClicked -= _progressPopupView.Show;
break;
case ComponentType.Rack:
if (_rackProperty != null)
component.onComponentClicked -= _rackProperty.SetPropertyWindow;
break;
case ComponentType.Queue:
if (_queueProperty != null)
component.onComponentClicked -= _queueProperty.SetPropertyWindow;
break;
case ComponentType.ASRS:
if (_asrsProperty != null)
component.onComponentClicked -= _asrsProperty.SetPropertyWindow;
break;
case ComponentType.RobotArm:
if (_robotArmProperty != null)
component.onComponentClicked -= _robotArmProperty.SetPropertyWindow;
break;
case ComponentType.Worker:
break;
}
}
public void UnbindConveyor(ConveyorComponent conveyor) component.onComponentClicked -= entry.PropertyHandler;
{
if (_conveyorProperty != null)
conveyor.onConveyorClicked -= _conveyorProperty.SetPropertyWindow;
}
public void UnbindNode(NodeComponent node) if (entry.ShowProgress && _progressPopupView != null)
{ component.onComponentClicked -= _progressPopupView.Show;
if (_nodeProperty != null)
node.onConveyorClicked -= _nodeProperty.SetPropertyWindow;
} }
public void BindConveyor(ConveyorComponent conveyor) public void BindConveyor(ConveyorComponent conveyor)
@@ -135,10 +74,22 @@ namespace Simulator.Data
conveyor.onConveyorClicked += _conveyorProperty.SetPropertyWindow; conveyor.onConveyorClicked += _conveyorProperty.SetPropertyWindow;
} }
public void UnbindConveyor(ConveyorComponent conveyor)
{
if (_conveyorProperty != null)
conveyor.onConveyorClicked -= _conveyorProperty.SetPropertyWindow;
}
public void BindNode(NodeComponent node) public void BindNode(NodeComponent node)
{ {
if (_nodeProperty != null) if (_nodeProperty != null)
node.onConveyorClicked += _nodeProperty.SetPropertyWindow; node.onConveyorClicked += _nodeProperty.SetPropertyWindow;
} }
public void UnbindNode(NodeComponent node)
{
if (_nodeProperty != null)
node.onConveyorClicked -= _nodeProperty.SetPropertyWindow;
}
} }
} }

View File

@@ -133,26 +133,7 @@ public class ASRSProperty : MonoBehaviour
private PropertyGroup CreatePositionGroup(ASRSDataClass asrs) private PropertyGroup CreatePositionGroup(ASRSDataClass asrs)
{ {
var group = new PropertyGroup("asrs_position", "위치 및 회전", isExpanded: true); return PropertyHelper.CreatePositionGroup("asrs_position", asrs, propertyWindow, SaveChange);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", asrs.physical.position.x)
{
}.Bind(
setter: v => {asrs.physical.position.x = v;SaveChange(asrs,v,"physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", asrs.physical.position.y)
{
}.Bind(
setter: v => {asrs.physical.position.y = v;SaveChange(asrs,v,"physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", asrs.physical.orientation)
{
}.Bind(
setter: v => {asrs.physical.orientation = v;SaveChange(asrs,v,"physical.orientation");propertyWindow.ApplyExternalValue("orientation",RotationSnap.SnapOrientation(v),false); }
),
});
return group;
} }
private PropertyGroup CreateRackLayoutGroup(ASRSDataClass asrs) private PropertyGroup CreateRackLayoutGroup(ASRSDataClass asrs)

View File

@@ -80,58 +80,12 @@ public class NodeProperty : MonoBehaviour
private PropertyGroup CreateInputDetailGroup(ConveyorNode node) private PropertyGroup CreateInputDetailGroup(ConveyorNode node)
{ {
var group = new PropertyGroup("inputs", "투입 대상 컴포넌트", isExpanded: true); return PropertyHelper.CreateConnectionGroup("inputs", "투입 대상 컴포넌트", node.inputs, SaveChange);
if (node.inputs == null||node.inputs.Count == 0)
{
group.AddItem(new StringProperty($"inputs.no_locs", "", "연결된 항목이 없습니다.")
{
IsReadOnly = true
});
return group;
}
foreach (var input in node.inputs)
{
string inputId = $"input.count.{input.target}";
var countProp = new IntProperty(inputId, input.target, input.required_items)
.Bind(setter: v =>
{
input.required_items = v; SaveChange(input, input.required_items, "required_items");
});
group.AddItem(countProp);
}
return group;
} }
private PropertyGroup CreateOutputDetailGroup(ConveyorNode node) private PropertyGroup CreateOutputDetailGroup(ConveyorNode node)
{ {
var group = new PropertyGroup("outputs", "출하 대상 컴포넌트", isExpanded: true); return PropertyHelper.CreateConnectionGroup("outputs", "출하 대상 컴포넌트", node.outputs, SaveChange);
if (node.outputs == null||node.outputs.Count == 0)
{
group.AddItem(new StringProperty($"outputs.no_locs", "", "연결된 항목이 없습니다.")
{
IsReadOnly = true
});
return group;
}
foreach (var output in node.outputs)
{
string outputId = $"output.count.{output.target}";
var countProp = new IntProperty(outputId, output.target, output.required_items)
.Bind(setter: v =>
{
output.required_items = v; SaveChange(output, output.required_items, "required_items");
});
group.AddItem(countProp);
}
return group;
} }
private void HandleDisplayModeChanged_InputOutput(string mode) private void HandleDisplayModeChanged_InputOutput(string mode)

View File

@@ -144,26 +144,7 @@ public class ProcessorProperty : MonoBehaviour
private PropertyGroup CreatePositionGroup(ProcessorDataClass processor) private PropertyGroup CreatePositionGroup(ProcessorDataClass processor)
{ {
var group = new PropertyGroup("processor_position", "위치 및 회전", isExpanded: true); return PropertyHelper.CreatePositionGroup("processor_position", processor, propertyWindow, SaveChange);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", processor.physical.position.x)
{
}.Bind(
setter: v => {processor.physical.position.x = v;SaveChange(processor,v,"physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", processor.physical.position.y)
{
}.Bind(
setter: v => {processor.physical.position.y = v;SaveChange(processor,v,"physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", processor.physical.orientation)
{
}.Bind(
setter: v => {processor.physical.orientation = v;SaveChange(processor,v,"physical.orientation");propertyWindow.ApplyExternalValue("orientation",RotationSnap.SnapOrientation(v),false); }
),
});
return group;
} }
#region #region

View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using Simulator.Data;
using UVC.UI.Window.PropertyWindow;
public static class PropertyHelper
{
public static void SaveChange(object source, object value, string name)
{
var path = PathIndexer.GetNodePath(source);
Patch updateData = new Patch();
updateData.value = value;
UpdateValueStack.AddPatch($"{path}.{name}", value);
}
public static PropertyGroup CreatePositionGroup(
string groupId,
ComponentDataBase data,
PropertyWindow propertyWindow,
Action<object, object, string> saveChange)
{
var group = new PropertyGroup(groupId, "위치 및 회전", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", data.physical.position.x)
{
}.Bind(
setter: v => { data.physical.position.x = v; saveChange(data, v, "physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", data.physical.position.y)
{
}.Bind(
setter: v => { data.physical.position.y = v; saveChange(data, v, "physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", data.physical.orientation)
{
}.Bind(
setter: v => { data.physical.orientation = v; saveChange(data, v, "physical.orientation"); propertyWindow.ApplyExternalValue("orientation", RotationSnap.SnapOrientation(v), false); }
),
});
return group;
}
public static PropertyGroup CreatePositionGroup(
string groupId,
Physical physical,
object dataOwner,
PropertyWindow propertyWindow,
Action<object, object, string> saveChange)
{
var group = new PropertyGroup(groupId, "위치 및 회전", isExpanded: true);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", physical.position.x)
{
}.Bind(
setter: v => { physical.position.x = v; saveChange(dataOwner, v, "physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", physical.position.y)
{
}.Bind(
setter: v => { physical.position.y = v; saveChange(dataOwner, v, "physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", physical.orientation)
{
}.Bind(
setter: v => { physical.orientation = v; saveChange(dataOwner, v, "physical.orientation"); propertyWindow.ApplyExternalValue("orientation", RotationSnap.SnapOrientation(v), false); }
),
});
return group;
}
public static PropertyGroup CreateConnectionGroup(
string groupId,
string label,
List<InOutItem> items,
Action<object, object, string> saveChange)
{
var group = new PropertyGroup(groupId, label, isExpanded: true);
if (items == null || items.Count == 0)
{
group.AddItem(new StringProperty($"{groupId}.no_locs", "", "연결된 항목이 없습니다.")
{
IsReadOnly = true
});
return group;
}
foreach (var item in items)
{
string itemId = $"{groupId}.count.{item.target}";
var countProp = new IntProperty(itemId, item.target, item.required_items)
.Bind(setter: v =>
{
item.required_items = v; saveChange(item, item.required_items, "required_items");
});
group.AddItem(countProp);
}
return group;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 02a054131b8b79749a6255f3830fd0b3

View File

@@ -88,26 +88,7 @@ public class QueueProperty : MonoBehaviour
private PropertyGroup CreatePositionGroup(QueueDataClass queue) private PropertyGroup CreatePositionGroup(QueueDataClass queue)
{ {
var group = new PropertyGroup("queue_position", "위치 및 회전", isExpanded: true); return PropertyHelper.CreatePositionGroup("queue_position", queue, propertyWindow, SaveChange);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", queue.physical.position.x)
{
}.Bind(
setter: v => {queue.physical.position.x = v;SaveChange(queue,v,"physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", queue.physical.position.y)
{
}.Bind(
setter: v => {queue.physical.position.y = v;SaveChange(queue,v,"physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", queue.physical.orientation)
{
}.Bind(
setter: v => {queue.physical.orientation = v;SaveChange(queue,v,"physical.orientation");propertyWindow.ApplyExternalValue("orientation",RotationSnap.SnapOrientation(v),false); }
),
});
return group;
} }
private void OnDestroy() private void OnDestroy()

View File

@@ -113,26 +113,7 @@ public class RackProperty : MonoBehaviour
private PropertyGroup CreatePositionGroup(RackDataClass rack) private PropertyGroup CreatePositionGroup(RackDataClass rack)
{ {
var group = new PropertyGroup("rack_position", "위치 및 회전", isExpanded: true); return PropertyHelper.CreatePositionGroup("rack_position", rack, propertyWindow, SaveChange);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", rack.physical.position.x)
{
}.Bind(
setter: v => {rack.physical.position.x = v;SaveChange(rack,v,"physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", rack.physical.position.y)
{
}.Bind(
setter: v => {rack.physical.position.y = v;SaveChange(rack,v,"physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", rack.physical.orientation)
{
}.Bind(
setter: v => {rack.physical.orientation = v;SaveChange(rack,v,"physical.orientation");propertyWindow.ApplyExternalValue("orientation",RotationSnap.SnapOrientation(v),false); }
),
});
return group;
} }
private PropertyGroup CreateRackLayoutGroup(RackDataClass rack) private PropertyGroup CreateRackLayoutGroup(RackDataClass rack)

View File

@@ -87,26 +87,7 @@ public class RobotArmProperty : MonoBehaviour
private PropertyGroup CreatePositionGroup(RobotArmDataClass robotArm) private PropertyGroup CreatePositionGroup(RobotArmDataClass robotArm)
{ {
var group = new PropertyGroup("robotArm_position", "위치 및 회전", isExpanded: true); return PropertyHelper.CreatePositionGroup("robotArm_position", robotArm, propertyWindow, SaveChange);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", robotArm.physical.position.x)
{
}.Bind(
setter: v => {robotArm.physical.position.x = v;SaveChange(robotArm,v,"physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", robotArm.physical.position.y)
{
}.Bind(
setter: v => {robotArm.physical.position.y = v;SaveChange(robotArm,v,"physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", robotArm.physical.orientation)
{
}.Bind(
setter: v => {robotArm.physical.orientation = v;SaveChange(robotArm,v,"physical.orientation");propertyWindow.ApplyExternalValue("orientation",RotationSnap.SnapOrientation(v),false); }
),
});
return group;
} }
PropertyGroup CreateInputsGroup(RobotArmDataClass robotArm) PropertyGroup CreateInputsGroup(RobotArmDataClass robotArm)

View File

@@ -57,53 +57,11 @@ public class SinkProperty : MonoBehaviour
private PropertyGroup CreatePositionGroup(SinkDataClass sink) private PropertyGroup CreatePositionGroup(SinkDataClass sink)
{ {
var group = new PropertyGroup("robotArm_position", "위치 및 회전", isExpanded: true); return PropertyHelper.CreatePositionGroup("sink_position", sink, propertyWindow, SaveChange);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", sink.physical.position.x)
{
}.Bind(
setter: v => {sink.physical.position.x = v;SaveChange(sink,v,"physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", sink.physical.position.y)
{
}.Bind(
setter: v => {sink.physical.position.y = v;SaveChange(sink,v,"physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", sink.physical.orientation)
{
}.Bind(
setter: v => {sink.physical.orientation = v;SaveChange(sink,v,"physical.orientation");propertyWindow.ApplyExternalValue("orientation",RotationSnap.SnapOrientation(v),false); }
),
});
return group;
} }
private PropertyGroup CreateInputDetailGroup(SinkDataClass sink) private PropertyGroup CreateInputDetailGroup(SinkDataClass sink)
{ {
var group = new PropertyGroup("inputs", "입력 연결", isExpanded: true); return PropertyHelper.CreateConnectionGroup("inputs", "입력 연결", sink.inputs, SaveChange);
if (sink.inputs == null || sink.inputs.Count == 0)
{
group.AddItem(new StringProperty($"inputs.no_locs", "", "연결된 항목이 없습니다.")
{
IsReadOnly = true
});
return group;
}
foreach (var input in sink.inputs)
{
string inputId = $"input.count.{input.target}";
var countProp = new IntProperty(inputId, input.target, input.required_items)
.Bind(setter: v =>
{
input.required_items = v; SaveChange(input, input.required_items, "required_items");
});
group.AddItem(countProp);
}
return group;
} }
} }

View File

@@ -117,26 +117,7 @@ public class SourceProperty : MonoBehaviour
private PropertyGroup CreatePositionGroup(SourceDataClass source) private PropertyGroup CreatePositionGroup(SourceDataClass source)
{ {
var group = new PropertyGroup("robotArm_position", "위치 및 회전", isExpanded: true); return PropertyHelper.CreatePositionGroup("source_position", source, propertyWindow, SaveChange);
group.AddItems(new IPropertyItem[]
{
new FloatProperty("x_position", "X 좌표(m)", source.physical.position.x)
{
}.Bind(
setter: v => {source.physical.position.x = v;SaveChange(source,v,"physical.position.x"); }
),
new FloatProperty("y_position", "y 좌표(m)", source.physical.position.y)
{
}.Bind(
setter: v => {source.physical.position.y = v;SaveChange(source,v,"physical.position.y"); }
),
new FloatProperty("orientation", "회전(°)", source.physical.orientation)
{
}.Bind(
setter: v => {source.physical.orientation = v;SaveChange(source,v,"physical.orientation");propertyWindow.ApplyExternalValue("orientation",RotationSnap.SnapOrientation(v),false); }
),
});
return group;
} }
private void OnDestroy() private void OnDestroy()

View File

@@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity",
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
"com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity", "com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity",
"com.unity.2d.sprite": "1.0.0", "com.unity.2d.sprite": "1.0.0",

155
README.md
View File

@@ -46,4 +46,157 @@ Digital Twin 가상공장 개발 용 라이브러리입니다.
- https://github.com/HolyShovelSoft/log4uni 다운로드 압축해제 > Assets > Plugins 폴더에 복사 - https://github.com/HolyShovelSoft/log4uni 다운로드 압축해제 > Assets > Plugins 폴더에 복사
- **NuGetForUnity_4.5.0** - **NuGetForUnity_4.5.0**
- Window > Package Manager > + > Add packacge from git URL > https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity - Window > Package Manager > + > Add packacge from git URL > https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity
- Menu > Nuget > Manage Nuget Packages > MessagePack 3.1.4 - Menu > Nuget > Manage Nuget Packages > MessagePack 3.1.4
### 아키텍처 (MVVM/MVC)
```
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ View │◄────│ ViewModel │◄────│ Model │
│ (UXML/USS) │ │ (Presenter) │ │ (Service) │
└─────────────┘ └──────────────┘ └─────────────┘
│ │
└── 이벤트 전달 ──────┘
```
| 레이어 | 책임 | 금지 사항 |
|--------|------|-----------|
| **View** | 표시/레이아웃, 이벤트 라우팅 | 비즈니스 로직, 상태 보유 |
| **ViewModel/Presenter** | 상태 관리, 데이터 변환, 바인딩 속성 | Unity API 직접 호출 (테스트 용이) |
| **Model/Service** | 도메인 로직, 데이터 접근 | UI 참조 |
- **MVVM**: UI 상태/양방향 동기화가 많을 때
- **MVC**: 입력 → 도메인 액션 → UI 반영 흐름이 단순할 때
### 필수 규약
- 파일 선두에 `#nullable enable`, 모든 참조형에 `?` 명시
- 비동기는 `UniTask` + `CancellationToken` 사용 (`Task`/코루틴 지양)
- 느슨한 결합: 인터페이스/이벤트로 연결
## 메모리 관리
### 이벤트 구독/해제
```csharp
private EventCallback<ClickEvent>? _onClick;
void OnEnable() {
_onClick = OnButtonClick;
_button?.RegisterCallback(_onClick);
}
void OnDisable() {
_button?.UnregisterCallback(_onClick);
}
```
### 체크리스트
- [ ] `RegisterCallback<T>``UnregisterCallback<T>` 대칭 확인
- [ ] `CancellationTokenSource``OnDestroy`에서 `Cancel/Dispose`
- [ ] VisualTreeAsset/USS 동일 리소스 반복 로드 방지 (캐싱)
- [ ] 클로저/람다 캡처로 인한 누수 점검
- [ ] 오브젝트 풀: `IPoolable.OnRent/OnReturn` 훅, 반환 시 `DOTween.Kill()`
## 주석 원칙 (C# XML)
```csharp
/// <summary>
/// 사용자 데이터를 비동기로 로드합니다.
/// </summary>
/// <param name="userId">사용자 ID.</param>
/// <param name="ct">취소 토큰.</param>
/// <returns>사용자 데이터 또는 null.</returns>
public async UniTask<UserData?> LoadUserAsync(string userId, CancellationToken ct)
```
- 클래스: 역할/책임/사용 예
- 메서드: 요약 + 파라미터/반환 (복잡 로직만 상세)
- 속성: 한 줄 요약
---
## 디자인 패턴 요약
| 패턴 | 사용 시점 |
|------|-----------|
| **DI/Composition Root** | 서비스/ViewModel 주입 일원화 |
| **Event Aggregator** | 컴포넌트 간 느슨한 통신 |
| **Command + Undo** | UI 액션에 되돌리기 필요 시 |
| **Strategy** | 정렬/필터 규칙 교체 |
| **State/FSM** | 모드 전환 (편집/선택/드래그) |
| **Factory** | 뷰/프리팹 생성 캡슐화 |
| **Object Pool** | 대량 아이템 재사용 |
| **Repository** | 데이터 소스 추상화 |
## 핵심 개발 원칙
* [cite_start]항상 TDD 사이클을 따릅니다: Red → Green → Refactor [cite: 4]
* [cite_start]가장 간단하게 실패하는 테스트를 먼저 작성합니다[cite: 4].
* [cite_start]테스트를 통과시키기 위한 최소한의 코드만 구현합니다[cite: 4].
* [cite_start]테스트가 통과한 후에만 리팩토링합니다[cite: 4].
* [cite_start]구조적 변경과 행위적 변경을 분리하여 Beck의 "Tidy First" 접근 방식을 따릅니다[cite: 4].
* [cite_start]개발 전반에 걸쳐 높은 코드 품질을 유지합니다[cite: 4].
* [cite_start]항상 한 번에 하나의 테스트를 작성하고, 실행시킨 다음 구조를 개선합니다[cite: 8].
## TDD 방법론 가이드라인
* [cite_start]작은 기능 증분을 정의하는 실패하는 테스트를 작성하는 것으로 시작합니다[cite: 4].
* [cite_start]행위를 설명하는 의미 있는 테스트 이름을 사용합니다 (예: "ShouldSumTwoPositiveNumbers")[cite: 4].
* [cite_start]테스트 실패를 명확하고 유익하게 만듭니다[cite: 4].
* 테스트를 통과시키기 위한 충분한 코드만 작성합니다. [cite_start]더 이상은 안 됩니다[cite: 4].
* [cite_start]테스트가 통과하면 리팩토링이 필요한지 고려합니다[cite: 5].
* [cite_start]새로운 기능을 위해 사이클을 반복합니다[cite: 5].
* [cite_start]항상 모든 테스트(오래 실행되는 테스트 제외)를 매번 실행합니다[cite: 9].
## Tidy First 접근 방식
* [cite_start]모든 변경 사항을 두 가지 유형으로 명확히 분리합니다[cite: 4]:
1. [cite_start]**구조적 변경 (STRUCTURAL CHANGES)**: 동작을 변경하지 않고 코드를 재배열하는 것 (이름 변경, 메서드 추출, 코드 이동)[cite: 4].
2. [cite_start]**행위적 변경 (BEHAVIORAL CHANGES)**: 실제 기능을 추가하거나 수정하는 것[cite: 4].
* [cite_start]구조적 변경과 행위적 변경을 동일한 커밋에 절대 섞지 않습니다[cite: 4].
* [cite_start]둘 다 필요한 경우 항상 구조적 변경을 먼저 수행합니다[cite: 4].
* [cite_start]변경 전후에 테스트를 실행하여 구조적 변경이 동작을 변경하지 않는지 확인합니다[cite: 4].
## 코드 품질 표준
* [cite_start]중복을 철저히 제거합니다[cite: 6].
* [cite_start]명명 및 구조를 통해 의도를 명확하게 표현합니다[cite: 6].
* [cite_start]의존성을 명시적으로 만듭니다[cite: 6].
* [cite_start]메서드를 작고 단일 책임에 집중하도록 유지합니다[cite: 6].
* [cite_start]상태와 부작용을 최소화합니다[cite: 6].
* [cite_start]가능한 가장 간단한 솔루션을 사용합니다[cite: 6].
* ## 리팩토링 가이드라인
* [cite_start]테스트가 통과할 때만 리팩토링합니다 ("Green" 단계)[cite: 6].
* [cite_start]확립된 리팩토링 패턴을 올바른 이름으로 사용합니다[cite: 6].
* [cite_start]한 번에 하나의 리팩토링 변경만 수행합니다[cite: 6].
* [cite_start]각 리팩토링 단계 후에 테스트를 실행합니다[cite: 6].
* [cite_start]중복을 제거하거나 명확성을 향상시키는 리팩토링을 우선시합니다[cite: 7].
## 예시 워크플로우
새로운 기능을 구현할 때:
1. [cite_start]기능의 작은 부분에 대한 간단한 실패 테스트를 작성합니다[cite: 7].
2. [cite_start]이를 통과시키기 위한 최소한의 코드를 구현합니다[cite: 7].
3. [cite_start]테스트가 통과하는지 확인하기 위해 테스트를 실행합니다 (Green)[cite: 7].
4. [cite_start]필요한 구조적 변경을 수행합니다 (Tidy First), 각 변경 후에 테스트를 실행합니다[cite: 7].
5. [cite_start]구조적 변경을 별도로 커밋합니다[cite: 7].
6. [cite_start]다음 작은 기능 증분을 위한 다른 테스트를 추가합니다[cite: 7].
7. [cite_start]기능이 완료될 때까지 반복하며, 행위적 변경을 구조적 변경과 별도로 커밋합니다[cite: 7].
[cite_start]이 프로세스를 정확하게 따르며, 항상 빠른 구현보다 깔끔하고 잘 테스트된 코드를 우선시합니다[cite: 7].
## C# 특정 지침 (Unity 환경)
* **Unity 컴포넌트 라이프사이클:** `Awake`, `Start`, `Update`, `FixedUpdate`, `LateUpdate`, `OnEnable`, `OnDisable`, `OnDestroy`와 같은 Unity 컴포넌트 라이프사이클 메서드의 사용 목적을 명확히 이해하고, 각 메서드에서 수행해야 할 작업의 종류를 제한합니다.
* **컴포넌트 기반 아키텍처:** 각 Unity 컴포넌트가 단일 책임을 갖도록 설계합니다. 관련된 기능은 별도의 컴포넌트로 분리합니다.
* **GameObject 및 Transform 접근:** `GetComponent<T>()` 호출을 최소화하고, 필요한 컴포넌트는 `Awake` 또는 `Start`에서 캐시하여 성능을 최적화합니다.
* **Coroutines 및 Events:** 비동기 작업에는 코루틴을 사용하되, 완료 시 콜백이나 이벤트를 사용하여 느슨한 결합을 유지합니다. UnityEvent 또는 C# 이벤트/델리게이트를 적극 활용합니다.
* **스크립터블 오브젝트(ScriptableObjects):** 데이터와 로직을 분리하기 위해 스크립터블 오브젝트를 활용하여 게임 데이터, 설정, 자원 등을 관리합니다.
* **메모리 관리:** 불필요한 GC(Garbage Collection) 할당을 피하기 위해 `new` 키워드 사용을 최소화하고, 컬렉션 사용 시 재사용 가능한 풀링 기법을 고려합니다.
* **Physics 상호작용:** 물리 계산은 `FixedUpdate`에서 수행하며, `Rigidbody`를 통해 물리적 상호작용을 처리합니다.
* **씬 관리:** 씬 로딩 및 언로딩 시 발생하는 의존성 문제를 최소화하기 위해 씬 간의 통신 방법을 명확히 정의합니다.
* **네임스페이스 사용:** 코드의 가독성과 충돌 방지를 위해 적절한 네임스페이스를 사용합니다.
* **람다식 및 LINQ:** 필요할 때 람다식과 LINQ를 사용하여 코드를 간결하게 만들 수 있지만, 성능 영향을 고려하여 과도한 사용을 피합니다.
* **null 체크:** Unity 오브젝트에 대한 `null` 체크는 일반 C# 객체와 다르게 동작할 수 있으므로, `== null` 연산자 사용 시 Unity의 오버로드된 연산자를 이해하고 사용합니다.
* **에디터 확장:** 개발 워크플로우를 개선하기 위해 Custom Editor 또는 EditorWindow와 같은 Unity 에디터 확장을 고려할 수 있습니다.