Modify: 카메라 컨트롤러 관련 스크립트 업데이트

This commit is contained in:
DESKTOP-S4BOTN2\user 2026-04-16 19:32:33 +09:00
parent 7a4e05d575
commit 2cfe3d7f5f
6 changed files with 434 additions and 17 deletions

View File

@ -439,6 +439,9 @@ public class StreamDeckServerManager : MonoBehaviour
case "get_drone_state":
HandleGetDroneState(service);
break;
case "toggle_default_blend":
HandleToggleDefaultBlend();
break;
// 아이템
case "toggle_item":
@ -642,6 +645,13 @@ public class StreamDeckServerManager : MonoBehaviour
HandleGetDroneState(service);
}
private void HandleToggleDefaultBlend()
{
if (cameraManager == null) return;
cameraManager.ToggleDefaultBlend();
// ToggleDefaultBlend 내부에서 NotifyCameraChanged를 호출하여 모든 클라이언트에 브로드캐스트됩니다.
}
private void HandleGetDroneState(StreamDeckService service)
{
if (cameraManager == null) return;

View File

@ -365,6 +365,61 @@ public class CameraManager : MonoBehaviour, IController
streamDeckManager.NotifyCameraChanged();
}
}
/// <summary>
/// Main Camera에 달린 CinemachineBrain의 Default Blend 스타일을 Cut ↔ EaseInOut 로 토글합니다.
/// </summary>
public void ToggleDefaultBlend()
{
var brain = GetCinemachineBrain();
if (brain == null)
{
Debug.LogWarning("[CameraManager] CinemachineBrain을 찾을 수 없습니다. Main Camera에 추가되어 있어야 합니다.");
return;
}
var blend = brain.DefaultBlend;
blend.Style = (blend.Style == CinemachineBlendDefinition.Styles.Cut)
? CinemachineBlendDefinition.Styles.EaseInOut
: CinemachineBlendDefinition.Styles.Cut;
brain.DefaultBlend = blend;
NotifyStreamDeckCameraStateChanged();
}
public bool IsDefaultBlendCut
{
get
{
var brain = GetCinemachineBrain();
return brain != null && brain.DefaultBlend.Style == CinemachineBlendDefinition.Styles.Cut;
}
}
public string DefaultBlendStyleName
{
get
{
var brain = GetCinemachineBrain();
return brain != null ? brain.DefaultBlend.Style.ToString() : "Unknown";
}
}
private CinemachineBrain cachedBrain;
public CinemachineBrain GetCinemachineBrain()
{
if (cachedBrain != null) return cachedBrain;
var mainCam = Camera.main;
if (mainCam != null)
{
cachedBrain = mainCam.GetComponent<CinemachineBrain>();
if (cachedBrain != null) return cachedBrain;
}
cachedBrain = FindFirstObjectByType<CinemachineBrain>();
return cachedBrain;
}
#endregion
#region Initialization
@ -1175,7 +1230,9 @@ public class CameraManager : MonoBehaviour, IController
camera_name = currentPreset.virtualCamera?.gameObject.name ?? "Unknown",
preset_name = currentPreset.presetName,
total_cameras = cameraPresets.Count,
is_drone_mode = IsDroneModeActive
is_drone_mode = IsDroneModeActive,
default_blend_style = DefaultBlendStyleName,
is_default_blend_cut = IsDefaultBlendCut
};
}
@ -1203,6 +1260,8 @@ public class CameraManager : MonoBehaviour, IController
public string preset_name;
public int total_cameras;
public bool is_drone_mode;
public string default_blend_style;
public bool is_default_blend_cut;
}
#endregion
@ -1253,6 +1312,10 @@ public class CameraManager : MonoBehaviour, IController
// 드론 상태는 GetCurrentCameraState()의 is_drone_mode로 반환됨
break;
case "toggle_default_blend":
ToggleDefaultBlend();
break;
default:
Debug.LogWarning($"[CameraManager] 알 수 없는 액션: {actionId}");
break;

View File

@ -213,6 +213,23 @@ public class RuntimeControlPanelManager
return;
}
// Default Blend 토글 (Cinemachine Brain)
var blendRow = new VisualElement();
blendRow.AddToClassList("action-row");
bool isCut = cam.IsDefaultBlendCut;
string styleName = cam.DefaultBlendStyleName;
var blendBtn = MakeButton(
$"Blend: {styleName} ▶ {(isCut ? "EaseInOut" : "Cut")}로 전환",
isCut ? "action-btn--secondary" : "action-btn--success");
blendBtn.clicked += () =>
{
cam.ToggleDefaultBlend();
SwitchCategory("camera");
};
blendRow.Add(blendBtn);
actionList.Add(blendRow);
var data = cam.GetCameraListData();
if (data?.presets == null) return;

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
@ -11,8 +12,13 @@ public class CameraManagerEditor : Editor
private const string UssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/CameraManagerEditor.uss";
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private VisualElement presetsSection;
private VisualElement presetsContainer;
private VisualElement dropZone;
private Label presetsTitleLabel;
private Button defaultBlendButton;
private Label defaultBlendStatusLabel;
private HelpBox brainNotFoundWarning;
private CameraManager manager;
public override VisualElement CreateInspectorGUI()
@ -36,6 +42,9 @@ public class CameraManagerEditor : Editor
SetupRotationTargetLogic(root);
SetupBlendTransitionLogic(root);
// Cinemachine Brain default blend toggle
SetupDefaultBlendSection(root);
// Preset list (dynamic)
BuildPresetsSection(root);
@ -162,12 +171,104 @@ public class CameraManagerEditor : Editor
#endregion
#region Cinemachine Brain
private void SetupDefaultBlendSection(VisualElement root)
{
var group = root.Q("defaultBlendGroup");
brainNotFoundWarning = root.Q<HelpBox>("brainNotFoundWarning");
if (group == null) return;
var row = new VisualElement();
row.AddToClassList("blend-toggle-row");
defaultBlendStatusLabel = new Label("현재 Default Blend:");
defaultBlendStatusLabel.AddToClassList("blend-toggle-label");
row.Add(defaultBlendStatusLabel);
defaultBlendButton = new Button(ToggleEditorDefaultBlend) { text = "-" };
defaultBlendButton.AddToClassList("blend-toggle-btn");
row.Add(defaultBlendButton);
group.Add(row);
// 인스펙터가 포커스를 가질 때 주기적으로 UI 동기화 (씬에서 직접 편집한 경우 반영)
root.schedule.Execute(UpdateDefaultBlendUI).Every(500);
UpdateDefaultBlendUI();
}
private CinemachineBrain FindBrain()
{
var mainCam = Camera.main;
if (mainCam != null)
{
var brain = mainCam.GetComponent<CinemachineBrain>();
if (brain != null) return brain;
}
return FindFirstObjectByType<CinemachineBrain>();
}
private void UpdateDefaultBlendUI()
{
if (defaultBlendButton == null) return;
var brain = FindBrain();
if (brain == null)
{
defaultBlendButton.text = "Brain 없음";
defaultBlendButton.SetEnabled(false);
defaultBlendButton.RemoveFromClassList("blend-toggle-btn--cut");
defaultBlendButton.RemoveFromClassList("blend-toggle-btn--ease");
if (defaultBlendStatusLabel != null)
defaultBlendStatusLabel.text = "CinemachineBrain을 찾을 수 없음";
if (brainNotFoundWarning != null)
brainNotFoundWarning.style.display = DisplayStyle.Flex;
return;
}
if (brainNotFoundWarning != null)
brainNotFoundWarning.style.display = DisplayStyle.None;
var style = brain.DefaultBlend.Style;
bool isCut = style == CinemachineBlendDefinition.Styles.Cut;
bool isEase = style == CinemachineBlendDefinition.Styles.EaseInOut;
if (defaultBlendStatusLabel != null)
defaultBlendStatusLabel.text = $"현재 Default Blend: {style}";
defaultBlendButton.SetEnabled(true);
defaultBlendButton.text = isCut ? "▶ EaseInOut 으로 전환" : "▶ Cut 으로 전환";
defaultBlendButton.EnableInClassList("blend-toggle-btn--cut", isCut);
defaultBlendButton.EnableInClassList("blend-toggle-btn--ease", isEase);
}
private void ToggleEditorDefaultBlend()
{
var brain = FindBrain();
if (brain == null) return;
Undo.RecordObject(brain, "Toggle CinemachineBrain Default Blend");
var blend = brain.DefaultBlend;
blend.Style = (blend.Style == CinemachineBlendDefinition.Styles.Cut)
? CinemachineBlendDefinition.Styles.EaseInOut
: CinemachineBlendDefinition.Styles.Cut;
brain.DefaultBlend = blend;
EditorUtility.SetDirty(brain);
UpdateDefaultBlendUI();
}
#endregion
#region Preset List
private void BuildPresetsSection(VisualElement root)
{
var section = root.Q("presetsSection");
if (section == null) return;
presetsSection = root.Q("presetsSection");
if (presetsSection == null) return;
// Header
var header = new VisualElement();
@ -177,19 +278,88 @@ public class CameraManagerEditor : Editor
presetsTitleLabel.AddToClassList("presets-title");
header.Add(presetsTitleLabel);
var addBtn = new Button(AddPreset) { text = "+ 프리셋 추가" };
var addBtn = new Button(AddPreset)
{
text = "+ 프리셋 추가",
tooltip = "씬의 첫 CinemachineCamera를 추가합니다. 아래 영역으로 드래그해서 추가할 수도 있습니다."
};
addBtn.AddToClassList("preset-add-btn");
header.Add(addBtn);
section.Add(header);
presetsSection.Add(header);
// Container
presetsContainer = new VisualElement();
section.Add(presetsContainer);
presetsSection.Add(presetsContainer);
// Drop zone
dropZone = new Label("CinemachineCamera를 여기로 드래그하여 추가");
dropZone.AddToClassList("preset-drop-zone");
presetsSection.Add(dropZone);
SetupDragAndDrop(presetsSection);
RebuildPresetList();
}
private void SetupDragAndDrop(VisualElement target)
{
target.RegisterCallback<DragUpdatedEvent>(evt =>
{
if (GetDraggedCameras().Count > 0)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
dropZone?.AddToClassList("preset-drop-zone--active");
evt.StopPropagation();
}
});
target.RegisterCallback<DragLeaveEvent>(_ =>
{
dropZone?.RemoveFromClassList("preset-drop-zone--active");
});
target.RegisterCallback<DragExitedEvent>(_ =>
{
dropZone?.RemoveFromClassList("preset-drop-zone--active");
});
target.RegisterCallback<DragPerformEvent>(evt =>
{
var cams = GetDraggedCameras();
dropZone?.RemoveFromClassList("preset-drop-zone--active");
if (cams.Count == 0) return;
DragAndDrop.AcceptDrag();
Undo.RecordObject(this.target, "Add Camera Preset (Drop)");
foreach (var cam in cams)
{
manager.cameraPresets.Add(new CameraManager.CameraPreset(cam));
}
EditorUtility.SetDirty(this.target);
RebuildPresetList();
evt.StopPropagation();
});
}
private List<CinemachineCamera> GetDraggedCameras()
{
var list = new List<CinemachineCamera>();
foreach (var obj in DragAndDrop.objectReferences)
{
if (obj is CinemachineCamera cc)
{
if (!list.Contains(cc)) list.Add(cc);
}
else if (obj is GameObject go)
{
var cam = go.GetComponent<CinemachineCamera>();
if (cam != null && !list.Contains(cam)) list.Add(cam);
}
}
return list;
}
private void RebuildPresetList()
{
if (presetsContainer == null || manager == null) return;
@ -226,14 +396,32 @@ public class CameraManagerEditor : Editor
var headerRow = new VisualElement();
headerRow.AddToClassList("preset-item-header");
var indexLabel = new Label($"{index + 1}");
indexLabel.AddToClassList("preset-index");
headerRow.Add(indexLabel);
int idx = index;
var indexField = new IntegerField
{
value = index + 1,
isDelayed = true,
tooltip = "카메라 순서 번호. 숫자를 입력 후 Enter를 누르면 해당 위치로 이동합니다."
};
indexField.AddToClassList("preset-index-field");
indexField.RegisterValueChangedCallback(evt =>
{
int newIndex = Mathf.Clamp(evt.newValue - 1, 0, manager.cameraPresets.Count - 1);
if (newIndex != idx)
{
MovePreset(idx, newIndex);
}
else if (evt.newValue != idx + 1)
{
indexField.SetValueWithoutNotify(idx + 1);
}
});
headerRow.Add(indexField);
var nameField = new TextField();
nameField.value = preset.presetName;
nameField.AddToClassList("preset-name-field");
int idx = index;
nameField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(target, "Rename Camera Preset");
@ -250,19 +438,24 @@ public class CameraManagerEditor : Editor
}
// Up button
var upBtn = new Button(() => SwapPresets(index, index - 1)) { text = "\u25B2" };
var upBtn = new Button(() => SwapPresets(index, index - 1)) { text = "\u25B2", tooltip = "위로 이동" };
upBtn.AddToClassList("preset-reorder-btn");
upBtn.SetEnabled(index > 0);
headerRow.Add(upBtn);
// Down button
var downBtn = new Button(() => SwapPresets(index, index + 1)) { text = "\u25BC" };
var downBtn = new Button(() => SwapPresets(index, index + 1)) { text = "\u25BC", tooltip = "아래로 이동" };
downBtn.AddToClassList("preset-reorder-btn");
downBtn.SetEnabled(index < manager.cameraPresets.Count - 1);
headerRow.Add(downBtn);
// Duplicate button
var duplicateBtn = new Button(() => DuplicatePreset(index)) { text = "\u29C9", tooltip = "프리셋 복제" };
duplicateBtn.AddToClassList("preset-duplicate-btn");
headerRow.Add(duplicateBtn);
// Delete button
var deleteBtn = new Button(() => DeletePreset(index)) { text = "X" };
var deleteBtn = new Button(() => DeletePreset(index)) { text = "X", tooltip = "프리셋 삭제" };
deleteBtn.AddToClassList("preset-delete-btn");
headerRow.Add(deleteBtn);
@ -278,11 +471,10 @@ public class CameraManagerEditor : Editor
allowSceneObjects = true,
value = preset.virtualCamera
};
int ci = index;
cameraField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(target, "Change Camera Preset Camera");
manager.cameraPresets[ci].virtualCamera = evt.newValue as CinemachineCamera;
manager.cameraPresets[idx].virtualCamera = evt.newValue as CinemachineCamera;
EditorUtility.SetDirty(target);
});
fields.Add(cameraField);
@ -292,11 +484,10 @@ public class CameraManagerEditor : Editor
tooltip = "이 카메라에서 마우스 조작(회전, 팬, 줌)을 허용할지 여부",
value = preset.allowMouseControl
};
int mi = index;
mouseToggle.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(target, "Toggle Mouse Control");
manager.cameraPresets[mi].allowMouseControl = evt.newValue;
manager.cameraPresets[idx].allowMouseControl = evt.newValue;
EditorUtility.SetDirty(target);
});
fields.Add(mouseToggle);
@ -337,6 +528,36 @@ public class CameraManagerEditor : Editor
RebuildPresetList();
}
private void MovePreset(int from, int to)
{
if (from == to) return;
if (from < 0 || from >= manager.cameraPresets.Count) return;
if (to < 0 || to >= manager.cameraPresets.Count) return;
Undo.RecordObject(target, "Move Camera Preset");
var preset = manager.cameraPresets[from];
manager.cameraPresets.RemoveAt(from);
manager.cameraPresets.Insert(to, preset);
EditorUtility.SetDirty(target);
RebuildPresetList();
}
private void DuplicatePreset(int index)
{
if (index < 0 || index >= manager.cameraPresets.Count) return;
Undo.RecordObject(target, "Duplicate Camera Preset");
var source = manager.cameraPresets[index];
var copy = new CameraManager.CameraPreset(source.virtualCamera)
{
presetName = $"{source.presetName} (복사)",
allowMouseControl = source.allowMouseControl
};
manager.cameraPresets.Insert(index + 1, copy);
EditorUtility.SetDirty(target);
RebuildPresetList();
}
#endregion
#region Play Mode State

View File

@ -1,5 +1,54 @@
/* Camera Manager Editor styles */
.blend-toggle-row {
flex-direction: row;
align-items: center;
padding: 4px 2px;
}
.blend-toggle-label {
flex-grow: 1;
font-size: 11px;
color: #d4d4d4;
}
.blend-toggle-btn {
min-width: 160px;
padding: 4px 10px;
border-radius: 4px;
border-width: 0;
font-size: 11px;
-unity-font-style: bold;
background-color: rgba(255, 255, 255, 0.1);
color: #e5e7eb;
}
.blend-toggle-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.blend-toggle-btn:disabled {
opacity: 0.5;
}
.blend-toggle-btn--cut {
background-color: rgba(234, 179, 8, 0.25);
color: #fde68a;
}
.blend-toggle-btn--cut:hover {
background-color: rgba(234, 179, 8, 0.4);
}
.blend-toggle-btn--ease {
background-color: rgba(34, 197, 94, 0.25);
color: #bbf7d0;
}
.blend-toggle-btn--ease:hover {
background-color: rgba(34, 197, 94, 0.4);
}
.presets-section {
margin-top: 12px;
}
@ -63,6 +112,18 @@
color: #94a3b8;
}
.preset-index-field {
width: 42px;
margin-right: 4px;
}
.preset-index-field > .unity-integer-field__input {
-unity-text-align: middle-center;
-unity-font-style: bold;
font-size: 11px;
padding: 0 2px;
}
.preset-name-field {
flex-grow: 1;
margin-left: 4px;
@ -97,6 +158,24 @@
opacity: 0.3;
}
.preset-duplicate-btn {
width: 24px;
height: 20px;
padding: 0;
margin: 0 1px;
border-radius: 3px;
border-width: 0;
background-color: rgba(99, 102, 241, 0.15);
color: #a5b4fc;
font-size: 12px;
-unity-text-align: middle-center;
}
.preset-duplicate-btn:hover {
background-color: rgba(99, 102, 241, 0.35);
color: #c7d2fe;
}
.preset-delete-btn {
width: 24px;
height: 20px;
@ -126,3 +205,22 @@
font-size: 11px;
-unity-font-style: italic;
}
.preset-drop-zone {
margin-top: 8px;
padding: 12px;
border-width: 1px;
border-color: rgba(255, 255, 255, 0.15);
border-radius: 6px;
background-color: rgba(255, 255, 255, 0.03);
-unity-text-align: middle-center;
color: #94a3b8;
font-size: 11px;
}
.preset-drop-zone--active {
border-color: #6366f1;
background-color: rgba(99, 102, 241, 0.15);
color: #c7d2fe;
-unity-font-style: bold;
}

View File

@ -60,6 +60,14 @@
</ui:Foldout>
</ui:VisualElement>
<!-- Cinemachine Brain Default Blend -->
<ui:VisualElement class="section">
<ui:Foldout text="Cinemachine Brain" value="true" class="section-foldout">
<ui:VisualElement name="defaultBlendGroup"/>
<ui:HelpBox name="brainNotFoundWarning" message-type="Warning" text="Main Camera에 CinemachineBrain이 없습니다. Main Camera를 설정하거나 Camera에 CinemachineBrain 컴포넌트를 추가하세요."/>
</ui:Foldout>
</ui:VisualElement>
<!-- 카메라 프리셋 (C#에서 동적 생성) -->
<ui:VisualElement name="presetsSection" class="section presets-section"/>