feat: PropertyWindow Network 탭 및 Twin Agent Auto 프로세스 추가

PropertyWindow에 Network 탭 시스템을 추가하고, Twin Agent의 자동 네트워크 설정 기능을 구현했습니다.

주요 기능:
- PropertyWindow 탭 시스템 (PropertyTab, PropertyTabView)
- Network 탭 추가 (Server Type별 동적 설정)
- Button PropertyType 추가 (ButtonProperty, ButtonPropertyUI)
- Label PropertyType 추가 (LabelProperty, LabelPropertyUI)
- Entity Processor 패턴 (IEntityProcessor, TwinAgentAutoProcessor)
- PropertyItem 이벤트 시스템 (ValueChanged, IsVisibleChanged, ValueChangedObject)
- 동적 가시성 제어 (Server Type, Connection Type 변경 시)
- 진행 상태 애니메이션 (타이핑 효과, 점 애니메이션, 완료 시 초록색)

Server Type 구성:
- Twin Agent: Auto 버튼 + 자동 진행 (Read Entity → Connection)
- Octopus Hub: Connection Type (MQTT/API), Topics, URI, Period
- Octopus AI: Agent Type, URI

기술 구현:
- PropertyItem 양방향 바인딩
- Entity 프로세서 컨테이너 패턴
- UniTask 비동기 진행 (1~3초 무작위 대기)
- 실시간 UI 갱신 (ValueChangedObject 이벤트)

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
wsh
2026-02-08 20:53:26 +09:00
parent 5c6b2bb78c
commit 91c1337d6a
41 changed files with 4634 additions and 15 deletions

View File

@@ -0,0 +1,259 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &5817941051672006636
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7298888429059513614}
- component: {fileID: 6782256294732231658}
- component: {fileID: 3760584139537418684}
- component: {fileID: 2050240156427873864}
m_Layer: 5
m_Name: PropertyTabButton
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7298888429059513614
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5817941051672006636}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8651094892039187925}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 60, y: 32}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &6782256294732231658
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5817941051672006636}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 2050240156427873864}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!222 &3760584139537418684
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5817941051672006636}
m_CullTransparentMesh: 1
--- !u!114 &2050240156427873864
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5817941051672006636}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 0.149, g: 0.149, b: 0.149, a: 0}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &6785518887253916802
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8651094892039187925}
- component: {fileID: 42745405435837700}
- component: {fileID: 5525776800996606844}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8651094892039187925
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6785518887253916802}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7298888429059513614}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -10, y: -20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &42745405435837700
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6785518887253916802}
m_CullTransparentMesh: 1
--- !u!114 &5525776800996606844
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6785518887253916802}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Tab
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4291611852
m_fontColor: {r: 0.8, g: 0.8, b: 0.8, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 10
m_fontSizeBase: 10
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c52761060ad8f2c45b4783fca02a4b95
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,90 @@
# PropertyTabButton Prefab 생성 가이드
## 📋 개요
PropertyWindow에서 사용할 탭 버튼 Prefab을 생성하는 방법입니다.
## 🛠️ 생성 방법
### 1단계: 빈 GameObject 생성
1. Hierarchy에서 우클릭 → `UI``Button - TextMeshPro` 선택
2. 이름을 `PropertyTabButton`으로 변경
### 2단계: Button 설정
**RectTransform**
- Width: 자동 (Content Size Fitter 사용)
- Height: 32
- Anchor: Stretch (양쪽 늘림)
**Button 컴포넌트**
- Target Graphic: Image
- Transition: Color Tint
- Normal Color: RGB(38, 38, 38) - #262626
- Highlighted Color: RGB(64, 64, 64) - #404040
- Pressed Color: RGB(51, 51, 51) - #333333
- Selected Color: RGB(51, 51, 51) - #333333
- Disabled Color: RGB(25, 25, 25) - #191919
**Image 컴포넌트** (Button 배경)
- Source Image: UI Sprite (Unity 기본 또는 커스텀)
- Image Type: Sliced
- Color: RGB(38, 38, 38) - #262626
### 3단계: Text (TMP) 자식 오브젝트 설정
Button 하위의 `Text (TMP)` 오브젝트:
**RectTransform**
- Anchor: Stretch (전체 확장)
- Left/Right/Top/Bottom: 10 (패딩)
**TextMeshProUGUI 컴포넌트**
- Text: "PROPERTIES" (기본값, 런타임에 변경됨)
- Font Asset: 프로젝트에 맞는 폰트
- Font Size: 11
- Color: RGB(204, 204, 204) - #CCCCCC (80% 밝기)
- Alignment: Center & Middle
- Wrapping: Disabled
- Overflow: Overflow
### 4단계: Content Size Fitter 추가
Button GameObject에 `Content Size Fitter` 컴포넌트 추가:
- Horizontal Fit: Preferred Size
- Vertical Fit: Unconstrained (또는 Fixed)
이렇게 하면 텍스트 길이에 따라 버튼 너비가 자동 조절됩니다.
### 5단계: Layout Element 추가 (선택 사항)
Button GameObject에 `Layout Element` 컴포넌트 추가:
- Min Width: 60 (최소 너비)
- Preferred Width: -1 (자동)
- Min Height: 32
- Preferred Height: 32
### 6단계: Prefab 저장
1. Hierarchy의 `PropertyTabButton`을 드래그
2. `Assets/DownloadAssets/XRLib/Resources/Prefabs/UI/Window/` 폴더에 드롭
3. 이제 `PropertyTabButton.prefab` 파일이 생성됨
## 🎨 시각적 구조
```
PropertyTabButton (Button)
├── Image (배경)
└── Text (TMP) (텍스트)
```
## 📐 최종 크기
- 높이: 32px (고정)
- 너비: 텍스트 길이 + 패딩 20px (자동 조절)
- 여백: 좌우 10px, 상하 0px
## 🔗 연결
PropertyWindow Prefab의 PropertyTabView 컴포넌트에서:
- `Tab Button Prefab` 필드에 생성한 `PropertyTabButton` Prefab을 드래그하여 연결
## 💡 참고
- 선택된 탭: PropertyTabView가 런타임에 색상을 변경합니다.
- 텍스트 내용: PropertyTabView가 런타임에 탭 이름으로 설정합니다.
- 클릭 이벤트: PropertyTabView가 런타임에 자동으로 등록합니다.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f4d47e4475932364bb053dc5facf32bf
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,683 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &3519998083214841716
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5263911610417920515}
- component: {fileID: 5504118748185253800}
- component: {fileID: 1799856880163421207}
- component: {fileID: 4926475996635603707}
m_Layer: 5
m_Name: Name
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &5263911610417920515
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3519998083214841716}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1673569847674262589}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 16}
m_Pivot: {x: 0, y: 1}
--- !u!114 &5504118748185253800
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3519998083214841716}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 266dd70132eff3d4eb32c995c009634a, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!222 &1799856880163421207
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3519998083214841716}
m_CullTransparentMesh: 1
--- !u!114 &4926475996635603707
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3519998083214841716}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Name
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 73a8cbdb8d46fbb4bae58573ac247b09, type: 2}
m_sharedMaterial: {fileID: -2117747647215524922, guid: 73a8cbdb8d46fbb4bae58573ac247b09, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4291611852
m_fontColor: {r: 0.8, g: 0.8, b: 0.8, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 13
m_fontSizeBase: 13
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &6001687989171742108
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7739657533834086419}
- component: {fileID: 4803966663021355126}
- component: {fileID: 1422331351419321184}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7739657533834086419
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6001687989171742108}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 7005612871078381859}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4803966663021355126
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6001687989171742108}
m_CullTransparentMesh: 1
--- !u!114 &1422331351419321184
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6001687989171742108}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Button
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 73a8cbdb8d46fbb4bae58573ac247b09, type: 2}
m_sharedMaterial: {fileID: -2117747647215524922, guid: 73a8cbdb8d46fbb4bae58573ac247b09, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 10
m_fontSizeBase: 10
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &7250795714712263932
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1673569847674262589}
- component: {fileID: 3468544156086869607}
- component: {fileID: 3578994535031451876}
- component: {fileID: 7190366006269617073}
- component: {fileID: 5345020441566453415}
m_Layer: 5
m_Name: ButtonPropertyUI
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1673569847674262589
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7250795714712263932}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 5263911610417920515}
- {fileID: 5993356640736488824}
- {fileID: 7005612871078381859}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -1, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!114 &3468544156086869607
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7250795714712263932}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2d6578f6d0ce25e40973af09160ed997, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UVC.UI.Window.PropertyWindow.UI.ButtonPropertyUI
_nameLabel: {fileID: 4926475996635603707}
_descriptionLabel: {fileID: 196915125063634945}
_button: {fileID: 8229148575517698044}
_buttonText: {fileID: 1422331351419321184}
--- !u!114 &3578994535031451876
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7250795714712263932}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Padding:
m_Left: 16
m_Right: 16
m_Top: 8
m_Bottom: 8
m_ChildAlignment: 0
m_Spacing: 4
m_ChildForceExpandWidth: 1
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
m_ChildControlHeight: 0
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!114 &7190366006269617073
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7250795714712263932}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!114 &5345020441566453415
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7250795714712263932}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
m_HorizontalFit: 0
m_VerticalFit: 2
--- !u!1 &7683527998330953134
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7005612871078381859}
- component: {fileID: 1713864416376247393}
- component: {fileID: 1524806464762208753}
- component: {fileID: 8229148575517698044}
- component: {fileID: 5064998542039494609}
m_Layer: 5
m_Name: Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7005612871078381859
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7683527998330953134}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7739657533834086419}
m_Father: {fileID: 1673569847674262589}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1713864416376247393
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7683527998330953134}
m_CullTransparentMesh: 1
--- !u!114 &1524806464762208753
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7683527998330953134}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &8229148575517698044
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7683527998330953134}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 0.6320754, g: 0.6320754, b: 0.6320754, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 1524806464762208753}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!114 &5064998542039494609
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7683527998330953134}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: 30
m_MinHeight: 20
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &8559061638628176317
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5993356640736488824}
- component: {fileID: 465031782351339931}
- component: {fileID: 196915125063634945}
m_Layer: 5
m_Name: Description
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &5993356640736488824
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8559061638628176317}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1673569847674262589}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 14}
m_Pivot: {x: 0, y: 1}
--- !u!222 &465031782351339931
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8559061638628176317}
m_CullTransparentMesh: 1
--- !u!114 &196915125063634945
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8559061638628176317}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Description
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2}
m_sharedMaterial: {fileID: 1892695685711531568, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4291611852
m_fontColor: {r: 0.8, g: 0.8, b: 0.8, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 12
m_fontSizeBase: 12
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 44a8f48a35bd63d4f81a2448c6f70d96
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,538 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &807157978223524053
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4879897783975719760}
- component: {fileID: 1584684691017275438}
- component: {fileID: 2134895009375913828}
m_Layer: 5
m_Name: Description
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4879897783975719760
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 807157978223524053}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6948320696978959352}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 14}
m_Pivot: {x: 0, y: 1}
--- !u!222 &1584684691017275438
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 807157978223524053}
m_CullTransparentMesh: 1
--- !u!114 &2134895009375913828
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 807157978223524053}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Description
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2}
m_sharedMaterial: {fileID: 1892695685711531568, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4291611852
m_fontColor: {r: 0.8, g: 0.8, b: 0.8, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 12
m_fontSizeBase: 12
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &1623746817766343238
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6020519455540207131}
- component: {fileID: 2684097375238853316}
- component: {fileID: 4280685791673950716}
m_Layer: 5
m_Name: ValueLabel
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6020519455540207131
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1623746817766343238}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6948320696978959352}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 14}
m_Pivot: {x: 0, y: 1}
--- !u!222 &2684097375238853316
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1623746817766343238}
m_CullTransparentMesh: 1
--- !u!114 &4280685791673950716
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1623746817766343238}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Description
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2}
m_sharedMaterial: {fileID: 1892695685711531568, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4291611852
m_fontColor: {r: 0.8, g: 0.8, b: 0.8, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 12
m_fontSizeBase: 12
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &2294208718397891721
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1356120985410992881}
- component: {fileID: 2220876580841847441}
- component: {fileID: 5581841670705286119}
- component: {fileID: 7734407643640775980}
m_Layer: 5
m_Name: Name
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1356120985410992881
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2294208718397891721}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 6948320696978959352}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 16}
m_Pivot: {x: 0, y: 1}
--- !u!114 &2220876580841847441
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2294208718397891721}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 266dd70132eff3d4eb32c995c009634a, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!222 &5581841670705286119
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2294208718397891721}
m_CullTransparentMesh: 1
--- !u!114 &7734407643640775980
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2294208718397891721}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Name
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2}
m_sharedMaterial: {fileID: 1892695685711531568, guid: 2ec62c2b5b163f64e92dc4988250c9c8, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4291611852
m_fontColor: {r: 0.8, g: 0.8, b: 0.8, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 13
m_fontSizeBase: 13
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &2484066679782419758
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6948320696978959352}
- component: {fileID: 4253558289765446409}
- component: {fileID: 1001481568101346305}
- component: {fileID: 1283532448571143673}
- component: {fileID: 8169199501776635067}
m_Layer: 5
m_Name: LabelPropertyUI
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &6948320696978959352
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2484066679782419758}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1356120985410992881}
- {fileID: 4879897783975719760}
- {fileID: 6020519455540207131}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -0}
m_SizeDelta: {x: 0, y: 20}
m_Pivot: {x: 0, y: 1}
--- !u!114 &4253558289765446409
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2484066679782419758}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: 20
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!114 &1001481568101346305
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2484066679782419758}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 266dd70132eff3d4eb32c995c009634a, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1283532448571143673
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2484066679782419758}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup
m_Padding:
m_Left: 16
m_Right: 16
m_Top: 4
m_Bottom: 4
m_ChildAlignment: 3
m_Spacing: 10
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
m_ChildControlHeight: 0
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!114 &8169199501776635067
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2484066679782419758}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a5bffbcbfc3cc8640851dadcc462cfe3, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UVC.UI.Window.PropertyWindow.UI.LabelPropertyUI
_nameLabel: {fileID: 7734407643640775980}
_descriptionLabel: {fileID: 2134895009375913828}
_valueLabel: {fileID: 4280685791673950716}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 93f804cf7d904534fb648912390f4b22
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,114 @@
# PropertyWindow Prefab 탭 시스템 설정 가이드
## 📋 개요
PropertyWindow Prefab에 탭 시스템을 추가하는 방법입니다.
## 🛠️ 설정 방법
### 1단계: Prefab 열기
1. Unity Editor에서 `Assets/DownloadAssets/XRLib/Resources/Prefabs/UI/Window/PropertyWindow.prefab` 더블클릭
2. Prefab Edit 모드로 진입
### 2단계: 기존 "PROPERTIES" 텍스트 교체
#### 2-1. Text (TMP) 오브젝트 삭제 또는 비활성화
1. Hierarchy에서 `PropertyWindow``Top``Text (TMP)` 선택
2. 삭제 (Delete 키) 또는 비활성화 (Inspector에서 체크박스 해제)
#### 2-2. TabContainer 생성
1. `Top` GameObject 우클릭 → `Create Empty` 선택
2. 이름을 `TabContainer`로 변경
3. Inspector에서 다음 컴포넌트 추가:
**RectTransform 설정**
- Anchor: Middle Left
- Anchor Min: (0, 0.5)
- Anchor Max: (0, 0.5)
- Pivot: (0, 0.5)
- Anchored Position: (20, 0)
- Width: 260 (buttons와 겹치지 않도록)
- Height: 32
**HorizontalLayoutGroup 추가**
`Add Component``Layout``Horizontal Layout Group`
- Padding: Left=0, Right=0, Top=0, Bottom=0
- Spacing: 0 (탭 간격)
- Child Alignment: Middle Left
- Child Controls Size:
- Width: ✓ (체크)
- Height: ✓ (체크)
- Child Force Expand:
- Width: ✗ (체크 해제)
- Height: ✓ (체크)
**Content Size Fitter 추가** (선택 사항)
`Add Component``Layout``Content Size Fitter`
- Horizontal Fit: Preferred Size
- Vertical Fit: Unconstrained
### 3단계: PropertyTabView 컴포넌트 추가
#### 3-1. PropertyWindow GameObject에 컴포넌트 추가
1. Hierarchy에서 최상위 `PropertyWindow` GameObject 선택
2. Inspector에서 `Add Component``PropertyTabView` 검색 및 추가
#### 3-2. PropertyTabView 필드 설정
Inspector에서 PropertyTabView 컴포넌트:
**References**
- `Tab Container`: 방금 만든 `TabContainer` GameObject를 드래그
- `Tab Button Prefab`: `PropertyTabButton.prefab` (생성 후 연결)
**Visual Settings**
- Selected Color: RGB(51, 51, 51) - #333333
- Normal Color: RGB(38, 38, 38) - #262626
- Hover Color: RGB(64, 64, 64) - #404040
### 4단계: PropertyWindow 컴포넌트에 PropertyTabView 연결
#### 4-1. PropertyWindow.cs 스크립트에 필드 추가 필요
(코드 수정은 다음 단계에서 진행)
1. PropertyWindow GameObject 선택
2. Inspector에서 PropertyWindow 컴포넌트 확인
3. `_view` 필드 옆에 `_tabView` 필드가 추가되어 있어야 함
4. `_tabView` 필드에 PropertyTabView 컴포넌트를 드래그
### 5단계: Prefab 저장 및 확인
1. Prefab Edit 모드 종료 (상단의 `<` 버튼 또는 Scene으로 돌아가기)
2. Prefab이 자동 저장됨
## 📐 최종 Hierarchy 구조
```
PropertyWindow
├── Top
│ ├── TabContainer (HorizontalLayoutGroup) ← 새로 추가
│ │ └── (탭 버튼들이 여기에 동적으로 생성됨)
│ └── buttons (기존 닫기 버튼 등)
├── ScrollView (기존 속성 리스트)
└── ...
```
## 🎨 시각적 레이아웃
```
┌─────────────────────────────────────────────┐
│ [Tab1] [Tab2] [Tab3] [X]│ ← Top
│ ↑ TabContainer ↑ buttons │
├─────────────────────────────────────────────┤
│ Property List (ScrollView) │
│ ... │
└─────────────────────────────────────────────┘
```
## ⚠️ 주의사항
1. **TabContainer 위치**: buttons GameObject와 겹치지 않도록 Width를 조정하세요.
2. **Layout Group**: HorizontalLayoutGroup이 제대로 설정되어야 탭 버튼들이 자동으로 배치됩니다.
3. **PropertyTabView 연결**: PropertyWindow.cs에서 _tabView 필드를 추가하고 Initialize 시 연결해야 합니다.
## 🔄 다음 단계
PropertyView.cs를 수정하여 PropertyTabView를 초기화해야 합니다.
(`PropertyView_TAB_INTEGRATION.md` 참조)

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 71a53eeea9e90614b904464661bbcf54
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,104 @@
using System.Collections.Generic;
using UnityEngine;
namespace UVC.Entity
{
/// <summary>
/// Scene 내의 물리적 객체를 나타내는 베이스 클래스입니다.
/// 순수한 데이터 클래스로, UI와 직접적인 의존성이 없습니다.
/// PropertyWindow 연동은 EntityPropertyAdapter를 통해 이루어집니다.
/// 프로세서 컨테이너 역할을 수행합니다.
/// </summary>
public abstract class Entity
{
/// <summary>
/// 엔티티의 고유 ID
/// </summary>
public abstract string Id { get; set; }
/// <summary>
/// 엔티티와 연결된 GameObject
/// </summary>
public abstract GameObject GameObject { get; set; }
/// <summary>
/// 엔티티의 이름
/// </summary>
public virtual string Name
{
get => GameObject != null ? GameObject.name : "Unknown";
set
{
if (GameObject != null)
GameObject.name = value;
}
}
/// <summary>
/// 등록된 프로세서 목록 (프로세서 ID -> 프로세서)
/// </summary>
private readonly Dictionary<string, IEntityProcessor> _processors = new Dictionary<string, IEntityProcessor>();
/// <summary>
/// 프로세서를 등록합니다.
/// </summary>
/// <param name="processor">등록할 프로세서</param>
public void RegisterProcessor(IEntityProcessor processor)
{
if (processor == null)
{
Debug.LogWarning("[Entity] Processor가 null입니다.");
return;
}
if (_processors.ContainsKey(processor.ProcessorId))
{
Debug.LogWarning($"[Entity] 이미 등록된 프로세서 ID입니다: {processor.ProcessorId}");
return;
}
_processors[processor.ProcessorId] = processor;
Debug.Log($"[Entity] 프로세서 등록: {processor.ProcessorId}");
}
/// <summary>
/// 특정 ID의 프로세서를 가져옵니다.
/// </summary>
/// <param name="processorId">프로세서 ID</param>
/// <returns>프로세서 또는 null</returns>
public IEntityProcessor GetProcessor(string processorId)
{
_processors.TryGetValue(processorId, out var processor);
return processor;
}
/// <summary>
/// 특정 타입의 프로세서를 가져옵니다.
/// </summary>
/// <typeparam name="T">프로세서 타입</typeparam>
/// <returns>프로세서 또는 null</returns>
public T GetProcessor<T>() where T : class, IEntityProcessor
{
foreach (var processor in _processors.Values)
{
if (processor is T typedProcessor)
{
return typedProcessor;
}
}
return null;
}
/// <summary>
/// 프로세서를 제거합니다.
/// </summary>
/// <param name="processorId">제거할 프로세서 ID</param>
public void UnregisterProcessor(string processorId)
{
if (_processors.Remove(processorId))
{
Debug.Log($"[Entity] 프로세서 제거: {processorId}");
}
}
}
}

View File

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

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UVC.UI.Window.PropertyWindow;
namespace UVC.Entity
{
/// <summary>
/// Entity에 등록되어 특정 작업을 수행하는 프로세서 인터페이스입니다.
/// </summary>
public interface IEntityProcessor
{
/// <summary>
/// 프로세서의 고유 ID
/// </summary>
string ProcessorId { get; }
/// <summary>
/// 프로세서를 초기화합니다.
/// </summary>
/// <param name="entity">소유 Entity</param>
/// <param name="propertyItems">PropertyItem들의 딕셔너리 (ID -> Item)</param>
void Initialize(Entity entity, Dictionary<string, IPropertyItem> propertyItems);
/// <summary>
/// 프로세서의 주요 작업을 실행합니다.
/// </summary>
UniTask Execute();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5866fa3234b9f204faa5fd13e9a1addb

View File

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

View File

@@ -0,0 +1,285 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UVC.UI.Window.PropertyWindow;
namespace UVC.Entity.Processors
{
/// <summary>
/// Twin Agent의 자동 설정 프로세서입니다.
/// Auto 버튼 클릭 시 순차적으로 네트워크 설정을 진행합니다.
/// </summary>
public class TwinAgentAutoProcessor : IEntityProcessor
{
private Entity _entity;
private Dictionary<string, IPropertyItem> _propertyItems;
public string ProcessorId => "twin_agent_auto";
public void Initialize(Entity entity, Dictionary<string, IPropertyItem> propertyItems)
{
_entity = entity;
_propertyItems = propertyItems;
}
public async UniTask Execute()
{
Debug.Log("[TwinAgentAutoProcessor] Auto 프로세스 시작");
// 0. Read Entity
await RunReadEntity();
// 1. Connection
await RunConnection();
Debug.Log("[TwinAgentAutoProcessor] Auto 프로세스 완료");
}
#region 0. Read Entity
private async UniTask RunReadEntity()
{
// 0-1. Get Entity Info
ShowNext("extract_network_info_status"); // 다음 항목 미리 표시
await RunGetEntityInfo();
// 0-2. Extract Entity Network Info
ShowNext("connecting_status");
await RunExtractEntityNetworkInfo();
// 0-3. Connecting Status
ShowNext("server_status"); // Connection 그룹의 첫 항목
await RunConnectingStatus();
}
private async UniTask RunGetEntityInfo()
{
await UpdateStatus("get_entity_info_status", "Read Entity Meta Data");
await RandomDelay();
await UpdateStatus("get_entity_info_status", "Read Entity Default Data");
await RandomDelay();
await UpdateStatus("get_entity_info_status", "Read Entity Detail Info");
await RandomDelay();
await UpdateStatus("get_entity_info_status", "Read Entity Status");
await RandomDelay();
await UpdateStatus("get_entity_info_status", "Coalescing");
await RandomDelay();
await UpdateStatus("get_entity_info_status", "Generate General Info");
await RandomDelay();
await UpdateStatus("get_entity_info_status", "Done");
}
private async UniTask RunExtractEntityNetworkInfo()
{
await UpdateStatus("extract_network_info_status", "Processing");
await RandomDelay();
await UpdateStatus("extract_network_info_status", "Perusing");
await RandomDelay();
await UpdateStatus("extract_network_info_status", "Inferring");
await RandomDelay();
await UpdateStatus("extract_network_info_status", "Done");
}
private async UniTask RunConnectingStatus()
{
await UpdateStatus("connecting_status", "Crunching");
await RandomDelay();
await UpdateStatus("connecting_status", "Sussing");
await RandomDelay();
await UpdateStatus("connecting_status", "Done");
}
#endregion
#region 1. Connection
private async UniTask RunConnection()
{
// 1-1. Server
ShowNext("port_status");
await RunServer();
// 1-2. Port
ShowNext("protocol_status");
await RunPort();
// 1-3. Protocol
ShowNext("server_status_check");
await RunProtocol();
// 1-4. Status
ShowNext("speed_status");
await RunStatus();
// 1-5. Speed
await RunSpeed();
}
private async UniTask RunServer()
{
await UpdateStatus("server_status", "Spelunking");
await RandomDelay();
await UpdateStatus("server_status", "Inferring");
await RandomDelay();
await UpdateStatus("server_status", "Generate Server List");
await RandomDelay();
await UpdateStatus("server_status", "Forming");
await RandomDelay();
await UpdateStatus("server_status", "Target Server");
await RandomDelay();
await UpdateStatus("server_status", "Done");
}
private async UniTask RunPort()
{
await UpdateStatus("port_status", "Scanning");
await RandomDelay();
await UpdateStatus("port_status", "Port Number");
await RandomDelay();
await UpdateStatus("port_status", "Done");
}
private async UniTask RunProtocol()
{
await UpdateStatus("protocol_status", "Shimmying");
await RandomDelay();
await UpdateStatus("protocol_status", "Transmuting");
await RandomDelay();
await UpdateStatus("protocol_status", "Perusing");
await RandomDelay();
await UpdateStatus("protocol_status", "Protocol Type");
await RandomDelay();
await UpdateStatus("protocol_status", "Done");
}
private async UniTask RunStatus()
{
await UpdateStatus("server_status_check", "ServerStatus");
await RandomDelay();
await UpdateStatus("server_status_check", "Done");
}
private async UniTask RunSpeed()
{
await UpdateStatus("speed_status", "Speed");
await RandomDelay();
await UpdateStatus("speed_status", "Done");
}
#endregion
#region Helper Methods
/// <summary>
/// PropertyItem의 값을 타이핑 효과와 함께 업데이트합니다.
/// </summary>
private async UniTask UpdateStatus(string propertyId, string statusText)
{
if (!_propertyItems.TryGetValue(propertyId, out var item))
return;
// 1. 타이핑 효과 (한 글자씩)
for (int i = 1; i <= statusText.Length; i++)
{
string partial = statusText.Substring(0, i);
item.SetValue(partial);
await UniTask.Delay(30); // 30ms마다 한 글자
}
// 2. "Done"이면 텍스트 색상 초록색으로 변경
if (statusText == "Done" && item is LabelProperty labelProp)
{
labelProp.TextColor = Color.green;
item.SetValue(statusText); // 색상 적용을 위해 다시 설정
}
Debug.Log($"[TwinAgentAutoProcessor] {propertyId}: {statusText}");
}
/// <summary>
/// 다음 진행 항목을 미리 표시합니다.
/// </summary>
private void ShowNext(string nextPropertyId)
{
if (_propertyItems.TryGetValue(nextPropertyId, out var nextItem))
{
nextItem.IsVisible = true;
}
}
/// <summary>
/// 무작위 시간 대기 (1~3초) + 점 애니메이션
/// </summary>
private async UniTask RandomDelay()
{
float delay = Random.Range(1f, 3f);
int totalMs = (int)(delay * 1000);
int dotInterval = 300; // 300ms마다 점 추가
int elapsed = 0;
// 현재 진행 중인 항목 찾기 (마지막으로 업데이트된 항목)
IPropertyItem currentItem = null;
string baseText = "";
foreach (var kvp in _propertyItems)
{
var value = kvp.Value.GetValue();
if (value != null && value.ToString() != "-" && !value.ToString().EndsWith("Done"))
{
currentItem = kvp.Value;
baseText = value.ToString().TrimEnd('.', ' ');
break;
}
}
// 점 애니메이션
if (currentItem != null)
{
int dotCount = 1;
while (elapsed < totalMs)
{
// 점 1~3개 순환
string dots = new string('.', dotCount);
currentItem.SetValue(baseText + dots);
dotCount = (dotCount % 3) + 1;
await UniTask.Delay(dotInterval);
elapsed += dotInterval;
}
// 원래 텍스트로 복원
currentItem.SetValue(baseText);
}
else
{
await UniTask.Delay(totalMs);
}
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7f604aa687fd0334a999a75cad954c0b

View File

@@ -200,8 +200,11 @@ namespace UVC.Studio.Manager
Debug.Log($"[SelectionManager] Selected: {stageObject.GameObject?.name}");
OnSelectionChanged?.Invoke(stageObject, true);
// PropertyWindow에 선택된 객체의 속성 표시
DisplayEquipmentProperties(stageObject);
// PropertyWindow 열기 (엔티티 기반)
if (_propertyWindow != null)
{
_propertyWindow.Open(stageObject);
}
// 기즈모 타겟 업데이트
UpdateGizmoTargets();
@@ -892,7 +895,7 @@ namespace UVC.Studio.Manager
/// <param name="groupIdPrefix">그룹 ID 접두사</param>
/// <param name="order">순서</param>
/// <returns>생성된 PropertyGroup, 속성이 없으면 null</returns>
private static PropertyGroup? CreateStatusSectionGroup(StatusSection section, string groupIdPrefix, int order)
public static PropertyGroup? CreateStatusSectionGroup(StatusSection section, string groupIdPrefix, int order)
{
if (section.properties == null || section.properties.Count == 0)
return null;
@@ -953,7 +956,7 @@ namespace UVC.Studio.Manager
/// <param name="prop">변환할 PropertyItem</param>
/// <param name="order">순서 (선택)</param>
/// <returns>변환된 IPropertyItem</returns>
private static IPropertyItem CreatePropertyItem(PropertyItem prop, int order = 0)
public static IPropertyItem CreatePropertyItem(PropertyItem prop, int order = 0)
{
string id = prop.id ?? Guid.NewGuid().ToString("N")[..8];
string label = prop.label ?? id;

View File

@@ -4,6 +4,8 @@ using System.Collections.Generic;
using RTGLite;
using UnityEngine;
using UVC.Config;
using UVC.Entity;
using UVC.Studio.Manager;
namespace UVC.Object3d.Manager
{
@@ -15,16 +17,16 @@ namespace UVC.Object3d.Manager
/// <summary>
/// 배치된 객체 정보
/// </summary>
public class StageObject
public class StageObject : UVC.Entity.Entity
{
/// <summary>배치된 객체의 고유 ID</summary>
public string Id { get; set; } = string.Empty;
public override string Id { get; set; } = string.Empty;
/// <summary>장비 아이템 정보</summary>
public EquipmentItem Equipment { get; set; } = default!;
/// <summary>인스턴스화된 GameObject</summary>
public GameObject GameObject { get; set; } = default!;
public override GameObject GameObject { get; set; } = default!;
/// <summary>배치 시간</summary>
public DateTime CreatedAt { get; set; } = DateTime.Now;

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// PropertyWindow의 탭 데이터를 담는 클래스입니다.
/// 각 탭은 고유 ID, 표시 이름, 속성 리스트를 가집니다.
/// </summary>
public class PropertyTab
{
/// <summary>
/// 탭의 고유 식별자
/// </summary>
public string Id { get; }
/// <summary>
/// UI에 표시될 탭 이름
/// </summary>
public string Name { get; set; }
/// <summary>
/// 탭에 표시될 속성 항목들의 목록
/// </summary>
public List<IPropertyItem> Properties { get; }
/// <summary>
/// 탭의 정렬 순서 (낮은 값이 먼저 표시됨)
/// </summary>
public int Order { get; set; }
/// <summary>
/// 탭이 활성화되었는지 여부
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// PropertyTab 생성자
/// </summary>
/// <param name="id">탭 고유 ID</param>
/// <param name="name">탭 표시 이름</param>
/// <param name="properties">탭에 표시할 속성 리스트 (null이면 빈 리스트 생성)</param>
/// <param name="order">정렬 순서 (기본값: 0)</param>
public PropertyTab(string id, string name, List<IPropertyItem>? properties = null, int order = 0)
{
if (string.IsNullOrEmpty(id))
throw new ArgumentNullException(nameof(id), "탭 ID는 null이거나 비어있을 수 없습니다.");
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name), "탭 이름은 null이거나 비어있을 수 없습니다.");
Id = id;
Name = name;
Properties = properties ?? new List<IPropertyItem>();
Order = order;
IsActive = false;
}
/// <summary>
/// 탭에 속성 아이템을 추가합니다.
/// </summary>
/// <param name="item">추가할 속성 아이템</param>
public void AddProperty(IPropertyItem item)
{
if (item != null && !Properties.Contains(item))
{
Properties.Add(item);
}
}
/// <summary>
/// 탭에서 속성 아이템을 제거합니다.
/// </summary>
/// <param name="itemId">제거할 아이템의 ID</param>
/// <returns>제거 성공 여부</returns>
public bool RemoveProperty(string itemId)
{
var item = Properties.Find(p => p.Id == itemId);
if (item != null)
{
return Properties.Remove(item);
}
return false;
}
/// <summary>
/// 탭의 모든 속성을 제거합니다.
/// </summary>
public void Clear()
{
Properties.Clear();
}
/// <summary>
/// 탭에 포함된 속성 개수를 반환합니다.
/// </summary>
public int Count => Properties.Count;
public override string ToString()
{
return $"PropertyTab[{Id}]: {Name} ({Count} items, Order: {Order}, Active: {IsActive})";
}
}
/// <summary>
/// 탭이 변경되었을 때 발생하는 이벤트 데이터 클래스입니다.
/// </summary>
public class TabChangedEventArgs : EventArgs
{
/// <summary>
/// 이전에 선택되었던 탭의 ID (null이면 선택된 탭이 없었음)
/// </summary>
public string? OldTabId { get; }
/// <summary>
/// 새로 선택된 탭의 ID
/// </summary>
public string NewTabId { get; }
public TabChangedEventArgs(string? oldTabId, string newTabId)
{
OldTabId = oldTabId;
NewTabId = newTabId;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8b1a8246b284660448bdc6c0298df270

View File

@@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.Entity;
using UVC.Entity.Processors;
using UVC.Object3d.Manager;
using UVC.Studio.Manager;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// Entity를 PropertyWindow용 탭 데이터로 변환하는 Adapter 클래스입니다.
/// Entity와 PropertyWindow 사이의 결합도를 낮추고, 변환 로직을 중앙화합니다.
/// </summary>
public static class EntityPropertyAdapter
{
/// <summary>
/// Entity를 분석하여 PropertyWindow에 표시할 탭 데이터 리스트를 반환합니다.
/// </summary>
/// <param name="entity">변환할 Entity</param>
/// <returns>탭 데이터 리스트</returns>
public static List<EntityTabData> GetTabsForEntity(UVC.Entity.Entity entity)
{
if (entity == null)
{
Debug.LogWarning("[EntityPropertyAdapter] Entity가 null입니다.");
return new List<EntityTabData>();
}
// Entity 타입별로 분기
if (entity is StageObjectManager.StageObject stageObject)
{
return GetTabsForStageObject(stageObject);
}
// 다른 Entity 타입 추가 가능
// if (entity is OtherEntity other)
// return GetTabsForOtherEntity(other);
Debug.LogWarning($"[EntityPropertyAdapter] 지원하지 않는 Entity 타입: {entity.GetType().Name}");
return new List<EntityTabData>();
}
/// <summary>
/// StageObject를 PropertyWindow 탭 데이터로 변환합니다.
/// DisplayEquipmentProperties의 로직을 그대로 사용합니다.
/// </summary>
private static List<EntityTabData> GetTabsForStageObject(StageObjectManager.StageObject stageObject)
{
var tabs = new List<EntityTabData>();
// ========================================
// PROPERTIES 탭 생성
// ========================================
var propertiesTab = CreatePropertiesTab(stageObject);
tabs.Add(propertiesTab);
// ========================================
// NETWORK 탭 생성 (나중에 구현)
// ========================================
var networkTab = CreateNetworkTab(stageObject);
if (networkTab != null)
tabs.Add(networkTab);
return tabs;
}
/// <summary>
/// StageObject의 PROPERTIES 탭 데이터를 생성합니다.
/// DisplayEquipmentProperties와 동일한 로직을 사용합니다.
/// </summary>
private static EntityTabData CreatePropertiesTab(StageObjectManager.StageObject stageObject)
{
var equipment = stageObject.Equipment;
var entries = new List<IPropertyEntry>();
int orderIndex = 0;
// 1. object_name 속성 추가 (수정 가능, 그룹 없이 개별)
var nameProperty = new StringProperty("object_name", "Name",
stageObject.GameObject != null ? stageObject.GameObject.name : "Unknown")
{
IsReadOnly = false,
Order = orderIndex++
};
entries.Add(nameProperty);
// 2. Transform 그룹 추가
if (stageObject.GameObject != null)
{
var transform = stageObject.GameObject.transform;
var transformGroup = new PropertyGroup("transform", "Transform", order: orderIndex++);
transformGroup.AddItems(new IPropertyItem[]
{
new Vector3Property("transform_position", "Position", transform.localPosition),
new Vector3Property("transform_rotation", "Rotation", transform.localEulerAngles),
new Vector3Property("transform_scale", "Scale", transform.localScale)
});
entries.Add(transformGroup);
}
// 3. Equipment의 PropertiesInfo를 PropertyGroup으로 변환
if (equipment?.propertiesInfo != null)
{
foreach (var propInfo in equipment.propertiesInfo)
{
// section이 "root"이면 그룹 없이 개별 등록
if (string.Equals(propInfo.section, "root", StringComparison.OrdinalIgnoreCase))
{
foreach (var prop in propInfo.properties)
{
var propertyItem = SelectionManager.CreatePropertyItem(prop, orderIndex++);
if (propertyItem != null)
{
entries.Add(propertyItem);
}
}
}
else
{
// 일반 섹션은 그룹으로 묶음
var group = new PropertyGroup(
$"section_{propInfo.section}",
propInfo.section ?? "Properties",
order: orderIndex++
);
foreach (var prop in propInfo.properties)
{
var propertyItem = SelectionManager.CreatePropertyItem(prop);
if (propertyItem != null)
{
group.AddItem(propertyItem);
}
}
if (group.Count > 0)
{
entries.Add(group);
}
}
}
}
// 4. StatusInfo 추가
if (equipment?.statusInfo != null)
{
// Network 상태 섹션
if (equipment.statusInfo.network != null)
{
var networkGroup = SelectionManager.CreateStatusSectionGroup(
equipment.statusInfo.network,
"status_network",
orderIndex++
);
if (networkGroup != null)
{
entries.Add(networkGroup);
}
}
// Equipment 상태 섹션
if (equipment.statusInfo.equipment != null)
{
var equipmentGroup = SelectionManager.CreateStatusSectionGroup(
equipment.statusInfo.equipment,
"status_equipment",
orderIndex++
);
if (equipmentGroup != null)
{
entries.Add(equipmentGroup);
}
}
}
// IPropertyEntry를 IPropertyItem으로 변환
var properties = new List<IPropertyItem>();
foreach (var entry in entries)
{
if (entry is IPropertyItem item)
{
properties.Add(item);
}
else if (entry is IPropertyGroup group)
{
properties.AddRange(group.Items);
}
}
return new EntityTabData("properties", "PROPERTIES", properties, order: 0);
}
/// <summary>
/// StageObject의 NETWORK 탭 데이터를 생성합니다.
/// (나중에 구현)
/// </summary>
private static EntityTabData? CreateNetworkTab(StageObjectManager.StageObject stageObject)
{
var properties = new List<IPropertyItem>();
var propertyDict = new Dictionary<string, IPropertyItem>(); // Processor 초기화용
int orderIndex = 0;
// ========================================
// 1. Server Connection 그룹 (기본 설정)
// ========================================
var serverGroup = new PropertyGroup("server_connection", "Server Connection", order: orderIndex++);
var serverTypeItems = new List<string> { "Twin Agent", "Octopus Hub", "Octopus AI" };
var serverTypeProperty = new ListProperty("server_type", "Server Type", serverTypeItems, "Twin Agent")
{ IsReadOnly = false };
var hostProperty = new StringProperty("server_host", "Server Host", "192.168.0.1")
{ IsReadOnly = false, IsVisible = false };
var portProperty = new IntProperty("server_port", "Port", 1883)
{ IsReadOnly = false, IsVisible = false };
serverGroup.AddItems(new IPropertyItem[] { serverTypeProperty, hostProperty, portProperty });
properties.AddRange(serverGroup.Items);
// ========================================
// 2. Twin Agent 설정
// ========================================
var twinAgentGroup = new PropertyGroup("twin_agent_details", "Twin Agent Settings", order: orderIndex++);
var autoButton = new ButtonProperty("auto_button", "Auto", "Run");
twinAgentGroup.AddItem(autoButton);
properties.AddRange(twinAgentGroup.Items);
// Twin Agent 진행 상태 PropertyItem들 생성
CreateTwinAgentProgressItems(properties, propertyDict, ref orderIndex, stageObject);
// Processor 생성 및 등록
var processor = new TwinAgentAutoProcessor();
processor.Initialize(stageObject, propertyDict);
stageObject.RegisterProcessor(processor);
// Auto 버튼 클릭 시 Processor 실행
autoButton.Clicked += async () =>
{
await processor.Execute();
};
// ========================================
// 3. Octopus Hub 설정
// ========================================
var octopusHubGroup = new PropertyGroup("octopus_hub_details", "Octopus Hub Settings", order: orderIndex++);
var connectionTypeProperty = new ListProperty("connection_type", "Connection Type",
new List<string> { "MQTT", "API" }, "MQTT")
{ IsReadOnly = false, IsVisible = false };
// TODO: Topics는 나중에 EditableList PropertyType으로 교체 필요
var topicsProperty = new StringProperty("topics", "Topics", "topic1, topic2, topic3")
{ IsReadOnly = false, IsVisible = false };
var uriProperty = new StringProperty("uri", "URI", "http://api.example.com")
{ IsReadOnly = false, IsVisible = false };
var periodProperty = new IntProperty("period", "Period", 1000)
{ IsReadOnly = false, IsVisible = false };
connectionTypeProperty.ValueChanged += (oldValue, newValue) =>
{
bool isMQTT = newValue == "MQTT";
topicsProperty.IsVisible = isMQTT;
uriProperty.IsVisible = !isMQTT;
periodProperty.IsVisible = !isMQTT;
};
octopusHubGroup.AddItems(new IPropertyItem[]
{
connectionTypeProperty, topicsProperty, uriProperty, periodProperty
});
properties.AddRange(octopusHubGroup.Items);
// ========================================
// 4. Octopus AI 설정
// ========================================
var octopusAIGroup = new PropertyGroup("octopus_ai_details", "Octopus AI Settings", order: orderIndex++);
var agentTypeProperty = new ListProperty("agent_type", "Agent Type",
new List<string> { "Optimizer", "DefectChecker" }, "Optimizer")
{ IsReadOnly = false, IsVisible = false };
var aiUriProperty = new StringProperty("ai_uri", "URI", "http://ai.example.com")
{ IsReadOnly = false, IsVisible = false };
octopusAIGroup.AddItems(new IPropertyItem[] { agentTypeProperty, aiUriProperty });
properties.AddRange(octopusAIGroup.Items);
// ========================================
// 동적 가시성 제어
// ========================================
serverTypeProperty.ValueChanged += (oldValue, newValue) =>
{
bool isTwinAgent = newValue == "Twin Agent";
bool isOctopusHub = newValue == "Octopus Hub";
bool isOctopusAI = newValue == "Octopus AI";
hostProperty.IsVisible = !isTwinAgent;
portProperty.IsVisible = !isTwinAgent;
autoButton.IsVisible = isTwinAgent;
// Twin Agent 진행 상태 항목들
foreach (var kvp in propertyDict)
{
kvp.Value.IsVisible = isTwinAgent;
}
connectionTypeProperty.IsVisible = isOctopusHub;
if (isOctopusHub)
{
bool isMQTT = connectionTypeProperty.Value == "MQTT";
topicsProperty.IsVisible = isMQTT;
uriProperty.IsVisible = !isMQTT;
periodProperty.IsVisible = !isMQTT;
}
else
{
topicsProperty.IsVisible = false;
uriProperty.IsVisible = false;
periodProperty.IsVisible = false;
}
agentTypeProperty.IsVisible = isOctopusAI;
aiUriProperty.IsVisible = isOctopusAI;
};
return new EntityTabData("network", "NETWORK", properties, order: 1);
}
private static void CreateTwinAgentProgressItems(
List<IPropertyItem> properties,
Dictionary<string, IPropertyItem> propertyDict,
ref int orderIndex,
StageObjectManager.StageObject stageObject)
{
// 0. Read Entity 그룹
var readEntityGroup = new PropertyGroup("read_entity", "Read Entity", order: orderIndex++);
// 0-1. Get Entity Info 상태
var getEntityInfoStatus = new LabelProperty("get_entity_info_status", "Get Entity Info", "-");
readEntityGroup.AddItem(getEntityInfoStatus);
propertyDict["get_entity_info_status"] = getEntityInfoStatus;
// 0-2. Extract Entity Network Info 상태
var extractNetworkStatus = new LabelProperty("extract_network_info_status", "Extract Network Info", "-")
{ IsVisible = false };
readEntityGroup.AddItem(extractNetworkStatus);
propertyDict["extract_network_info_status"] = extractNetworkStatus;
// 0-3. Connecting Status
var connectingStatus = new LabelProperty("connecting_status", "Connecting Status", "-")
{ IsVisible = false };
readEntityGroup.AddItem(connectingStatus);
propertyDict["connecting_status"] = connectingStatus;
properties.AddRange(readEntityGroup.Items);
// 1. Connection 그룹
var connectionGroup = new PropertyGroup("connection", "Connection", order: orderIndex++);
// 1-1. Server
var serverStatus = new LabelProperty("server_status", "Server", "-")
{ IsVisible = false };
connectionGroup.AddItem(serverStatus);
propertyDict["server_status"] = serverStatus;
// 1-2. Port
var portStatus = new LabelProperty("port_status", "Port", "-")
{ IsVisible = false };
connectionGroup.AddItem(portStatus);
propertyDict["port_status"] = portStatus;
// 1-3. Protocol
var protocolStatus = new LabelProperty("protocol_status", "Protocol", "-")
{ IsVisible = false };
connectionGroup.AddItem(protocolStatus);
propertyDict["protocol_status"] = protocolStatus;
// 1-4. Status
var serverStatusCheck = new LabelProperty("server_status_check", "Status", "-")
{ IsVisible = false };
connectionGroup.AddItem(serverStatusCheck);
propertyDict["server_status_check"] = serverStatusCheck;
// 1-5. Speed
var speedStatus = new LabelProperty("speed_status", "Speed", "-")
{ IsVisible = false };
connectionGroup.AddItem(speedStatus);
propertyDict["speed_status"] = speedStatus;
properties.AddRange(connectionGroup.Items);
}
}
}

View File

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

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// Entity가 PropertyWindow에 제공하는 탭 데이터입니다.
/// TabId, TabName, Properties를 포함합니다.
/// </summary>
public class EntityTabData
{
/// <summary>
/// 탭 고유 ID (예: "properties", "network")
/// </summary>
public string TabId { get; set; } = string.Empty;
/// <summary>
/// UI에 표시될 탭 이름 (예: "PROPERTIES", "NETWORK")
/// </summary>
public string TabName { get; set; } = string.Empty;
/// <summary>
/// 탭에 표시될 속성 아이템 리스트
/// </summary>
public List<IPropertyItem> Properties { get; set; } = new List<IPropertyItem>();
/// <summary>
/// 탭의 정렬 순서 (낮은 값이 먼저 표시됨)
/// </summary>
public int Order { get; set; } = 0;
public EntityTabData() { }
public EntityTabData(string tabId, string tabName, List<IPropertyItem> properties, int order = 0)
{
TabId = tabId;
TabName = tabName;
Properties = properties ?? new List<IPropertyItem>();
Order = order;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6bd468aa396e86a4aabe526ec12b8b78

View File

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

View File

@@ -0,0 +1,243 @@
using System.Collections.Generic;
using UnityEngine;
namespace UVC.UI.Window.PropertyWindow.Examples
{
/// <summary>
/// PropertyWindow 탭 시스템 사용 예제
/// </summary>
public class PropertyWindowTabExample : MonoBehaviour
{
[SerializeField] private PropertyWindow propertyWindow;
private void Start()
{
if (propertyWindow == null)
{
Debug.LogError("[PropertyWindowTabExample] PropertyWindow가 설정되지 않았습니다.");
return;
}
// 예제 실행
SetupTabsExample();
}
/// <summary>
/// 탭 시스템 기본 사용 예제
/// </summary>
private void SetupTabsExample()
{
// ========================================
// 방법 1: 기본 탭 초기화 (PROPERTIES, NETWORK)
// ========================================
// 기본 탭에 속성 추가
propertyWindow.AddPropertiesToTab("properties", CreatePropertiesData());
propertyWindow.AddPropertiesToTab("network", CreateNetworkData());
// ========================================
// 방법 2: 추가 탭 생성 (Equipment Data)
// ========================================
var equipmentTab = propertyWindow.AddTab(
id: "equipment",
name: "Equipment Data",
properties: CreateEquipmentData(),
order: 2
);
// ========================================
// 4. 탭 변경 이벤트 구독
// ========================================
propertyWindow.TabChanged += OnTabChanged;
// ========================================
// 5. 특정 탭 선택 (선택 사항)
// ========================================
// 첫 번째 탭은 자동으로 선택되므로 필요시에만 호출
// propertyWindow.SelectTab("equipment");
Debug.Log("[PropertyWindowTabExample] 탭 시스템 초기화 완료");
Debug.Log($"- 총 탭 개수: {propertyWindow.Tabs.Count}");
Debug.Log($"- 현재 선택된 탭: {propertyWindow.CurrentTabId}");
}
/// <summary>
/// PROPERTIES 탭 데이터 생성
/// </summary>
private List<IPropertyItem> CreatePropertiesData()
{
var properties = new List<IPropertyItem>();
// 예제: String 속성
properties.Add(new StringProperty(
id: "name",
name: "Name",
initialValue: "QPG CAP FILL"
));
// 예제: Vector3 속성 (Transform)
properties.Add(new Vector3Property(
id: "position",
name: "Position",
initialValue: new Vector3(-41.592f, 0, 7.84f)
));
properties.Add(new Vector3Property(
id: "rotation",
name: "Rotation",
initialValue: Vector3.zero
));
properties.Add(new Vector3Property(
id: "scale",
name: "Scale",
initialValue: Vector3.one
));
// 예제: Int 속성
properties.Add(new IntProperty(
id: "drivingSpeed",
name: "Driving Speed (cm/s)",
initialValue: 50
));
return properties;
}
/// <summary>
/// Equipment Data 탭 데이터 생성
/// </summary>
private List<IPropertyItem> CreateEquipmentData()
{
var properties = new List<IPropertyItem>();
properties.Add(new StringProperty(
id: "equipmentId",
name: "Equipment ID",
initialValue: "EQ-001"
));
properties.Add(new StringProperty(
id: "manufacturer",
name: "Manufacturer",
initialValue: "ABC Company"
));
properties.Add(new IntProperty(
id: "repeatSpacing",
name: "Repeat Spacing (cm)",
initialValue: 200
));
properties.Add(new IntProperty(
id: "repeatAxis",
name: "Repeat Axis",
initialValue: 0
));
return properties;
}
/// <summary>
/// Network AI 탭 데이터 생성
/// </summary>
private List<IPropertyItem> CreateNetworkData()
{
var properties = new List<IPropertyItem>();
properties.Add(new StringProperty(
id: "serverConnection",
name: "Server Connection",
initialValue: "Connected"
)
{
IsReadOnly = true
});
properties.Add(new IntProperty(
id: "conveyorSpeed",
name: "Conveyor Speed (cm/s)",
initialValue: 200
));
properties.Add(new IntProperty(
id: "width",
name: "Width (cm)",
initialValue: 150
));
properties.Add(new IntProperty(
id: "height",
name: "Height (cm)",
initialValue: 300
));
properties.Add(new IntProperty(
id: "length",
name: "Length (cm)",
initialValue: 200
));
return properties;
}
/// <summary>
/// 탭 변경 이벤트 핸들러
/// </summary>
private void OnTabChanged(object sender, TabChangedEventArgs e)
{
Debug.Log($"[PropertyWindowTabExample] 탭 변경: {e.OldTabId} → {e.NewTabId}");
// 탭별 추가 처리가 필요한 경우 여기에 작성
switch (e.NewTabId)
{
case "properties":
Debug.Log("Properties 탭 활성화");
break;
case "equipment":
Debug.Log("Equipment Data 탭 활성화");
break;
case "network":
Debug.Log("Network AI 탭 활성화");
break;
}
}
/// <summary>
/// 탭 추가/제거 예제 (런타임)
/// </summary>
[ContextMenu("Add Custom Tab")]
private void AddCustomTab()
{
var customProperties = new List<IPropertyItem>
{
new StringProperty("custom1", "Custom Property 1", "Value 1"),
new IntProperty("custom2", "Custom Property 2", 100)
};
propertyWindow.AddTab("custom", "Custom Tab", customProperties, order: 99);
Debug.Log("[PropertyWindowTabExample] Custom 탭 추가됨");
}
[ContextMenu("Remove Equipment Tab")]
private void RemoveEquipmentTab()
{
bool removed = propertyWindow.RemoveTab("equipment");
Debug.Log($"[PropertyWindowTabExample] Equipment 탭 제거: {removed}");
}
[ContextMenu("Select Network Tab")]
private void SelectNetworkTab()
{
propertyWindow.SelectTab("network");
}
private void OnDestroy()
{
if (propertyWindow != null)
{
propertyWindow.TabChanged -= OnTabChanged;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5c44fafcc4bb3364ba2848a8542ef5a4

View File

@@ -28,6 +28,8 @@ namespace UVC.UI.Window.PropertyWindow
DateRange,
DateTimeRange,
ColorState,
Button,
Label,
}
/// <summary>
@@ -95,6 +97,21 @@ namespace UVC.UI.Window.PropertyWindow
/// </summary>
bool IsReadOnly { get; set; }
/// <summary>
/// UI에서 표시 여부. false이면 UI에 표시되지 않습니다.
/// </summary>
bool IsVisible { get; set; }
/// <summary>
/// IsVisible 값이 변경되었을 때 발생하는 이벤트입니다.
/// </summary>
event Action<bool>? IsVisibleChanged;
/// <summary>
/// 값이 변경되었을 때 발생하는 이벤트입니다. (object 타입, UI 갱신용)
/// </summary>
event Action<object, object>? ValueChangedObject;
/// <summary>
/// 속성의 현재 값 (object 타입)
/// </summary>
@@ -130,6 +147,30 @@ namespace UVC.UI.Window.PropertyWindow
public string Description { get; set; }
public string Tooltip { get; set; }
public bool IsReadOnly { get; set; } = false;
private bool _isVisible = true;
/// <summary>
/// IsVisible 값이 변경되었을 때 발생하는 이벤트입니다.
/// </summary>
public event Action<bool>? IsVisibleChanged;
/// <summary>
/// UI에서 표시 여부. false이면 UI에 표시되지 않습니다.
/// </summary>
public bool IsVisible
{
get => _isVisible;
set
{
if (_isVisible != value) // 값이 변경되었을 때만 (재귀 방지)
{
_isVisible = value;
IsVisibleChanged?.Invoke(value);
}
}
}
public abstract PropertyType PropertyType { get; }
/// <summary>
@@ -145,6 +186,17 @@ namespace UVC.UI.Window.PropertyWindow
/// <summary>
/// 실제 데이터가 저장되는 필드
/// </summary>
/// <summary>
/// 값이 변경되었을 때 발생하는 이벤트입니다. (oldValue, newValue)
/// </summary>
public event Action<T, T>? ValueChanged;
/// <summary>
/// 값이 변경되었을 때 발생하는 이벤트입니다. (object 타입, UI 갱신용)
/// </summary>
public event Action<object, object>? ValueChangedObject;
protected T _value;
/// <summary>
@@ -167,10 +219,18 @@ namespace UVC.UI.Window.PropertyWindow
public object GetValue() => _value;
public void SetValue(object value)
{
// 타입 안정성을 위해 캐스팅 시도
T oldValue = _value;
// 타입 안정성을 위해 캠스팅 시도
if (value is T typedValue)
{
_value = typedValue;
// ValueChanged 이벤트 발생 (제네릭)
ValueChanged?.Invoke(oldValue, typedValue);
// ValueChangedObject 이벤트 발생 (object 타입, UI 갱신용)
ValueChangedObject?.Invoke(oldValue, typedValue);
}
else
{
@@ -339,6 +399,51 @@ namespace UVC.UI.Window.PropertyWindow
public ColorStateProperty(string id, string name, Tuple<string, Color?> initialValue) : base(id, name, initialValue) { }
}
// --- 버튼 타입 속성 ---
public class ButtonProperty : PropertyItem<object> // 값은 사용하지 않음
{
public override PropertyType PropertyType => PropertyType.Button;
/// <summary>
/// 버튼에 표시될 텍스트입니다.
/// </summary>
public string ButtonText { get; set; }
/// <summary>
/// 버튼이 클릭되었을 때 발생하는 이벤트입니다.
/// </summary>
public event Action? Clicked;
public ButtonProperty(string id, string name, string buttonText = "Execute") : base(id, name, null)
{
ButtonText = buttonText;
}
/// <summary>
/// 버튼 클릭을 트리거합니다. UI에서 호출됩니다.
/// </summary>
public void Click()
{
Clicked?.Invoke();
}
}
// --- 라벨 타입 속성 (읽기 전용 텍스트 표시) ---
public class LabelProperty : PropertyItem<string>
{
public override PropertyType PropertyType => PropertyType.Label;
/// <summary>
/// 텍스트 색상
/// </summary>
public Color TextColor { get; set; } = Color.white;
public LabelProperty(string id, string name, string initialValue) : base(id, name, initialValue)
{
IsReadOnly = true; // 항상 읽기 전용
}
}
#endregion
}

View File

@@ -36,6 +36,8 @@ namespace UVC.UI.Window.PropertyWindow
[SerializeField] private GameObject _dateRangePropertyPrefab;
[SerializeField] private GameObject _dateTimeRangePropertyPrefab;
[SerializeField] private GameObject _colorStatePropertyPrefab;
[SerializeField] private GameObject _buttonPropertyPrefab;
[SerializeField] private GameObject _labelPropertyPrefab;
/// <summary>
/// View가 상호작용할 Controller 인스턴스입니다.
@@ -148,7 +150,7 @@ namespace UVC.UI.Window.PropertyWindow
/// <summary>
/// 개별 속성 아이템 UI를 생성합니다.
/// </summary>
private void DrawPropertyItem(IPropertyItem item, Transform container)
private void DrawPropertyItem(IPropertyItem item, Transform container)
{
GameObject prefab = GetPrefabForProperty(item.PropertyType);
if (prefab != null)
@@ -160,16 +162,19 @@ namespace UVC.UI.Window.PropertyWindow
{
propertyUI.Setup(item, _controller);
_itemViews[item.Id] = uiInstance;
// IsVisible에 따라 초기 활성화 상태 설정 (UI는 항상 생성)
uiInstance.SetActive(item.IsVisible);
}
else
{
Debug.LogError($"[PropertyView] 프리 '{prefab.name}'에 IPropertyUI를 구현한 스크립트가 없습니다.");
Debug.LogError($"[PropertyView] 프리 '{prefab.name}'에 IPropertyUI를 구현한 스크립트가 없습니다.");
Destroy(uiInstance);
}
}
else
{
Debug.LogWarning($"[PropertyView] '{item.PropertyType}' 타입에 대한 UI 프리이 지정되지 않았습니다.");
Debug.LogWarning($"[PropertyView] '{item.PropertyType}' 타입에 대한 UI 프리이 지정되지 않았습니다.");
}
}
@@ -294,6 +299,10 @@ namespace UVC.UI.Window.PropertyWindow
return _dateTimeRangePropertyPrefab;
case PropertyType.ColorState:
return _colorStatePropertyPrefab;
case PropertyType.Button:
return _buttonPropertyPrefab;
case PropertyType.Label:
return _labelPropertyPrefab;
default:
Debug.LogWarning($"'{type}' 타입에 대한 프리팹이 정의되지 않았습니다.");
return null;

View File

@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UVC.Entity;
namespace UVC.UI.Window.PropertyWindow
{
@@ -17,6 +18,9 @@ namespace UVC.UI.Window.PropertyWindow
[SerializeField]
private PropertyView _view;
[SerializeField]
private PropertyTabView _tabView;
#region Internal Data Structures
/// <summary>
@@ -34,6 +38,16 @@ namespace UVC.UI.Window.PropertyWindow
/// </summary>
private readonly Dictionary<string, IPropertyItem> _itemIndex = new Dictionary<string, IPropertyItem>();
/// <summary>
/// 탭 목록 (탭 ID -> PropertyTab)
/// </summary>
private readonly Dictionary<string, PropertyTab> _tabs = new Dictionary<string, PropertyTab>();
/// <summary>
/// 현재 선택된 탭의 ID
/// </summary>
private string? _currentTabId = null;
#endregion
#region Public Properties
@@ -72,6 +86,21 @@ namespace UVC.UI.Window.PropertyWindow
/// </summary>
public IReadOnlyList<IPropertyGroup> Groups => _groupIndex.Values.ToList().AsReadOnly();
/// <summary>
/// 모든 탭의 읽기 전용 목록을 반환합니다.
/// </summary>
public IReadOnlyList<PropertyTab> Tabs => _tabs.Values.OrderBy(t => t.Order).ToList().AsReadOnly();
/// <summary>
/// 현재 선택된 탭의 ID를 반환합니다.
/// </summary>
public string? CurrentTabId => _currentTabId;
/// <summary>
/// 현재 선택된 탭을 반환합니다.
/// </summary>
public PropertyTab? CurrentTab => _currentTabId != null && _tabs.TryGetValue(_currentTabId, out var tab) ? tab : null;
#endregion
#region Events
@@ -111,6 +140,11 @@ namespace UVC.UI.Window.PropertyWindow
/// </summary>
public event EventHandler? EntriesCleared;
/// <summary>
/// 탭이 변경되었을 때 발생하는 이벤트입니다.
/// </summary>
public event EventHandler<TabChangedEventArgs>? TabChanged;
#endregion
#region Load Methods ( + + )
@@ -386,6 +420,237 @@ namespace UVC.UI.Window.PropertyWindow
return item;
}
#endregion
#region Tab Management
/// <summary>
/// 새로운 탭을 추가합니다.
/// 첫 번째 탭인 경우 자동으로 선택됩니다.
/// </summary>
/// <param name="id">탭 고유 ID</param>
/// <param name="name">탭 표시 이름</param>
/// <param name="properties">탭에 표시할 속성 리스트</param>
/// <param name="order">정렬 순서 (기본값: 0)</param>
/// <returns>생성된 PropertyTab 인스턴스</returns>
public PropertyTab AddTab(string id, string name, List<IPropertyItem> properties, int order = 0)
{
if (string.IsNullOrEmpty(id))
throw new ArgumentNullException(nameof(id));
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
if (_tabs.ContainsKey(id))
{
Debug.LogWarning($"[PropertyWindow] 이미 존재하는 탭 ID입니다: {id}");
return _tabs[id];
}
var tab = new PropertyTab(id, name, properties, order);
_tabs[id] = tab;
// TabView 업데이트
if (_tabView != null)
{
_tabView.RefreshTabs();
}
// 첫 번째 탭이면 자동으로 선택
if (_tabs.Count == 1)
{
SelectTab(id);
}
return tab;
}
/// <summary>
/// 특정 ID의 탭을 선택하고 해당 탭의 속성을 로드합니다.
/// </summary>
/// <param name="tabId">선택할 탭의 ID</param>
/// <returns>선택 성공 여부</returns>
public bool SelectTab(string tabId)
{
if (!_tabs.TryGetValue(tabId, out var tab))
{
Debug.LogWarning($"[PropertyWindow] 탭을 찾을 수 없습니다: {tabId}");
return false;
}
// 이미 선택된 탭이면 아무것도 하지 않음 (콘텐츠 보존)
if (_currentTabId == tabId)
{
return true;
}
string? oldTabId = _currentTabId;
// 이전 탭 비활성화
if (oldTabId != null && _tabs.TryGetValue(oldTabId, out var oldTab))
{
oldTab.IsActive = false;
}
// 새 탭 활성화
_currentTabId = tabId;
tab.IsActive = true;
// 탭의 속성 로드
LoadProperties(tab.Properties);
// 이벤트 발생
TabChanged?.Invoke(this, new TabChangedEventArgs(oldTabId, tabId));
return true;
}
/// <summary>
/// 특정 ID의 탭을 제거합니다.
/// 현재 선택된 탭이 제거되면 첫 번째 탭이 자동으로 선택됩니다.
/// </summary>
/// <param name="tabId">제거할 탭의 ID</param>
/// <returns>제거 성공 여부</returns>
public bool RemoveTab(string tabId)
{
if (!_tabs.TryGetValue(tabId, out var tab))
{
return false;
}
_tabs.Remove(tabId);
// TabView에서 버튼 제거
if (_tabView != null)
{
_tabView.RemoveTabButton(tabId);
}
// 현재 선택된 탭이 제거되었다면
if (_currentTabId == tabId)
{
_currentTabId = null;
// 남은 탭 중 첫 번째를 선택
if (_tabs.Count > 0)
{
var firstTab = _tabs.Values.OrderBy(t => t.Order).First();
SelectTab(firstTab.Id);
}
else
{
// 탭이 하나도 없으면 속성 클리어
Clear();
}
}
return true;
}
/// <summary>
/// 특정 ID의 탭을 가져옵니다.
/// </summary>
/// <param name="tabId">탭 ID</param>
/// <returns>탭 또는 null</returns>
public PropertyTab? GetTab(string tabId)
{
_tabs.TryGetValue(tabId, out var tab);
return tab;
}
/// <summary>
/// 모든 탭을 제거합니다.
/// </summary>
public void ClearTabs()
{
_tabs.Clear();
_currentTabId = null;
Clear();
}
/// <summary>
/// 특정 탭에 속성을 추가합니다.
/// 탭이 없으면 자동으로 생성됩니다.
/// </summary>
/// <param name="tabId">탭 ID</param>
/// <param name="property">추가할 속성</param>
public void AddPropertyToTab(string tabId, IPropertyItem property)
{
if (!_tabs.TryGetValue(tabId, out var tab))
{
Debug.LogWarning($"[PropertyWindow] 탭을 찾을 수 없습니다: {tabId}");
return;
}
tab.AddProperty(property);
// 현재 선택된 탭이면 UI 갱신
if (_currentTabId == tabId)
{
LoadProperties(tab.Properties);
}
}
/// <summary>
/// 특정 탭에 여러 속성을 추가합니다.
/// </summary>
/// <param name="tabId">탭 ID</param>
/// <param name="properties">추가할 속성 리스트</param>
public void AddPropertiesToTab(string tabId, List<IPropertyItem> properties)
{
if (!_tabs.TryGetValue(tabId, out var tab))
{
Debug.LogWarning($"[PropertyWindow] 탭을 찾을 수 없습니다: {tabId}");
return;
}
foreach (var property in properties)
{
tab.AddProperty(property);
}
// 현재 선택된 탭이면 UI 갱신
if (_currentTabId == tabId)
{
LoadProperties(tab.Properties);
}
}
/// <summary>
/// 엔티티를 기반으로 PropertyWindow를 열고 탭을 구성합니다.
/// EntityPropertyAdapter를 통해 엔티티의 탭 데이터를 가져옵니다.
/// </summary>
/// <param name="entity">표시할 엔티티</param>
public void Open(UVC.Entity.Entity entity)
{
if (entity == null)
{
Debug.LogWarning("[PropertyWindow] Entity가 null입니다.");
return;
}
// 기존 탭 클리어
ClearTabs();
// Adapter를 통해 Entity → 탭 데이터 변환
var tabsData = EntityPropertyAdapter.GetTabsForEntity(entity);
// 탭 생성
foreach (var tabData in tabsData)
{
AddTab(tabData.TabId, tabData.TabName, tabData.Properties, tabData.Order);
}
// Window 표시
Show();
Debug.Log($"[PropertyWindow] Opened for entity: {entity.Name} (탭 개수: {Tabs.Count})");
}
/// <summary>
/// NETWORK 탭의 Server Type 변경 시 동적 가시성 제어를 설정합니다.
/// </summary>
#endregion
#region Clear and Refresh
@@ -417,6 +682,11 @@ namespace UVC.UI.Window.PropertyWindow
{
_view.Initialize(this);
}
if (_tabView != null)
{
_tabView.Initialize(this);
_tabView.RefreshTabs();
}
}
#endregion
@@ -495,7 +765,7 @@ namespace UVC.UI.Window.PropertyWindow
/// <summary>
/// 엔트리를 내부 컬렉션에 추가합니다.
/// </summary>
private void AddEntryInternal(IPropertyEntry entry)
private void AddEntryInternal(IPropertyEntry entry)
{
if (entry is IPropertyGroup group)
{
@@ -504,10 +774,25 @@ namespace UVC.UI.Window.PropertyWindow
_groupIndex[group.GroupId] = group;
_entries.Add(group);
// 그룹 내 아이템들도 인덱스에 추가
// 그룹 내 아이템들도 인덱스에 추가 및 이벤트 등록
foreach (var item in group.Items)
{
_itemIndex[item.Id] = item;
// IsVisibleChanged 이벤트 등록 (재귀 방지)
item.IsVisibleChanged += (visible) =>
{
SetPropertyVisibility(item.Id, visible);
};
// ValueChangedObject 이벤트 등록 (UI 갱신)
item.ValueChangedObject += (oldValue, newValue) =>
{
if (_view != null)
{
_view.UpdatePropertyValue(item.Id, newValue);
}
};
}
}
}
@@ -516,6 +801,22 @@ namespace UVC.UI.Window.PropertyWindow
if (!_itemIndex.ContainsKey(item.Id))
{
_itemIndex[item.Id] = item;
// IsVisibleChanged 이벤트 등록 (재귀 방지)
item.IsVisibleChanged += (visible) =>
{
SetPropertyVisibility(item.Id, visible);
};
// ValueChangedObject 이벤트 등록 (UI 갱신)
item.ValueChangedObject += (oldValue, newValue) =>
{
if (_view != null)
{
_view.UpdatePropertyValue(item.Id, newValue);
}
};
if (item.GroupId == null)
{
_entries.Add(item);
@@ -728,5 +1029,4 @@ namespace UVC.UI.Window.PropertyWindow
}
#endregion
}
}
}}

View File

@@ -0,0 +1,246 @@
# PropertyWindow 탭 시스템
## 📋 개요
PropertyWindow에 탭 시스템이 추가되었습니다. 이제 여러 탭을 추가하고 탭별로 다른 속성 리스트를 표시할 수 있습니다.
## ✨ 주요 기능
- ✅ 탭 동적 추가/제거
- ✅ 탭 클릭으로 전환
- ✅ 탭별 독립적인 속성 리스트
- ✅ 첫 번째 탭 자동 선택
- ✅ 탭 변경 이벤트
- ✅ 시각적 선택 상태 표시
## 🎯 시각적 결과
```
┌─────────────────────────────────────────────┐
│ [PROPERTIES] [Equipment Data] [Network AI] X│ ← 탭 버튼들
├─────────────────────────────────────────────┤
│ Name: QPG CAP FILL │
│ Position: (-41.592, 0, 7.84) │
│ ... │
└─────────────────────────────────────────────┘
```
## 🚀 사용 방법
### 기본 사용
```csharp
// 1. PropertyWindow 참조 가져오기
PropertyWindow propertyWindow = GetComponent<PropertyWindow>();
// 2. 탭 추가
propertyWindow.AddTab(
id: "properties",
name: "PROPERTIES",
properties: propertiesList
);
propertyWindow.AddTab(
id: "equipment",
name: "Equipment Data",
properties: equipmentList
);
propertyWindow.AddTab(
id: "network",
name: "Network AI",
properties: networkList
);
// 3. 이벤트 구독
propertyWindow.TabChanged += (sender, e) => {
Debug.Log($"탭 변경: {e.OldTabId} → {e.NewTabId}");
};
```
### 탭 전환
```csharp
// 특정 탭 선택
propertyWindow.SelectTab("equipment");
// 현재 선택된 탭 확인
string currentTabId = propertyWindow.CurrentTabId;
PropertyTab currentTab = propertyWindow.CurrentTab;
```
### 탭 제거
```csharp
// 특정 탭 제거
propertyWindow.RemoveTab("equipment");
// 모든 탭 제거
propertyWindow.ClearTabs();
```
### 탭 정보 조회
```csharp
// 특정 탭 가져오기
PropertyTab tab = propertyWindow.GetTab("properties");
// 모든 탭 목록
IReadOnlyList<PropertyTab> tabs = propertyWindow.Tabs;
```
## 📦 새로 추가된 파일
### 핵심 클래스
- `PropertyTab.cs` - 탭 데이터 구조
- `TabChangedEventArgs.cs` - 탭 변경 이벤트 인자
- `PropertyTabView.cs` - 탭 UI 관리
### 가이드 문서
- `PropertyTabButton_SETUP_GUIDE.md` - 탭 버튼 Prefab 생성 가이드
- `PropertyWindow_TAB_SETUP_GUIDE.md` - PropertyWindow Prefab 수정 가이드
- `TAB_SYSTEM_README.md` - 이 문서
### 예제 코드
- `PropertyWindowTabExample.cs` - 사용 예제
## 🛠️ Unity Editor 설정 (필수)
### 1단계: PropertyTabButton Prefab 생성
`PropertyTabButton_SETUP_GUIDE.md` 참조하여 탭 버튼 Prefab 생성
### 2단계: PropertyWindow Prefab 수정
`PropertyWindow_TAB_SETUP_GUIDE.md` 참조하여 기존 Prefab 수정:
1. "PROPERTIES" 텍스트 제거
2. TabContainer 추가 (HorizontalLayoutGroup)
3. PropertyTabView 컴포넌트 추가 및 설정
### 3단계: 연결 확인
- PropertyWindow → PropertyTabView 컴포넌트 확인
- PropertyTabView → TabContainer 연결 확인
- PropertyTabView → TabButtonPrefab 연결 확인
## 📖 API 레퍼런스
### PropertyWindow 클래스
#### 메서드
**AddTab(id, name, properties, order)**
- 새로운 탭 추가
- 첫 번째 탭은 자동으로 선택됨
- 반환값: `PropertyTab`
**SelectTab(tabId)**
- 특정 탭 선택 및 속성 로드
- 반환값: `bool` (성공 여부)
**RemoveTab(tabId)**
- 탭 제거
- 현재 탭이 제거되면 첫 번째 탭 자동 선택
- 반환값: `bool` (성공 여부)
**GetTab(tabId)**
- 특정 탭 조회
- 반환값: `PropertyTab?`
**ClearTabs()**
- 모든 탭 제거
#### 속성
**Tabs**
- 모든 탭 목록
- 타입: `IReadOnlyList<PropertyTab>`
**CurrentTabId**
- 현재 선택된 탭 ID
- 타입: `string?`
**CurrentTab**
- 현재 선택된 탭
- 타입: `PropertyTab?`
#### 이벤트
**TabChanged**
- 탭이 변경될 때 발생
- 타입: `EventHandler<TabChangedEventArgs>`
### PropertyTab 클래스
#### 속성
**Id** - 탭 고유 ID
**Name** - 탭 표시 이름
**Properties** - 속성 리스트
**Order** - 정렬 순서
**IsActive** - 활성화 상태
#### 메서드
**AddProperty(item)** - 속성 추가
**RemoveProperty(itemId)** - 속성 제거
**Clear()** - 모든 속성 제거
**Count** - 속성 개수
### PropertyTabView 클래스
#### 메서드
**Initialize(propertyWindow)** - PropertyWindow와 연결
**RefreshTabs()** - 모든 탭 버튼 재생성
**RemoveTabButton(tabId)** - 특정 탭 버튼 제거
#### 설정 필드 (Inspector)
**Tab Container** - 탭 버튼이 배치될 컨테이너
**Tab Button Prefab** - 탭 버튼 Prefab
**Selected Color** - 선택된 탭 색상
**Normal Color** - 기본 탭 색상
**Hover Color** - 호버 시 탭 색상
## 🎨 색상 테마
기본 색상 (수정 가능):
- **Selected**: `#333333` (RGB 51, 51, 51)
- **Normal**: `#262626` (RGB 38, 38, 38)
- **Hover**: `#404040` (RGB 64, 64, 64)
- **Text (Selected)**: `#FFFFFF` (White)
- **Text (Normal)**: `#CCCCCC` (RGB 204, 204, 204)
## ⚠️ 주의사항
1. **탭 ID 중복**: 같은 ID로 탭을 추가하면 경고 로그 출력
2. **탭 자동 선택**: 첫 번째 탭은 자동으로 선택됨
3. **Prefab 설정**: Unity Editor에서 Prefab 설정을 완료해야 동작함
4. **이벤트 해제**: OnDestroy에서 이벤트 구독 해제 필요
## 🔄 하위 호환성
기존 코드는 영향을 받지 않습니다:
- `LoadProperties()` - 탭 없이 사용 가능 (기존 방식)
- `LoadGroupedProperties()` - 그룹 방식도 계속 사용 가능
- 탭 시스템은 **선택적 기능**입니다
## 📚 예제 코드
전체 예제는 `PropertyWindowTabExample.cs` 참조
## 🐛 문제 해결
**Q: 탭 버튼이 보이지 않아요**
A: PropertyWindow Prefab에서 TabContainer와 PropertyTabView가 올바르게 설정되었는지 확인하세요.
**Q: 탭을 클릭해도 반응이 없어요**
A: PropertyTabView의 TabButtonPrefab이 설정되었는지, Button 컴포넌트가 있는지 확인하세요.
**Q: 탭이 추가되는데 레이아웃이 이상해요**
A: TabContainer에 HorizontalLayoutGroup이 올바르게 설정되었는지 확인하세요.
**Q: 컴파일 에러가 발생해요**
A: Unity를 재시작하여 새 파일들을 인식하도록 하세요.
## 📞 문의
이슈나 문의사항은 프로젝트 관리자에게 연락하세요.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 43872a3da9cbaa144bf33c2b32668b54
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,126 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// ButtonProperty를 위한 UI를 제어하는 스크립트입니다.
/// Button 컴포넌트를 사용하여 클릭 이벤트를 처리합니다.
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class ButtonPropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름을 표시할 Text 컴포넌트
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private Button _button; // 버튼 컴포넌트
[SerializeField]
private TextMeshProUGUI _buttonText; // 버튼 텍스트
private ButtonProperty _propertyItem;
private PropertyWindow _controller;
/// <summary>
/// PropertyView에 의해 호출되어 UI를 초기화하고 데이터를 설정합니다.
/// </summary>
/// <param name="item">UI에 표시할 속성 데이터(IPropertyItem)</param>
/// <param name="controller">상호작용할 PropertyWindow</param>
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is ButtonProperty typedItem))
{
Debug.LogError($"ButtonPropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다. {item.GetType()}");
return;
}
_propertyItem = typedItem;
_controller = controller;
// --- 데이터 바인딩 ---
// 1. 속성 이름 설정
if (_nameLabel != null)
{
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정 (TooltipHandler 컴포넌트가 있다면 연동)
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
}
// 2. Description 설정
if (_descriptionLabel != null)
{
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
}
// 3. 버튼 텍스트 설정
if (_buttonText != null)
{
_buttonText.text = _propertyItem.ButtonText;
}
// 4. 읽기 전용 상태에 따라 버튼의 상호작용 여부 결정
if (_button != null)
{
_button.interactable = !_propertyItem.IsReadOnly;
// --- 이벤트 리스너 등록 ---
_button.onClick.RemoveAllListeners();
_button.onClick.AddListener(OnButtonClicked);
}
}
/// <summary>
/// 버튼이 클릭되었을 때 호출됩니다.
/// </summary>
private void OnButtonClicked()
{
if (_propertyItem != null)
{
_propertyItem.Click();
}
}
/// <summary>
/// UI의 읽기 전용 상태를 설정합니다.
/// </summary>
/// <param name="isReadOnly">읽기 전용 여부</param>
public void SetReadOnly(bool isReadOnly)
{
if (_button != null)
{
_button.interactable = !isReadOnly;
}
}
/// <summary>
/// PropertyWindow로부터 값 업데이트 요청이 왔을 때 호출됩니다.
/// ButtonProperty는 값이 없으므로 아무것도 하지 않습니다.
/// </summary>
/// <param name="newValue">새로운 값 (사용 안 함)</param>
public void UpdateValue(object newValue)
{
// ButtonProperty는 값이 없으므로 아무것도 하지 않음
}
}
}

View File

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

View File

@@ -0,0 +1,94 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Extention;
using UVC.UI.Tooltip;
namespace UVC.UI.Window.PropertyWindow.UI
{
/// <summary>
/// LabelProperty를 위한 UI를 제어하는 스크립트입니다.
/// 읽기 전용 텍스트를 표시합니다 (Label + Label 구조).
/// </summary>
[RequireComponent(typeof(LayoutElement))]
public class LabelPropertyUI : MonoBehaviour, IPropertyUI
{
[Header("UI Components")]
[SerializeField]
private TextMeshProUGUI _nameLabel; // 속성 이름 (왼쪽)
[SerializeField]
private TextMeshProUGUI _descriptionLabel;
[SerializeField]
private TextMeshProUGUI _valueLabel; // 속성 값 (오른쪽, 원래 InputField 자리)
private LabelProperty _propertyItem;
private PropertyWindow _controller;
public void Setup(IPropertyItem item, PropertyWindow controller)
{
if (!(item is LabelProperty typedItem))
{
Debug.LogError($"LabelPropertyUI에 잘못된 타입의 PropertyItem이 전달되었습니다. {item.GetType()}");
return;
}
_propertyItem = typedItem;
_controller = controller;
// 1. 속성 이름 설정
if (_nameLabel != null)
{
_nameLabel.text = _propertyItem.Name;
// 툴팁 설정
TooltipHandler tooltipHandler = _nameLabel.GetComponent<TooltipHandler>();
if (tooltipHandler != null && !_propertyItem.Tooltip.IsNullOrEmpty())
{
tooltipHandler.Tooltip = _propertyItem.Tooltip;
}
}
// 2. Description 설정
if (_descriptionLabel != null)
{
if (_propertyItem.Description.IsNullOrEmpty())
{
_descriptionLabel.gameObject.SetActive(false);
}
else
{
_descriptionLabel.gameObject.SetActive(true);
_descriptionLabel.text = _propertyItem.Description;
}
}
// 3. 값 및 색상 설정
if (_valueLabel != null)
{
_valueLabel.text = _propertyItem.Value;
_valueLabel.color = _propertyItem.TextColor; // 초기 색상 설정
}
}
public void SetReadOnly(bool isReadOnly)
{
// LabelProperty는 항상 읽기 전용이므로 아무것도 하지 않음
}
public void UpdateValue(object newValue)
{
if (_valueLabel != null && newValue is string stringValue)
{
_valueLabel.text = stringValue;
// TextColor 적용
if (_propertyItem != null)
{
_valueLabel.color = _propertyItem.TextColor;
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,247 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace UVC.UI.Window.PropertyWindow
{
/// <summary>
/// PropertyWindow의 탭 UI를 관리하는 View 클래스입니다.
/// 탭 버튼을 동적으로 생성하고, 선택 상태를 관리합니다.
/// </summary>
public class PropertyTabView : MonoBehaviour
{
[Header("References")]
[Tooltip("탭 버튼들이 배치될 컨테이너 (HorizontalLayoutGroup 권장)")]
[SerializeField] private Transform _tabContainer;
[Tooltip("탭 버튼 Prefab")]
[SerializeField] private GameObject _tabButtonPrefab;
[Header("Visual Settings")]
[Tooltip("선택된 탭의 배경 색상 (#3C3C3C)")]
[SerializeField] private Color _selectedColor = new Color(0.235f, 0.235f, 0.235f, 1f);
[Tooltip("선택되지 않은 탭의 배경 색상 (PropertyWindow 배경색 #181818)")]
[SerializeField] private Color _normalColor = new Color(0.094f, 0.094f, 0.094f, 1f);
[Tooltip("호버 시 탭의 배경 색상")]
[SerializeField] private Color _hoverColor = new Color(0.2f, 0.2f, 0.2f, 1f);
[Header("Text Colors")]
[Tooltip("선택되지 않은 탭의 텍스트 색상 (#464343)")]
[SerializeField] private Color _normalTextColor = new Color(0.275f, 0.263f, 0.263f, 1f);
[Tooltip("선택된 탭의 텍스트 색상 (White)")]
[SerializeField] private Color _selectedTextColor = Color.white;
/// <summary>
/// View가 상호작용할 PropertyWindow 인스턴스
/// </summary>
private PropertyWindow? _propertyWindow;
/// <summary>
/// 탭 ID -> 탭 버튼 GameObject 매핑
/// </summary>
private readonly Dictionary<string, GameObject> _tabButtons = new Dictionary<string, GameObject>();
/// <summary>
/// PropertyWindow를 설정하고 초기화합니다.
/// </summary>
/// <param name="propertyWindow">연결할 PropertyWindow</param>
public void Initialize(PropertyWindow propertyWindow)
{
if (_propertyWindow != null)
{
// 기존 이벤트 해제
_propertyWindow.TabChanged -= OnTabChanged;
}
_propertyWindow = propertyWindow;
if (_propertyWindow != null)
{
// 이벤트 등록
_propertyWindow.TabChanged += OnTabChanged;
// 기존 탭들 렌더링
RefreshTabs();
}
}
/// <summary>
/// 모든 탭 버튼을 다시 생성합니다.
/// </summary>
public void RefreshTabs()
{
if (_propertyWindow == null) return;
// 기존 버튼 제거
ClearAllButtons();
// 탭 버튼 생성
foreach (var tab in _propertyWindow.Tabs)
{
CreateTabButton(tab);
}
// 선택 상태 업데이트
UpdateSelectionVisual();
}
/// <summary>
/// 특정 탭의 버튼을 생성합니다.
/// </summary>
/// <param name="tab">생성할 탭 데이터</param>
private void CreateTabButton(PropertyTab tab)
{
if (_tabButtonPrefab == null || _tabContainer == null)
{
Debug.LogError("[PropertyTabView] TabButtonPrefab 또는 TabContainer가 설정되지 않았습니다.");
return;
}
// 버튼 생성
var buttonObj = Instantiate(_tabButtonPrefab, _tabContainer);
buttonObj.name = $"Tab_{tab.Id}";
// 텍스트 설정
var text = buttonObj.GetComponentInChildren<TextMeshProUGUI>();
if (text != null)
{
text.text = tab.Name;
// 텍스트 렌더링 강제 업데이트
text.ForceMeshUpdate();
// 텍스트 너비 계산 (좌우 5px 여백 = 총 10px)
float textWidth = text.preferredWidth;
float paddingHorizontal = 10f;
float buttonWidth = textWidth + paddingHorizontal;
// 버튼 너비 설정
var rectTransform = buttonObj.GetComponent<RectTransform>();
if (rectTransform != null)
{
rectTransform.sizeDelta = new Vector2(buttonWidth, rectTransform.sizeDelta.y);
}
}
// 버튼 클릭 이벤트 등록
var button = buttonObj.GetComponent<Button>();
if (button != null)
{
button.onClick.AddListener(() => OnTabButtonClicked(tab.Id));
}
// 딕셔너리에 저장
_tabButtons[tab.Id] = buttonObj;
}
/// <summary>
/// 탭 버튼이 클릭되었을 때 호출됩니다.
/// </summary>
/// <param name="tabId">클릭된 탭의 ID</param>
private void OnTabButtonClicked(string tabId)
{
if (_propertyWindow != null)
{
_propertyWindow.SelectTab(tabId);
}
}
/// <summary>
/// PropertyWindow의 TabChanged 이벤트 핸들러입니다.
/// </summary>
private void OnTabChanged(object? sender, TabChangedEventArgs e)
{
UpdateSelectionVisual();
}
/// <summary>
/// 선택된 탭의 시각적 상태를 업데이트합니다.
/// </summary>
private void UpdateSelectionVisual()
{
if (_propertyWindow == null) return;
string? currentTabId = _propertyWindow.CurrentTabId;
foreach (var kvp in _tabButtons)
{
string tabId = kvp.Key;
GameObject buttonObj = kvp.Value;
bool isSelected = tabId == currentTabId;
// 배경 이미지 색상 변경
var image = buttonObj.GetComponent<Image>();
if (image != null)
{
image.color = isSelected ? _selectedColor : _normalColor;
}
// Button의 ColorBlock 설정 (호버 효과)
var button = buttonObj.GetComponent<Button>();
if (button != null)
{
var colors = button.colors;
colors.normalColor = isSelected ? _selectedColor : _normalColor;
colors.highlightedColor = _hoverColor;
colors.pressedColor = _selectedColor;
colors.selectedColor = _selectedColor;
button.colors = colors;
}
// 텍스트 색상 변경
var text = buttonObj.GetComponentInChildren<TextMeshProUGUI>();
if (text != null)
{
text.color = isSelected ? _selectedTextColor : _normalTextColor;
}
}
}
/// <summary>
/// 모든 탭 버튼을 제거합니다.
/// </summary>
private void ClearAllButtons()
{
foreach (var buttonObj in _tabButtons.Values)
{
if (buttonObj != null)
{
Destroy(buttonObj);
}
}
_tabButtons.Clear();
}
/// <summary>
/// 특정 탭 버튼을 제거합니다.
/// </summary>
/// <param name="tabId">제거할 탭의 ID</param>
public void RemoveTabButton(string tabId)
{
if (_tabButtons.TryGetValue(tabId, out var buttonObj))
{
if (buttonObj != null)
{
Destroy(buttonObj);
}
_tabButtons.Remove(tabId);
}
}
private void OnDestroy()
{
if (_propertyWindow != null)
{
_propertyWindow.TabChanged -= OnTabChanged;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8ad4c10a9bdb92244a68c6269e11fe03

492
CLAUDE.md Normal file
View File

@@ -0,0 +1,492 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 프로젝트 개요
2026년 2월 11일 OCTOPUS DAY 전시회를 위한 디지털 트윈 시연 프로젝트입니다.
- **Unity 버전**: 6000.2.12f1 (Unity 6.0.2)
- **언어**: C# (.NET 6.0+)
- **주요 기술**: MQTT IoT 통신, 실시간 센서 데이터 시각화, 3D 디지털 트윈
## 개발 환경
이 프로젝트는 Unity Editor에서 실행됩니다:
- Unity Hub를 통해 Unity 6000.2.12f1 설치 필요
- IDE: JetBrains Rider 또는 Visual Studio
- Windows 빌드 타겟 (기본값: FullScreen 1920x1080@60fps)
## 런타임 설정 파일
`Assets/StreamingAssets/` 폴더의 JSON 파일들은 런타임에 로드되며 코드 수정 없이 설정을 변경할 수 있습니다:
- **MQTTConfig.json**: MQTT 브로커 호스트/포트 설정
- **FactoryAppConfig.json**: 현대위아 시연 설정 (API 엔드포인트, 프레임레이트)
- **AppConfig.json**: 기본 앱 설정 (언어, 윈도우 크기)
## 아키텍처
### 멀티 테넌트 구조
3개의 독립적인 시연 모듈이 있으며, 각각 자체 Scene과 Manager를 가집니다:
1. **ChunilENG** (`Assets/Scripts/ChunilENG/`)
- 한전기술 전력 IoT 시연
- 실시간 온도계 데이터 (MQTT 구독: `DVI/HOT/+`)
- 주요 Manager: DataManager, ViewManager, MachineInfoItemManager
2. **KEPCO** (`Assets/Scripts/KEPCO/`)
- 한전 전력 감시 시연
- 센서/시설 3D 시각화
- 주요 Manager: FacilityManager, SensorManager, ColorPalette
3. **HyundaiWIA** (`Assets/Scripts/HyundaiWIA/`)
- 현대위아 스마트팩토리 시연
- 설비 이상 감지 시나리오 (AnomalyScenario)
- Dashboard: AMR, COBOT, ASRS, Lifter, Storage
### 초기화 흐름
```
OctopusTwinAppMain (App 진입점, Singleton)
↓ Initialized 이벤트
{Scene}Main (예: ChunilENGSceneMain, Singleton)
↓ async Init()
Building/Facility 초기화
↓ await LoadManager<T>()
Manager 로드 (DataManager, UIManager, ViewManager 등)
↓ SetupDataSetting()
MQTT 연결 및 이벤트 바인딩
```
모든 Manager는 `Manager` 베이스 클래스를 상속하며 `async UniTask Init()` 메서드를 구현합니다.
### 핵심 디자인 패턴
1. **Singleton 패턴**
- `SingletonApp<T>`: 앱 전역 진입점 (OctopusTwinAppMain)
- `SingletonScene<T>`: Scene별 관리자 (ChunilENGSceneMain, KEPCOSceneMain 등)
2. **Command 패턴**
- 71개의 ICommand 구현체
- CameraCommand, DataCommand, ObjectCommand, UICommand 등
- UI 이벤트와 비즈니스 로직 분리
3. **이벤트 기반 통신**
```csharp
// MQTT → DataManager → UI 체인
mqttManager.onThermostatData += dataManager.SetThermostatDataList;
dataManager.onSetThermostatData += ui.SetData;
```
## MQTT 통신
### 구현 위치
- **핵심 클래스**: `Assets/Scripts/ChunilENG/Managements/MQTT.cs`
- **라이브러리**: Best MQTT v3.0.4 (TCP 또는 WebSocket)
### 연결 구조
```csharp
// 브로커 연결
host: "106.247.236.204"
port: "8901"
topics: "DVI/HOT/+" // 와일드카드 구독
// 이벤트 핸들러
OnConnected → OnMessage (JSON 파싱) → 콜백 실행
```
### 데이터 흐름
```
IoT 센서
↓ MQTT Publish (JSON 페이로드)
MQTT.cs OnMessage()
↓ JsonConvert.DeserializeObject<ThermostatData>()
DataManager.SetThermostatDataList()
↓ onSetThermostatData 이벤트
UI 업데이트 (ThermostatControlPanel)
```
## 비동기 처리
프로젝트 전체에서 **UniTask** (Cysharp)를 사용합니다 (212회 사용):
- Scene 초기화: `await building.Init()`
- Manager 로딩: `await LoadManager<T>()`
- 데이터 로드: `await UniTask.CompletedTask`
**주의**: `async void`는 이벤트 핸들러에서만 사용하고, 일반 메서드는 `async UniTask` 사용.
## 3D 객체 계층
```
Scene
├── Building (GameObject)
│ ├── Facility (컴포넌트)
│ │ └── Sensor (Tag_Machine으로 필터링)
│ └── Floor (층별 관리)
├── Canvas
│ ├── PopupCanvas (동적 팝업)
│ ├── StaticCanvas (고정 UI)
│ └── TopMenuPanel
└── Camera
└── OrbitalController (3D 카메라 제어)
```
**객체 초기화 패턴**: Building → Floor → Machine/Thermostat/Facility → Sensor (모두 async Init)
## 리소스 구조
- **JSON 데이터**: `Assets/Resources/Data/{ChunilENG|KEPCO|HyundaiWIA}/`
- MachineData.json, Facility.json, SensorColorData.json 등
- **Prefab**: `Assets/Resources/` (각 Scene Main Prefab)
- **UI**: `Assets/Resources/UI/`
JSON은 `Resources.Load<TextAsset>(path).text`로 로드하고 Newtonsoft.Json으로 역직렬화합니다.
## 주요 외부 패키지
- **com.tivadar.best.http** v3.0.16 (HTTP/2 지원)
- **com.tivadar.best.mqtt** v3.0.4 (MQTT 프로토콜)
- **com.tivadar.best.websockets** v3.0.7 (WebSocket)
- **com.cysharp.unitask** (async/await)
- **com.unity.nuget.newtonsoft-json** v3.2.2 (JSON 처리)
- **com.unity.render-pipelines.universal** v17.2.0 (URP)
## Scene 정보
| Scene | 용도 |
|-------|------|
| Demo.unity | 종합 데모 |
| Demo_Home.unity | 프로젝트 선택 화면 |
| Demo_시연.unity | 한전기술 시연 (MQTT 실시간 데이터) |
| HyundaiWIA.unity | 현대위아 스마트팩토리 시연 |
## 코드 작성 시 주의사항
1. **Manager 추가 시**:
- `Manager` 베이스 클래스 상속
- `async UniTask Init()` 오버라이드
- `{Scene}Main.cs`의 `LoadManager<T>()` 호출 추가
2. **Command 추가 시**:
- `ICommand` 인터페이스 구현
- `Execute()` 메서드에 로직 작성
- 적절한 폴더에 배치 (CameraCommand, DataCommand 등)
3. **MQTT 토픽 추가 시**:
- `MQTT.cs`의 `subscriptionTopics` 배열에 추가
- 해당 토픽의 콜백 함수 등록 (`thermostatTopicTable` 등)
- 데이터 구조체 정의 (`[Serializable]` 특성 필요)
4. **UI 이벤트 바인딩**:
- `SetupDataSetting()` 메서드에서 이벤트 체인 구성
- Manager → UI 순서로 연결
## UI 시스템
### Canvas 계층 구조
프로젝트는 3단계 Canvas 아키텍처를 사용합니다:
```
Level 1: StaticCanvas (항상 표시)
├── LeftSidePanel (도구 모음)
├── TopMenuPanel (메뉴)
└── BottomToolbar (카메라 버튼, ChunilENG만)
Level 2: PopupCanvas (동적 Panel)
├── TotalProgressPanel (종합 현황)
├── MachineDashBoard (기계 대시보드)
├── FloorControlPanel (층 조절)
└── SettingPanel (설정)
Level 3: LocalPopupManager (개별 Popup)
└── PopupBase 상속 (ToastPopup 등)
```
### 기본 클래스
- **UICanvas**: 모든 Canvas의 베이스
- `LoadPanel<T>()`: Panel 로드
- `GetPanel<T>()`: Panel 획득
- `OpenPanel<T>()`: Panel 열기
- CanvasPanelOpenMode: None (다중), Single (단일)
- **UIPanel**: 모든 Panel의 베이스
- `async UniTask Init()`: 비동기 초기화
- `Open()`: Panel 활성화
- `Close()`: Panel 비활성화
- `GetElement<T>(name)`: 자식 컴포넌트 검색
- **PopupBase**: 모든 Popup의 베이스
- 자동 PopupBlocker 연동
- `ClosePopup()`: 팝업 닫기
- **UIManager**: Canvas 관리자
- `LoadCanvas<T>()`: Canvas 로드
- `GetCanvas<T>()`: Canvas 획득
### 프로젝트별 주요 UI
**ChunilENG** (생산 관리)
- MachineDashBoard: 기계 상세 정보
- TotalProgressPanel: 종합 진행현황 (자동 순환)
- ThermostatControlPanel: MQTT 온도계 제어
- 특징: IColorChangeBehaviour 색상 변경
**KEPCO** (전력 감시)
- FacilityAndSensorTypeTogglePanel: 설비/센서 목록
- TotalProgressPanel: MTR/GIS 구분 표시
- 특징: ScriptableAnimation (Echo, Scaling 등)
**HyundaiWIA** (스마트팩토리)
- AnomalyScenario: 이상 시나리오 관리자
- EquipmentTabController: 설비 탭 (AMR, COBOT, Lifter, Storage)
- 특징: 단계별 팝업 시나리오
### Panel/Popup 생명주기
```csharp
// Panel 생명주기
1. LoadPanel<T>() → Panel 검색/생성
2. await Init() → 자식 컴포넌트 캐싱
3. Open() → gameObject.SetActive(true)
4. Close() → gameObject.SetActive(false)
// Popup 생명주기
1. OnEnable() → PopupBlocker.Show()
2. 사용자 상호작용
3. ClosePopup() → gameObject.SetActive(false)
4. OnDisable() → PopupBlocker.Hide()
```
### 데이터 바인딩 패턴
```csharp
// 1. 직접 바인딩
MachineDashBoard.SetDetailDashBoardData(data, machine);
// 2. Content 기반 바인딩
ProgressContent.SetProductionStatusItem(completeInfoList);
// 3. 동적 생성 바인딩
var content = Instantiate(FacilitiesContent, parent);
content.SetProductionStatusItem(group.Panels, group.Type);
```
### UI 추가 가이드
1. **Canvas 추가**:
- `UICanvas` 상속
- `{Project}UIManager.Init()`에서 `LoadCanvas<T>()` 호출
2. **Panel 추가**:
- `UIPanel` 상속
- `async UniTask Init()` 구현 (GetElement로 자식 캐싱)
- Canvas의 Init()에서 `LoadPanel<T>()` 호출
3. **Popup 추가**:
- `PopupBase` 상속
- `OnEnable`/`OnDisable`에서 PopupBlocker 자동 처리
- `ClosePopup()` 메서드로 닫기
4. **데이터 표시**:
- `SetData()` 또는 `Set{DataType}()` 메서드 구현
- 이벤트 구독: `dataManager.onSetData += panel.SetData`
5. **애니메이션**:
- ChunilENG: Panel_Effect, IColorChangeBehaviour
- KEPCO: ScriptableAnimation (Animation_Scaling 등)
- HyundaiWIA: DOTween (RectTransform.DOAnchorPos)
### 공통 UI 컴포넌트
- **PopupBlocker**: 전역 팝업 배경 차단 (Singleton)
- **ToastPopup**: 자동 닫힘 알림 (displayDuration)
- **UILoading**: 로딩 화면 (Progress Bar, Fade)
- **LanguageController**: 언어 전환
- **BottomToolbar**: 카메라 이동 버튼
### 표준 UI 패턴
**Panel 열기 (권장)**
```csharp
var uiManager = ChunilENGSceneMain.Instance.GetManager<ChunilENGUIManager>();
uiManager.GetCanvas<PopupCanvas>().OpenPanel<MachineDashBoard>(CanvasPanelOpenMode.Single);
```
**데이터와 함께 Panel 열기**
```csharp
var uiManager = ChunilENGSceneMain.Instance.GetManager<ChunilENGUIManager>();
var panel = uiManager.GetCanvas<PopupCanvas>().GetPanel<MachineDashBoard>();
panel.SetDetailDashBoardData(data, machine);
uiManager.GetCanvas<PopupCanvas>().OpenPanel<MachineDashBoard>(CanvasPanelOpenMode.Single);
```
**Panel 닫기**
```csharp
var panel = uiManager.GetCanvas<PopupCanvas>().GetPanel<MachineDashBoard>();
panel.Close();
```
**Command 패턴 (UI 열기)**
```csharp
public class OpenMachineDashBoardCommand : ICommand
{
public void Execute(object? parameter = null)
{
var uiManager = ChunilENGSceneMain.Instance.GetManager<ChunilENGUIManager>();
uiManager.GetCanvas<PopupCanvas>().OpenPanel<MachineDashBoard>(CanvasPanelOpenMode.Single);
}
}
```
**이벤트 구독**
```csharp
// SetupDataSetting 패턴
mqttManager.onThermostatData += dataManager.SetThermostatDataList;
dataManager.onSetThermostatData += panel.SetData;
```
**Panel 초기화**
```csharp
public override async UniTask Init()
{
// GetElement로 UI 요소 캐싱
MachineName = GetElement<TextMeshProUGUI>(nameof(MachineName));
Button_Close = GetElement<Button>(nameof(Button_Close));
// 이벤트 리스너 등록
Button_Close.onClick.AddListener(Close);
await UniTask.CompletedTask;
}
```
## 3D 오브젝트 상호작용
### 오브젝트 계층 구조
```
Building
├── Floor (층별 관리)
│ ├── Facility (설비, KEPCO)
│ │ └── Sensor (센서)
│ ├── Machine (기계, ChunilENG)
│ └── Thermostat (온도계, ChunilENG)
```
### 주요 오브젝트 클래스
**Building**
- `GetMachines()` / `GetThermostats()`: 오브젝트 리스트 반환
- `SetFloor(index)`: 층 변경 및 가시성 제어
- `Init()`: 모든 자식 오브젝트 비동기 초기화
**Facility / Machine**
- `centerPos`: 메시 중심 좌표 (카메라 포커싱용)
- `originScale`: 원본 스케일
- `Init()`: centerPos, 컴포넌트 초기화
- `focusDistance/Azimuth/Elevation`: 카메라 설정값 (Machine)
**Sensor**
- `SetSensorState(state)`: 상태 변경 및 색상 업데이트
- `Active()` / `Inactive()`: 센서 활성화/비활성화
- `Hovering()`: 호버 상태 동기화, Outline 활성화
- `outline`: Outline 컴포넌트 (외곽선 효과)
### 마우스 상호작용
**MachineClickManager** (ChunilENG)
- Raycast 기반 3D 오브젝트 클릭 감지
- 이벤트: `onLeftClickMachine`, `onLeftClickArea`
**UI 아이콘 클릭** (KEPCO)
- UI_FacilityIcon / UI_SensorIcon이 IPointerClickHandler 구현
- `OnPointerClick()`: SelectedObjectCommand 실행
- `OnPointerEnter/Exit()`: 호버 효과 (상태 변경, Outline)
### Command 패턴
**SelectedObjectCommand**
- `Execute()`: ViewManager.SetTargetPosToMachine() 호출
- 설비/센서 타입 판별 후 카메라 포커싱
**SelectedMachineCommand**
- Machine 오브젝트로 카메라 포커싱
**OpenFacilityInfoPanel**
- 설비 정보 패널 열기
### 카메라 제어
**OrbitalController**
- 궤도형 카메라 제어 (Orbital Pattern)
- `SetTargetPos(Vector3)`: 타겟 위치 설정
- `AnimateToState()`: DoTween 기반 부드러운 이동
- `SetViewMode(ViewMode)`: PerspectiveView / TopView 전환
- 입력: 좌클릭(Pan), 우클릭(Orbit), 휠(Zoom)
### 오브젝트 검색
**Tag 기반 검색**
- `Tag_Machine`: Unity VisualScripting 태그로 머신/설비 마킹
- `FindObjectsByType<Tag_Machine>()`: 태그된 오브젝트 검색
**타입별 필터링**
- FacilityManager: 설비타입 필터링 ("GIS", "주변압기")
- SensorManager: SensorType별 그룹화 (PD, CB, DGA 등)
- Dictionary 기반 빠른 검색 (name/code → 오브젝트)
### 시각적 효과
**선택/하이라이트**
- Outline: 호버 시 외곽선 표시
- Color 변경: MaterialPropertyBlock으로 상태별 색상
- Animation: ScriptableAnimation (Echo, Scaling)
**상태별 색상** (KEPCO)
- ColorPalette에서 SensorState별 색상 관리
- Normal(녹색), Interest(청록), Care(노랑), Anomaly(주황), Danger(빨강)
### 오브젝트 추가 가이드
1. **새 오브젝트 타입 추가**:
- Building/Facility/Machine 중 적절한 클래스 상속
- `async UniTask Init()` 구현
- centerPos, originScale 초기화
2. **클릭 이벤트 추가**:
- ICommand 구현체 생성 (SelectedXXXCommand)
- MachineClickManager 또는 IPointerClickHandler 사용
3. **오브젝트 검색**:
- Tag_Machine 태그 추가
- Manager에서 FindObjectsByType으로 검색
- Dictionary에 등록 (이름/코드 → 오브젝트)
4. **시각적 효과**:
- Outline 컴포넌트 추가
- MaterialPropertyBlock으로 색상 변경
- ScriptableAnimation 활용
## 성능 최적화
- **타겟 프레임레이트**: 60fps (FactoryAppConfig.json)
- **LOD 사용**: Sensor_LOD.cs (Level of Detail)
- **DOTween 애니메이션**: ScriptableAnimationManager로 최적화
- **URP 렌더링**: 경량화된 Universal Render Pipeline
## 디버깅
- **로그**: Unity Console 창 확인
- **MQTT 연결 상태**: `MQTT.cs`의 `OnStateChange` 이벤트 로그
- **Manager 초기화**: 각 Manager의 `isInit` 플래그 확인
- **JSON 파싱 에러**: Newtonsoft.Json의 예외 메시지 확인
## 알려진 제약사항
- WebGL 빌드 시 MQTT는 WebSocket 모드로만 작동 (TCP 불가)
- MQTT QoS Level 3 사용 (정확히 1회 배달)
- 현재 중앙 MQTT 수신 시스템(DataRepository)은 주석 처리됨 (각 Scene별로 독립 연결)