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:
@@ -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}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c52761060ad8f2c45b4783fca02a4b95
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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가 런타임에 자동으로 등록합니다.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d47e4475932364bb053dc5facf32bf
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44a8f48a35bd63d4f81a2448c6f70d96
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 93f804cf7d904534fb648912390f4b22
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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` 참조)
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71a53eeea9e90614b904464661bbcf54
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/DownloadAssets/XRLib/Scripts/UVC/Entity.meta
Normal file
8
Assets/DownloadAssets/XRLib/Scripts/UVC/Entity.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7cb1d9d47062753489be2eb89fe15987
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
104
Assets/DownloadAssets/XRLib/Scripts/UVC/Entity/Entity.cs
Normal file
104
Assets/DownloadAssets/XRLib/Scripts/UVC/Entity/Entity.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f30195d469d854a4e9added87fb904df
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5866fa3234b9f204faa5fd13e9a1addb
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20ebee858c3f61043a8af0022a1e9536
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f604aa687fd0334a999a75cad954c0b
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b1a8246b284660448bdc6c0298df270
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e990a792dfd382747b996702cba76017
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6bd468aa396e86a4aabe526ec12b8b78
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cabb5835a571a344fb867cee197f64c6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c44fafcc4bb3364ba2848a8542ef5a4
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -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를 재시작하여 새 파일들을 인식하도록 하세요.
|
||||
|
||||
## 📞 문의
|
||||
|
||||
이슈나 문의사항은 프로젝트 관리자에게 연락하세요.
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43872a3da9cbaa144bf33c2b32668b54
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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는 값이 없으므로 아무것도 하지 않음
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d6578f6d0ce25e40973af09160ed997
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5bffbcbfc3cc8640851dadcc462cfe3
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ad4c10a9bdb92244a68c6269e11fe03
|
||||
492
CLAUDE.md
Normal file
492
CLAUDE.md
Normal 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별로 독립 연결)
|
||||
Reference in New Issue
Block a user