- 모든 컨트롤러 에디터를 IMGUI → UI Toolkit(UXML/USS)으로 전환 (Camera, Item, Event, Avatar, System, StreamDeck, OptiTrack, Facial) - StreamingleCommon.uss 공통 테마 + 개별 에디터 USS 스타일시트 - SystemController 서브매니저 분리 (OptiTrack, Facial, Recording, Screenshot 등) - 런타임 컨트롤 패널 (ESC 토글, 좌측 오버레이, 150% 스케일) - 웹 대시보드 서버 (StreamingleDashboardServer) + 리타게팅 통합 - 설정 도구(StreamingleControllerSetupTool) UXML 재작성 + 원클릭 설정 - SimplePoseTransfer UXML 에디터 추가 - 전체 UXML 한글화 + NanumGothic 폰트 적용 - Streamingle.Debug → Streamingle.Debugging 네임스페이스 변경 (Debug.Log 충돌 해결) - 불필요 코드 제거 (rawkey.cs, RetargetingHTTPServer, OptitrackSkeletonAnimator 등) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
428 lines
15 KiB
C#
428 lines
15 KiB
C#
using System;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
using UnityEditor;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace Streamingle.Editor
|
|
{
|
|
public class StreamingleControllerSetupToolAdvanced : EditorWindow
|
|
{
|
|
private const string UxmlPath = "Assets/Scripts/Editor/StreamingleTools/StreamingleControllerSetupTool.uxml";
|
|
|
|
private struct ControllerDef
|
|
{
|
|
public string label;
|
|
public string propertyName;
|
|
public Type componentType;
|
|
public string gameObjectName;
|
|
}
|
|
|
|
private ControllerDef[] controllerDefs;
|
|
private Component[] existingComponents;
|
|
private Toggle[] createToggles;
|
|
|
|
private VisualElement statusList;
|
|
private VisualElement createTogglesContainer;
|
|
private Toggle toggleCreateParent;
|
|
private TextField fieldParentName;
|
|
private Toggle toggleAutoConnect;
|
|
private Toggle toggleMoveExisting;
|
|
private Button btnOneClick;
|
|
private Button btnCreate;
|
|
private Button btnConnect;
|
|
private Label resultText;
|
|
|
|
private readonly StringBuilder logBuilder = new();
|
|
|
|
[MenuItem("Tools/Streamingle/고급 컨트롤러 설정 도구")]
|
|
public static void ShowWindow()
|
|
{
|
|
var wnd = GetWindow<StreamingleControllerSetupToolAdvanced>("Streamingle 설정");
|
|
wnd.minSize = new Vector2(360, 380);
|
|
}
|
|
|
|
private void InitControllerDefs()
|
|
{
|
|
controllerDefs = new ControllerDef[]
|
|
{
|
|
new() { label = "StreamDeck 서버 매니저", propertyName = null, componentType = typeof(StreamDeckServerManager), gameObjectName = "StreamDeck 서버 매니저" },
|
|
new() { label = "시스템 컨트롤러", propertyName = "systemController", componentType = typeof(SystemController), gameObjectName = "시스템 컨트롤러" },
|
|
new() { label = "카메라 매니저", propertyName = "cameraManager", componentType = typeof(CameraManager), gameObjectName = "카메라 매니저" },
|
|
new() { label = "아이템 컨트롤러", propertyName = "itemController", componentType = typeof(ItemController), gameObjectName = "아이템 컨트롤러" },
|
|
new() { label = "이벤트 컨트롤러", propertyName = "eventController", componentType = typeof(EventController), gameObjectName = "이벤트 컨트롤러" },
|
|
new() { label = "아바타 의상 컨트롤러", propertyName = "avatarOutfitController", componentType = typeof(AvatarOutfitController), gameObjectName = "아바타 의상 컨트롤러" },
|
|
};
|
|
|
|
existingComponents = new Component[controllerDefs.Length];
|
|
createToggles = new Toggle[controllerDefs.Length];
|
|
}
|
|
|
|
private void CreateGUI()
|
|
{
|
|
InitControllerDefs();
|
|
|
|
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
|
|
if (uxml != null) uxml.CloneTree(rootVisualElement);
|
|
|
|
statusList = rootVisualElement.Q("status-list");
|
|
createTogglesContainer = rootVisualElement.Q("create-toggles");
|
|
toggleCreateParent = rootVisualElement.Q<Toggle>("toggle-create-parent");
|
|
fieldParentName = rootVisualElement.Q<TextField>("field-parent-name");
|
|
toggleAutoConnect = rootVisualElement.Q<Toggle>("toggle-auto-connect");
|
|
toggleMoveExisting = rootVisualElement.Q<Toggle>("toggle-move-existing");
|
|
btnOneClick = rootVisualElement.Q<Button>("btn-one-click");
|
|
btnCreate = rootVisualElement.Q<Button>("btn-create");
|
|
btnConnect = rootVisualElement.Q<Button>("btn-connect");
|
|
resultText = rootVisualElement.Q<Label>("result-text");
|
|
|
|
toggleCreateParent?.RegisterValueChangedCallback(evt =>
|
|
{
|
|
if (fieldParentName != null)
|
|
fieldParentName.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
});
|
|
|
|
btnOneClick?.RegisterCallback<ClickEvent>(_ => OneClickSetup());
|
|
btnCreate?.RegisterCallback<ClickEvent>(_ => CreateControllers());
|
|
btnConnect?.RegisterCallback<ClickEvent>(_ => ConnectExistingControllers());
|
|
rootVisualElement.Q<Button>("btn-refresh")?.RegisterCallback<ClickEvent>(_ => RefreshAll());
|
|
rootVisualElement.Q<Button>("btn-move")?.RegisterCallback<ClickEvent>(_ => MoveExistingControllersToParent());
|
|
|
|
BuildCreateToggles();
|
|
RefreshAll();
|
|
}
|
|
|
|
private void OnFocus()
|
|
{
|
|
if (statusList != null) RefreshAll();
|
|
}
|
|
|
|
#region Status
|
|
|
|
private void RefreshAll()
|
|
{
|
|
FindExistingControllers();
|
|
RebuildStatusList();
|
|
UpdateCreateToggles();
|
|
UpdateButtonStates();
|
|
}
|
|
|
|
private void FindExistingControllers()
|
|
{
|
|
if (controllerDefs == null) return;
|
|
for (int i = 0; i < controllerDefs.Length; i++)
|
|
existingComponents[i] = FindFirstObjectByType(controllerDefs[i].componentType) as Component;
|
|
}
|
|
|
|
private void RebuildStatusList()
|
|
{
|
|
if (statusList == null) return;
|
|
statusList.Clear();
|
|
|
|
int foundCount = 0;
|
|
for (int i = 0; i < controllerDefs.Length; i++)
|
|
{
|
|
var def = controllerDefs[i];
|
|
var comp = existingComponents[i];
|
|
bool found = comp != null;
|
|
if (found) foundCount++;
|
|
|
|
var row = new VisualElement();
|
|
row.AddToClassList("status-row");
|
|
|
|
var dot = new VisualElement();
|
|
dot.AddToClassList("status-dot");
|
|
dot.AddToClassList(found ? "status-dot--found" : "status-dot--missing");
|
|
row.Add(dot);
|
|
|
|
var nameLabel = new Label(def.label);
|
|
nameLabel.AddToClassList("status-name");
|
|
row.Add(nameLabel);
|
|
|
|
var locLabel = new Label(found ? GetParentInfo(comp.transform) : "미발견");
|
|
locLabel.AddToClassList("status-location");
|
|
row.Add(locLabel);
|
|
|
|
if (found)
|
|
{
|
|
var selectBtn = new Button(() =>
|
|
{
|
|
Selection.activeGameObject = comp.gameObject;
|
|
EditorGUIUtility.PingObject(comp.gameObject);
|
|
}) { text = "선택" };
|
|
selectBtn.AddToClassList("status-select-btn");
|
|
row.Add(selectBtn);
|
|
}
|
|
|
|
statusList.Add(row);
|
|
}
|
|
|
|
var summary = new VisualElement();
|
|
summary.AddToClassList("summary-line");
|
|
summary.style.marginTop = 4;
|
|
summary.style.paddingTop = 4;
|
|
summary.style.borderTopWidth = 1;
|
|
summary.style.borderTopColor = new Color(1, 1, 1, 0.04f);
|
|
|
|
var countLabel = new Label($"{foundCount}/{controllerDefs.Length}");
|
|
countLabel.AddToClassList("summary-count");
|
|
summary.Add(countLabel);
|
|
|
|
var summaryText = new Label("개 발견됨");
|
|
summaryText.AddToClassList("summary-label");
|
|
summary.Add(summaryText);
|
|
|
|
statusList.Add(summary);
|
|
}
|
|
|
|
private string GetParentInfo(Transform t)
|
|
{
|
|
return t.parent == null ? "(루트)" : $"({t.parent.name})";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Create Toggles
|
|
|
|
private void BuildCreateToggles()
|
|
{
|
|
if (createTogglesContainer == null) return;
|
|
|
|
for (int i = 0; i < controllerDefs.Length; i++)
|
|
{
|
|
var row = new VisualElement();
|
|
row.AddToClassList("controller-toggle-row");
|
|
|
|
var toggle = new Toggle(controllerDefs[i].label) { value = true };
|
|
createToggles[i] = toggle;
|
|
row.Add(toggle);
|
|
|
|
var badge = new Label("존재");
|
|
badge.AddToClassList("controller-exists-badge");
|
|
badge.style.display = DisplayStyle.None;
|
|
badge.name = $"badge-{i}";
|
|
row.Add(badge);
|
|
|
|
createTogglesContainer.Add(row);
|
|
}
|
|
}
|
|
|
|
private void UpdateCreateToggles()
|
|
{
|
|
for (int i = 0; i < controllerDefs.Length; i++)
|
|
{
|
|
bool exists = existingComponents[i] != null;
|
|
var toggle = createToggles[i];
|
|
if (toggle == null) continue;
|
|
|
|
toggle.SetEnabled(!exists);
|
|
if (exists) toggle.value = false;
|
|
|
|
var badge = createTogglesContainer.Q<Label>($"badge-{i}");
|
|
if (badge != null)
|
|
badge.style.display = exists ? DisplayStyle.Flex : DisplayStyle.None;
|
|
}
|
|
}
|
|
|
|
private void UpdateButtonStates()
|
|
{
|
|
bool hasStreamDeck = existingComponents[0] != null;
|
|
if (btnConnect != null) btnConnect.SetEnabled(hasStreamDeck);
|
|
|
|
bool anyMissing = false;
|
|
for (int i = 0; i < existingComponents.Length; i++)
|
|
{
|
|
if (existingComponents[i] == null) { anyMissing = true; break; }
|
|
}
|
|
if (btnOneClick != null) btnOneClick.SetEnabled(anyMissing || !hasStreamDeck);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Actions
|
|
|
|
private void OneClickSetup()
|
|
{
|
|
logBuilder.Clear();
|
|
Log("=== 원클릭 설정 시작 ===");
|
|
|
|
FindExistingControllers();
|
|
|
|
for (int i = 0; i < controllerDefs.Length; i++)
|
|
{
|
|
if (existingComponents[i] == null)
|
|
createToggles[i].value = true;
|
|
}
|
|
|
|
CreateControllers();
|
|
ConnectExistingControllers();
|
|
|
|
if (toggleMoveExisting != null && toggleMoveExisting.value)
|
|
MoveExistingControllersToParent();
|
|
|
|
Log("=== 설정 완료 ===");
|
|
FlushLog();
|
|
RefreshAll();
|
|
}
|
|
|
|
private void CreateControllers()
|
|
{
|
|
logBuilder.Clear();
|
|
|
|
GameObject parent = null;
|
|
bool useParent = toggleCreateParent?.value ?? true;
|
|
string parentName = fieldParentName?.value ?? "Streamingle 컨트롤러들";
|
|
|
|
if (useParent)
|
|
{
|
|
parent = GameObject.Find(parentName);
|
|
if (parent == null)
|
|
{
|
|
parent = new GameObject(parentName);
|
|
Undo.RegisterCreatedObjectUndo(parent, "Create Streamingle Parent");
|
|
Log($"+ 부모 오브젝트 생성: {parentName}");
|
|
}
|
|
}
|
|
|
|
int created = 0;
|
|
for (int i = 0; i < controllerDefs.Length; i++)
|
|
{
|
|
if (existingComponents[i] != null) continue;
|
|
if (createToggles[i] == null || !createToggles[i].value) continue;
|
|
|
|
var def = controllerDefs[i];
|
|
var go = new GameObject(def.gameObjectName);
|
|
Undo.RegisterCreatedObjectUndo(go, $"Create {def.label}");
|
|
|
|
if (parent != null)
|
|
go.transform.SetParent(parent.transform);
|
|
|
|
go.AddComponent(def.componentType);
|
|
existingComponents[i] = go.GetComponent(def.componentType);
|
|
Log($"+ 생성: {def.label}");
|
|
created++;
|
|
}
|
|
|
|
if (created == 0)
|
|
Log("생성할 항목이 없습니다.");
|
|
else
|
|
Log($"{created}개 컨트롤러 생성됨.");
|
|
|
|
if (created > 0 && (toggleAutoConnect?.value ?? true))
|
|
ConnectExistingControllers();
|
|
|
|
if (created > 0 && parent != null && (toggleMoveExisting?.value ?? true))
|
|
MoveExistingControllersToParent(parent);
|
|
|
|
FlushLog();
|
|
RefreshAll();
|
|
|
|
if (parent != null)
|
|
{
|
|
Selection.activeGameObject = parent;
|
|
EditorGUIUtility.PingObject(parent);
|
|
}
|
|
}
|
|
|
|
private void ConnectExistingControllers()
|
|
{
|
|
FindExistingControllers();
|
|
|
|
var manager = existingComponents[0] as StreamDeckServerManager;
|
|
if (manager == null)
|
|
{
|
|
Log("! StreamDeck 서버 매니저를 찾을 수 없습니다.");
|
|
FlushLog();
|
|
return;
|
|
}
|
|
|
|
var so = new SerializedObject(manager);
|
|
so.Update();
|
|
|
|
int connected = 0;
|
|
for (int i = 1; i < controllerDefs.Length; i++)
|
|
{
|
|
var def = controllerDefs[i];
|
|
var comp = existingComponents[i];
|
|
if (comp == null || def.propertyName == null) continue;
|
|
|
|
var prop = so.FindProperty(def.propertyName);
|
|
if (prop != null && prop.objectReferenceValue != comp)
|
|
{
|
|
prop.objectReferenceValue = comp;
|
|
connected++;
|
|
Log($" -> 연결: {def.label}");
|
|
}
|
|
}
|
|
|
|
so.ApplyModifiedProperties();
|
|
|
|
if (connected == 0)
|
|
Log("모든 컨트롤러가 이미 연결되어 있습니다.");
|
|
else
|
|
Log($"{connected}개 컨트롤러를 매니저에 연결했습니다.");
|
|
|
|
FlushLog();
|
|
}
|
|
|
|
private void MoveExistingControllersToParent(GameObject parent = null)
|
|
{
|
|
FindExistingControllers();
|
|
|
|
string parentName = fieldParentName?.value ?? "Streamingle 컨트롤러들";
|
|
|
|
if (parent == null)
|
|
{
|
|
parent = GameObject.Find(parentName);
|
|
if (parent == null)
|
|
{
|
|
parent = new GameObject(parentName);
|
|
Undo.RegisterCreatedObjectUndo(parent, "Create Streamingle Parent");
|
|
Log($"+ 부모 오브젝트 생성: {parentName}");
|
|
}
|
|
}
|
|
|
|
int moved = 0;
|
|
for (int i = 0; i < controllerDefs.Length; i++)
|
|
{
|
|
var comp = existingComponents[i];
|
|
if (comp == null) continue;
|
|
if (comp.transform.parent == parent.transform) continue;
|
|
|
|
Undo.SetTransformParent(comp.transform, parent.transform, $"Move {controllerDefs[i].label}");
|
|
moved++;
|
|
Log($" -> 이동: {controllerDefs[i].label}");
|
|
}
|
|
|
|
if (moved == 0)
|
|
Log("이동할 컨트롤러가 없습니다.");
|
|
else
|
|
Log($"{moved}개 컨트롤러를 {parentName} 하위로 이동.");
|
|
|
|
FlushLog();
|
|
RefreshAll();
|
|
|
|
Selection.activeGameObject = parent;
|
|
EditorGUIUtility.PingObject(parent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Logging
|
|
|
|
private void Log(string msg)
|
|
{
|
|
logBuilder.AppendLine(msg);
|
|
}
|
|
|
|
private void FlushLog()
|
|
{
|
|
if (resultText != null)
|
|
resultText.text = logBuilder.ToString().TrimEnd();
|
|
Debug.Log($"[Streamingle Setup] {logBuilder}");
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|