user 4a49ecd772 Refactor: 배경/프랍 브라우저 IMGUI→UI Toolkit 전환 + USS 리디자인
- BackgroundSceneLoaderWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField)
- PropBrowserWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField)
- StreamingleCommon.uss: 브라우저 공통 스타일 추가 (그리드/리스트/뷰토글/액션바/상태바)
- excludeFromWeb 상태 새로고침 시 보존 수정
- 삭제된 배경 리소스 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 01:55:48 +09:00

1432 lines
57 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
namespace Streamingle.Background.Editor
{
/// <summary>
/// 원래 라이팅 세팅을 저장하는 클래스
/// </summary>
[Serializable]
public class OriginalLightingSettings
{
// RenderSettings
public Material skybox;
public Light sun;
public AmbientMode ambientMode;
public Color ambientSkyColor;
public Color ambientEquatorColor;
public Color ambientGroundColor;
public Color ambientLight;
public float ambientIntensity;
public Color subtractiveShadowColor;
// Fog
public bool fog;
public Color fogColor;
public FogMode fogMode;
public float fogDensity;
public float fogStartDistance;
public float fogEndDistance;
// 원래 씬의 라이트/볼륨 활성 상태
public List<LightBackup> originalLights = new List<LightBackup>();
public List<VolumeBackup> originalVolumes = new List<VolumeBackup>();
public List<DirectionalLightBackup> originalDirectionalLights = new List<DirectionalLightBackup>();
public List<ComponentBackup> originalNiloToonOverriders = new List<ComponentBackup>();
[Serializable]
public class LightBackup
{
public Light light;
public bool wasEnabled;
}
[Serializable]
public class VolumeBackup
{
public Volume volume;
public bool wasEnabled;
}
[Serializable]
public class DirectionalLightBackup
{
public Light light;
public bool wasEnabled;
}
[Serializable]
public class ComponentBackup
{
public MonoBehaviour component;
public bool wasEnabled;
}
public void Capture(Scene scene)
{
skybox = RenderSettings.skybox;
sun = RenderSettings.sun;
ambientMode = RenderSettings.ambientMode;
ambientSkyColor = RenderSettings.ambientSkyColor;
ambientEquatorColor = RenderSettings.ambientEquatorColor;
ambientGroundColor = RenderSettings.ambientGroundColor;
ambientLight = RenderSettings.ambientLight;
ambientIntensity = RenderSettings.ambientIntensity;
subtractiveShadowColor = RenderSettings.subtractiveShadowColor;
fog = RenderSettings.fog;
fogColor = RenderSettings.fogColor;
fogMode = RenderSettings.fogMode;
fogDensity = RenderSettings.fogDensity;
fogStartDistance = RenderSettings.fogStartDistance;
fogEndDistance = RenderSettings.fogEndDistance;
originalLights.Clear();
originalVolumes.Clear();
originalDirectionalLights.Clear();
originalNiloToonOverriders.Clear();
var roots = scene.GetRootGameObjects();
foreach (var root in roots)
{
if (root.name == "[Background Lighting]") continue;
var lights = root.GetComponentsInChildren<Light>(true);
foreach (var light in lights)
{
originalLights.Add(new LightBackup { light = light, wasEnabled = light.enabled });
if (light.type == LightType.Directional)
{
originalDirectionalLights.Add(new DirectionalLightBackup { light = light, wasEnabled = light.enabled });
}
}
var volumes = root.GetComponentsInChildren<Volume>(true);
foreach (var volume in volumes)
{
originalVolumes.Add(new VolumeBackup { volume = volume, wasEnabled = volume.enabled });
}
var niloToonOverriders = root.GetComponentsInChildren<MonoBehaviour>(true)
.Where(mb => mb != null && mb.GetType().Name == "NiloToonCharacterMainLightOverrider");
foreach (var overrider in niloToonOverriders)
{
originalNiloToonOverriders.Add(new ComponentBackup { component = overrider, wasEnabled = overrider.enabled });
}
}
}
public void DisableOriginalLighting()
{
foreach (var backup in originalDirectionalLights)
if (backup.light != null) backup.light.enabled = false;
foreach (var backup in originalNiloToonOverriders)
if (backup.component != null) backup.component.enabled = false;
int lightCount = originalDirectionalLights.Count(b => b.light != null);
int overriderCount = originalNiloToonOverriders.Count(b => b.component != null);
if (lightCount > 0 || overriderCount > 0)
Debug.Log($"기존 씬 라이팅 비활성화: Directional Light {lightCount}개, NiloToonOverrider {overriderCount}개");
}
public void RestoreOriginalLighting()
{
foreach (var backup in originalDirectionalLights)
if (backup.light != null) backup.light.enabled = backup.wasEnabled;
foreach (var backup in originalNiloToonOverriders)
if (backup.component != null) backup.component.enabled = backup.wasEnabled;
int lightCount = originalDirectionalLights.Count(b => b.light != null);
int overriderCount = originalNiloToonOverriders.Count(b => b.component != null);
if (lightCount > 0 || overriderCount > 0)
Debug.Log($"기존 씬 라이팅 복원: Directional Light {lightCount}개, NiloToonOverrider {overriderCount}개");
}
public void Restore()
{
RenderSettings.skybox = skybox;
RenderSettings.sun = sun;
RenderSettings.ambientMode = ambientMode;
RenderSettings.ambientSkyColor = ambientSkyColor;
RenderSettings.ambientEquatorColor = ambientEquatorColor;
RenderSettings.ambientGroundColor = ambientGroundColor;
RenderSettings.ambientLight = ambientLight;
RenderSettings.ambientIntensity = ambientIntensity;
RenderSettings.subtractiveShadowColor = subtractiveShadowColor;
RenderSettings.fog = fog;
RenderSettings.fogColor = fogColor;
RenderSettings.fogMode = fogMode;
RenderSettings.fogDensity = fogDensity;
RenderSettings.fogStartDistance = fogStartDistance;
RenderSettings.fogEndDistance = fogEndDistance;
foreach (var backup in originalLights)
if (backup.light != null) backup.light.enabled = backup.wasEnabled;
foreach (var backup in originalVolumes)
if (backup.volume != null) backup.volume.enabled = backup.wasEnabled;
RestoreOriginalLighting();
}
}
/// <summary>
/// 배경 씬 로더 에디터 윈도우 (UI Toolkit)
/// </summary>
public class BackgroundSceneLoaderWindow : EditorWindow
{
private const string BACKGROUND_PATH = "Assets/ResourcesData/Background";
private const string DATABASE_PATH = "Assets/Resources/Settings/BackgroundSceneDatabase.asset";
private const int THUMBNAIL_SIZE = 128;
private BackgroundSceneDatabase _database;
private Dictionary<string, List<BackgroundSceneInfo>> _categorizedScenes;
private Dictionary<string, bool> _categoryFoldouts = new Dictionary<string, bool>();
private Dictionary<string, Texture2D> _thumbnailCache = new Dictionary<string, Texture2D>();
private string _searchFilter = "";
private string _selectedCategory = "전체";
private int _viewMode = 0; // 0: 그리드, 1: 리스트
private bool _isLoading;
private Scene? _loadedBackgroundScene;
private string _currentSceneName = "없음";
// 라이팅 옵션
private bool _copyLighting = true;
private bool _copySkybox = true;
private bool _copyAmbient = true;
private bool _copyFog = true;
private bool _autoRestoreOnUnload = true;
private OriginalLightingSettings _originalLightingSettings = new OriginalLightingSettings();
private bool _hasBackup = false;
// UI 참조
private ToolbarSearchField _searchField;
private DropdownField _categoryDropdown;
private Button _gridBtn, _listBtn;
private ScrollView _scrollView;
private Label _statusLabel;
private Button _unloadBtn;
private Foldout _lightingFoldout;
private Toggle _copyLightingToggle, _copySkyboxToggle, _copyAmbientToggle, _copyFogToggle, _autoRestoreToggle;
private VisualElement _subToggles;
private Label _backupInfoLabel;
private Button _copyLightingBtn, _restoreBtn;
[MenuItem("Streamingle/Background Scene Loader %#b")]
public static void ShowWindow()
{
var window = GetWindow<BackgroundSceneLoaderWindow>("배경 씬 로더");
window.minSize = new Vector2(400, 500);
window.Show();
}
public void CreateGUI()
{
var root = rootVisualElement;
// 공용 USS 로드
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(
"Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss");
if (uss != null) root.styleSheets.Add(uss);
root.AddToClassList("tool-root");
root.style.paddingTop = 0;
root.style.paddingBottom = 0;
root.style.paddingLeft = 0;
root.style.paddingRight = 0;
LoadOrCreateDatabase();
BuildUI(root);
RegisterEvents();
UpdateCurrentSceneStatus();
RebuildContent();
root.RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
}
private static Button MakeBarButton(string text, System.Action onClick)
{
var btn = new Button(onClick) { text = text };
btn.style.height = 26;
btn.style.paddingLeft = 14;
btn.style.paddingRight = 14;
btn.style.fontSize = 12;
return btn;
}
private void BuildUI(VisualElement root)
{
// ── 검색 툴바 (Toolbar 사용 — ToolbarSearchField 정상 렌더링) ──
var toolbar = new Toolbar();
toolbar.style.borderBottomWidth = 1;
toolbar.style.borderBottomColor = new Color(1f, 1f, 1f, 0.06f);
_searchField = new ToolbarSearchField();
_searchField.style.flexGrow = 1;
_searchField.RegisterValueChangedCallback(OnSearchChanged);
toolbar.Add(_searchField);
var refreshBtn = new ToolbarButton(OnRefreshClicked) { text = "새로고침" };
toolbar.Add(refreshBtn);
root.Add(toolbar);
// ── 컨트롤 바 ──
var controlsRow = new VisualElement();
controlsRow.AddToClassList("browser-controls-row");
controlsRow.style.minHeight = 38;
var catLabel = new Label("카테고리");
catLabel.style.fontSize = 12;
catLabel.style.marginRight = 4;
controlsRow.Add(catLabel);
_categoryDropdown = new DropdownField();
_categoryDropdown.style.minWidth = 120;
_categoryDropdown.style.height = 26;
_categoryDropdown.RegisterValueChangedCallback(OnCategoryChanged);
controlsRow.Add(_categoryDropdown);
controlsRow.Add(new VisualElement { style = { flexGrow = 1 } });
var viewGroup = new VisualElement();
viewGroup.AddToClassList("view-toggle-group");
viewGroup.style.height = 28;
_gridBtn = new Button(() => SetViewMode(0)) { text = "그리드" };
_gridBtn.AddToClassList("view-btn");
_gridBtn.style.height = 24;
viewGroup.Add(_gridBtn);
_listBtn = new Button(() => SetViewMode(1)) { text = "리스트" };
_listBtn.AddToClassList("view-btn");
_listBtn.style.height = 24;
viewGroup.Add(_listBtn);
controlsRow.Add(viewGroup);
root.Add(controlsRow);
// ── 스크롤 콘텐츠 ──
_scrollView = new ScrollView(ScrollViewMode.Vertical);
_scrollView.AddToClassList("browser-scroll");
root.Add(_scrollView);
// ── 라이팅 옵션 ──
BuildLightingOptions(root);
// ── 상태바 ──
var statusBar = new VisualElement();
statusBar.AddToClassList("status-bar");
statusBar.style.minHeight = 40;
_statusLabel = new Label("현재 배경: 없음");
_statusLabel.AddToClassList("status-label");
_statusLabel.style.fontSize = 12;
statusBar.Add(_statusLabel);
statusBar.Add(new VisualElement { style = { flexGrow = 1 } });
var buildBtn = MakeBarButton("빌드 세팅", ShowBuildSettingsMenu);
statusBar.Add(buildBtn);
_unloadBtn = MakeBarButton("배경 언로드", UnloadCurrentBackground);
_unloadBtn.AddToClassList("btn-danger");
_unloadBtn.style.height = 26;
statusBar.Add(_unloadBtn);
root.Add(statusBar);
UpdateCategoryDropdown();
UpdateViewModeButtons();
}
private void BuildLightingOptions(VisualElement root)
{
_lightingFoldout = new Foldout { text = "라이팅 복사 옵션", value = false };
_lightingFoldout.AddToClassList("category-foldout");
var container = new VisualElement();
container.AddToClassList("lighting-options");
_copyLightingToggle = new Toggle("라이팅 복사 활성화") { value = _copyLighting };
_copyLightingToggle.RegisterValueChangedCallback(evt =>
{
_copyLighting = evt.newValue;
_subToggles.SetEnabled(_copyLighting);
});
container.Add(_copyLightingToggle);
_subToggles = new VisualElement();
_subToggles.AddToClassList("sub-toggles");
_copySkyboxToggle = new Toggle("Skybox") { value = _copySkybox };
_copySkyboxToggle.RegisterValueChangedCallback(evt => _copySkybox = evt.newValue);
_subToggles.Add(_copySkyboxToggle);
_copyAmbientToggle = new Toggle("Ambient (환경광)") { value = _copyAmbient };
_copyAmbientToggle.RegisterValueChangedCallback(evt => _copyAmbient = evt.newValue);
_subToggles.Add(_copyAmbientToggle);
_copyFogToggle = new Toggle("Fog (안개)") { value = _copyFog };
_copyFogToggle.RegisterValueChangedCallback(evt => _copyFog = evt.newValue);
_subToggles.Add(_copyFogToggle);
container.Add(_subToggles);
_autoRestoreToggle = new Toggle("배경 언로드 시 자동 복원") { value = _autoRestoreOnUnload };
_autoRestoreToggle.RegisterValueChangedCallback(evt => _autoRestoreOnUnload = evt.newValue);
container.Add(_autoRestoreToggle);
_backupInfoLabel = new Label("원래 라이팅 세팅이 백업되었습니다. '원래대로 복원' 버튼으로 복원할 수 있습니다.");
_backupInfoLabel.AddToClassList("backup-info");
_backupInfoLabel.style.display = DisplayStyle.None;
container.Add(_backupInfoLabel);
var btnRow = new VisualElement();
btnRow.AddToClassList("lighting-buttons");
_copyLightingBtn = new Button(CopyLightingFromCurrentBackground) { text = "현재 배경에서 라이팅 복사" };
_copyLightingBtn.AddToClassList("btn-primary");
btnRow.Add(_copyLightingBtn);
_restoreBtn = new Button(RestoreOriginalLighting) { text = "원래대로 복원" };
btnRow.Add(_restoreBtn);
container.Add(btnRow);
_lightingFoldout.Add(container);
root.Add(_lightingFoldout);
}
// ─── 이벤트 관리 ───
private void RegisterEvents()
{
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
EditorSceneManager.sceneOpened += OnSceneOpened;
EditorSceneManager.sceneClosed += OnSceneClosed;
}
private void OnDetachFromPanel(DetachFromPanelEvent evt)
{
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
EditorSceneManager.sceneOpened -= OnSceneOpened;
EditorSceneManager.sceneClosed -= OnSceneClosed;
ClearThumbnailCache();
}
private void OnSceneOpened(Scene scene, OpenSceneMode mode)
{
UpdateCurrentSceneStatus();
if (!string.IsNullOrEmpty(scene.path) && scene.path.Replace("\\", "/").Contains(BACKGROUND_PATH))
{
var sceneInfo = _database?.FindByPath(scene.path);
if (sceneInfo == null) RefreshSceneList();
}
RebuildContent();
}
private void OnSceneClosed(Scene scene)
{
UpdateCurrentSceneStatus();
RebuildContent();
}
private void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredEditMode || state == PlayModeStateChange.EnteredPlayMode)
{
UpdateCurrentSceneStatus();
RebuildContent();
}
}
private void OnRefreshClicked()
{
RefreshSceneList();
RefreshAllThumbnails();
UpdateCategoryDropdown();
RebuildContent();
}
private void OnSearchChanged(ChangeEvent<string> evt)
{
_searchFilter = evt.newValue;
RebuildContent();
}
private void OnCategoryChanged(ChangeEvent<string> evt)
{
_selectedCategory = evt.newValue ?? "전체";
RebuildContent();
}
private void SetViewMode(int mode)
{
_viewMode = mode;
UpdateViewModeButtons();
RebuildContent();
}
private void UpdateViewModeButtons()
{
_gridBtn.EnableInClassList("view-btn--active", _viewMode == 0);
_listBtn.EnableInClassList("view-btn--active", _viewMode == 1);
}
private void UpdateCategoryDropdown()
{
var categories = new List<string> { "전체" };
if (_categorizedScenes != null)
categories.AddRange(_categorizedScenes.Keys.OrderBy(x => x));
_categoryDropdown.choices = categories;
if (!categories.Contains(_selectedCategory))
_selectedCategory = "전체";
_categoryDropdown.SetValueWithoutNotify(_selectedCategory);
}
// ─── 콘텐츠 빌드 ───
private void RebuildContent()
{
if (_scrollView == null) return;
_scrollView.Clear();
if (_database == null || _categorizedScenes == null)
{
_scrollView.Add(new Label("데이터베이스를 로드하는 중...") { pickingMode = PickingMode.Ignore });
_scrollView.Q<Label>().AddToClassList("browser-empty");
return;
}
if (_database.scenes.Count == 0)
{
_scrollView.Add(new Label("배경 씬이 없습니다.\n새로고침 버튼을 눌러 씬을 검색하세요.") { pickingMode = PickingMode.Ignore });
_scrollView.Q<Label>().AddToClassList("browser-empty");
return;
}
var filteredScenes = GetFilteredScenes();
if (_viewMode == 0)
BuildGridView(filteredScenes);
else
BuildListView(filteredScenes);
UpdateStatusBar();
}
private Dictionary<string, List<BackgroundSceneInfo>> GetFilteredScenes()
{
var result = new Dictionary<string, List<BackgroundSceneInfo>>();
foreach (var kvp in _categorizedScenes)
{
if (_selectedCategory != "전체" && kvp.Key != _selectedCategory) continue;
var scenes = kvp.Value;
if (!string.IsNullOrEmpty(_searchFilter))
{
scenes = scenes.Where(s =>
s.sceneName.IndexOf(_searchFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
s.categoryName.IndexOf(_searchFilter, StringComparison.OrdinalIgnoreCase) >= 0
).ToList();
}
if (scenes.Count > 0)
result[kvp.Key] = scenes;
}
return result;
}
private void BuildGridView(Dictionary<string, List<BackgroundSceneInfo>> scenes)
{
foreach (var kvp in scenes.OrderBy(x => x.Key))
{
string category = kvp.Key;
var sceneList = kvp.Value;
if (!_categoryFoldouts.ContainsKey(category))
_categoryFoldouts[category] = true;
var foldout = new Foldout { text = $"{category} ({sceneList.Count})", value = _categoryFoldouts[category] };
foldout.AddToClassList("category-foldout");
foldout.RegisterValueChangedCallback(evt => _categoryFoldouts[category] = evt.newValue);
var gridContainer = new VisualElement();
gridContainer.AddToClassList("grid-container");
foreach (var sceneInfo in sceneList)
{
var item = CreateGridItem(sceneInfo);
gridContainer.Add(item);
}
foldout.Add(gridContainer);
_scrollView.Add(foldout);
}
}
private VisualElement CreateGridItem(BackgroundSceneInfo sceneInfo)
{
var item = new VisualElement();
item.AddToClassList("grid-item");
bool isCurrentScene = _currentSceneName == sceneInfo.sceneName;
item.EnableInClassList("grid-item--active", isCurrentScene);
// 썸네일
var thumbContainer = new VisualElement();
thumbContainer.AddToClassList("grid-thumbnail");
var thumbnail = GetThumbnail(sceneInfo);
if (thumbnail != null)
{
thumbContainer.style.backgroundImage = new StyleBackground(thumbnail);
}
else
{
var noThumb = new Label("No\nThumbnail");
noThumb.AddToClassList("no-thumbnail");
thumbContainer.Add(noThumb);
}
// 웹 제외 배지
if (sceneInfo.excludeFromWeb)
{
var badge = new Label("X");
badge.AddToClassList("exclude-badge");
thumbContainer.Add(badge);
}
item.Add(thumbContainer);
// 라벨
var label = new Label(sceneInfo.sceneName);
label.AddToClassList("grid-label");
item.Add(label);
// 더블클릭 → 로드
item.RegisterCallback<ClickEvent>(evt =>
{
if (evt.clickCount == 2)
{
LoadScene(sceneInfo);
evt.StopPropagation();
}
});
// 우클릭 → 컨텍스트 메뉴
item.AddManipulator(new ContextualMenuManipulator(menuEvt =>
{
menuEvt.menu.AppendAction("로드 (Additive)", _ => LoadScene(sceneInfo, LoadSceneMode.Additive));
menuEvt.menu.AppendAction("로드 (Single)", _ => LoadScene(sceneInfo, LoadSceneMode.Single));
menuEvt.menu.AppendSeparator();
menuEvt.menu.AppendAction("프로젝트에서 선택", _ => SelectInProject(sceneInfo));
menuEvt.menu.AppendAction("폴더 열기", _ => RevealInFinder(sceneInfo));
menuEvt.menu.AppendSeparator();
menuEvt.menu.AppendAction("썸네일 생성", _ => CreateThumbnail(sceneInfo));
menuEvt.menu.AppendSeparator();
menuEvt.menu.AppendAction("웹사이트 업로드 제외",
_ => ToggleExcludeFromWeb(sceneInfo),
sceneInfo.excludeFromWeb ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal);
}));
return item;
}
private void BuildListView(Dictionary<string, List<BackgroundSceneInfo>> scenes)
{
foreach (var kvp in scenes.OrderBy(x => x.Key))
{
string category = kvp.Key;
var sceneList = kvp.Value;
if (!_categoryFoldouts.ContainsKey(category))
_categoryFoldouts[category] = true;
var foldout = new Foldout { text = $"{category} ({sceneList.Count})", value = _categoryFoldouts[category] };
foldout.AddToClassList("category-foldout");
foldout.RegisterValueChangedCallback(evt => _categoryFoldouts[category] = evt.newValue);
foreach (var sceneInfo in sceneList)
{
var row = CreateListRow(sceneInfo);
foldout.Add(row);
}
_scrollView.Add(foldout);
}
}
private VisualElement CreateListRow(BackgroundSceneInfo sceneInfo)
{
bool isCurrentScene = _currentSceneName == sceneInfo.sceneName;
var row = new VisualElement();
row.AddToClassList("list-row");
row.EnableInClassList("list-row--active", isCurrentScene);
// 썸네일
var thumb = new VisualElement();
thumb.AddToClassList("list-thumbnail");
var thumbnail = GetThumbnail(sceneInfo);
if (thumbnail != null)
thumb.style.backgroundImage = new StyleBackground(thumbnail);
row.Add(thumb);
// 정보
var info = new VisualElement();
info.AddToClassList("list-row-info");
var nameRow = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
var nameLabel = new Label(sceneInfo.sceneName);
nameLabel.AddToClassList("list-row-name");
nameLabel.EnableInClassList("list-row-name--active", isCurrentScene);
nameRow.Add(nameLabel);
if (sceneInfo.excludeFromWeb)
{
var excludeLabel = new Label("[제외]");
excludeLabel.style.color = new Color(0.9f, 0.3f, 0.3f);
excludeLabel.style.fontSize = 10;
excludeLabel.style.marginLeft = 6;
nameRow.Add(excludeLabel);
}
info.Add(nameRow);
var detailLabel = new Label(sceneInfo.categoryName);
detailLabel.AddToClassList("list-row-detail");
info.Add(detailLabel);
row.Add(info);
// 버튼
var buttons = new VisualElement();
buttons.AddToClassList("list-row-buttons");
var loadBtn = new Button(() => LoadScene(sceneInfo))
{
text = isCurrentScene ? "로드됨" : "로드"
};
if (isCurrentScene) loadBtn.AddToClassList("btn-loaded");
buttons.Add(loadBtn);
var menuBtn = new Button() { text = "..." };
buttons.Add(menuBtn);
menuBtn.AddManipulator(new ContextualMenuManipulator(menuEvt =>
{
menuEvt.menu.AppendAction("로드 (Additive)", _ => LoadScene(sceneInfo, LoadSceneMode.Additive));
menuEvt.menu.AppendAction("로드 (Single)", _ => LoadScene(sceneInfo, LoadSceneMode.Single));
menuEvt.menu.AppendSeparator();
menuEvt.menu.AppendAction("프로젝트에서 선택", _ => SelectInProject(sceneInfo));
menuEvt.menu.AppendAction("폴더 열기", _ => RevealInFinder(sceneInfo));
menuEvt.menu.AppendSeparator();
menuEvt.menu.AppendAction("썸네일 생성", _ => CreateThumbnail(sceneInfo));
menuEvt.menu.AppendSeparator();
menuEvt.menu.AppendAction("웹사이트 업로드 제외",
_ => ToggleExcludeFromWeb(sceneInfo),
sceneInfo.excludeFromWeb ? DropdownMenuAction.Status.Checked : DropdownMenuAction.Status.Normal);
}));
row.Add(buttons);
// 더블클릭 → 로드
row.RegisterCallback<ClickEvent>(evt =>
{
if (evt.clickCount == 2)
{
LoadScene(sceneInfo);
evt.StopPropagation();
}
});
return row;
}
private void UpdateStatusBar()
{
if (_statusLabel != null)
_statusLabel.text = $"현재 배경: {_currentSceneName}";
if (_unloadBtn != null)
_unloadBtn.SetEnabled(_loadedBackgroundScene.HasValue && !_isLoading);
if (_backupInfoLabel != null)
_backupInfoLabel.style.display = _hasBackup ? DisplayStyle.Flex : DisplayStyle.None;
if (_restoreBtn != null)
_restoreBtn.SetEnabled(_hasBackup);
}
// ─── 데이터베이스 / 씬 관리 (기존 로직 100% 유지) ───
private void LoadOrCreateDatabase()
{
_database = AssetDatabase.LoadAssetAtPath<BackgroundSceneDatabase>(DATABASE_PATH);
if (_database == null)
{
string dirPath = Path.GetDirectoryName(DATABASE_PATH);
if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
_database = CreateInstance<BackgroundSceneDatabase>();
AssetDatabase.CreateAsset(_database, DATABASE_PATH);
AssetDatabase.SaveAssets();
}
RefreshSceneList();
}
private string FindSceneFolderWithCorrectCase(string parentFolder)
{
var subDirs = Directory.GetDirectories(parentFolder);
foreach (var dir in subDirs)
{
string dirName = Path.GetFileName(dir);
if (string.Equals(dirName, "Scene", StringComparison.OrdinalIgnoreCase))
return dir;
}
return null;
}
private void RefreshSceneList()
{
if (_database == null) return;
// excludeFromWeb 상태 보존
var excludeMap = new Dictionary<string, bool>();
foreach (var scene in _database.scenes)
if (scene.excludeFromWeb)
excludeMap[scene.scenePath] = true;
_database.scenes.Clear();
if (!Directory.Exists(BACKGROUND_PATH))
{
Debug.LogWarning($"배경 폴더가 존재하지 않습니다: {BACKGROUND_PATH}");
return;
}
var backgroundFolders = Directory.GetDirectories(BACKGROUND_PATH);
foreach (var folderPath in backgroundFolders)
{
string folderName = Path.GetFileName(folderPath);
string sceneFolderPath = FindSceneFolderWithCorrectCase(folderPath) ?? folderPath;
var sceneFiles = Directory.GetFiles(sceneFolderPath, "*.unity", SearchOption.AllDirectories);
foreach (var sceneFile in sceneFiles)
{
if (sceneFile.Contains("SkyBox")) continue;
string assetPath = sceneFile.Replace("\\", "/");
string sceneName = Path.GetFileNameWithoutExtension(sceneFile);
string sceneDir = Path.GetDirectoryName(sceneFile);
string thumbnailPath = FindThumbnail(sceneDir, sceneName);
var info = new BackgroundSceneInfo
{
sceneName = sceneName,
scenePath = assetPath,
categoryName = folderName,
thumbnailPath = thumbnailPath
};
// 보존된 excludeFromWeb 상태 복원
if (excludeMap.ContainsKey(assetPath))
info.excludeFromWeb = true;
_database.scenes.Add(info);
}
}
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
_categorizedScenes = _database.GetScenesByCategory();
foreach (var category in _categorizedScenes.Keys)
if (!_categoryFoldouts.ContainsKey(category))
_categoryFoldouts[category] = true;
Debug.Log($"배경 씬 {_database.scenes.Count}개를 로드했습니다.");
}
private string FindThumbnail(string directory, string sceneName)
{
string[] extensions = { ".png", ".jpg", ".jpeg" };
foreach (var ext in extensions)
{
string thumbnailPath = Path.Combine(directory, sceneName + ext);
if (File.Exists(thumbnailPath))
return thumbnailPath.Replace("\\", "/");
}
return null;
}
private Texture2D GetThumbnail(BackgroundSceneInfo sceneInfo)
{
if (string.IsNullOrEmpty(sceneInfo.thumbnailPath)) return null;
if (_thumbnailCache.TryGetValue(sceneInfo.thumbnailPath, out var cached)) return cached;
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(sceneInfo.thumbnailPath);
if (texture != null) _thumbnailCache[sceneInfo.thumbnailPath] = texture;
return texture;
}
private void ClearThumbnailCache() => _thumbnailCache.Clear();
public void RefreshThumbnail(string thumbnailPath)
{
if (string.IsNullOrEmpty(thumbnailPath)) return;
if (_thumbnailCache.ContainsKey(thumbnailPath))
{
var oldTexture = _thumbnailCache[thumbnailPath];
if (oldTexture != null) Resources.UnloadAsset(oldTexture);
_thumbnailCache.Remove(thumbnailPath);
}
var importer = AssetImporter.GetAtPath(thumbnailPath);
if (importer != null) importer.SaveAndReimport();
else AssetDatabase.ImportAsset(thumbnailPath, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>(thumbnailPath);
if (texture != null) _thumbnailCache[thumbnailPath] = texture;
RebuildContent();
}
public void RefreshAllThumbnails()
{
foreach (var kvp in _thumbnailCache)
if (kvp.Value != null) Resources.UnloadAsset(kvp.Value);
_thumbnailCache.Clear();
if (_database != null)
{
AssetDatabase.StartAssetEditing();
try
{
foreach (var scene in _database.scenes)
{
if (!string.IsNullOrEmpty(scene.thumbnailPath))
{
var importer = AssetImporter.GetAtPath(scene.thumbnailPath);
if (importer != null) EditorUtility.SetDirty(importer);
AssetDatabase.ImportAsset(scene.thumbnailPath, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
}
}
}
finally { AssetDatabase.StopAssetEditing(); }
}
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
Debug.Log("모든 썸네일이 새로고침되었습니다.");
}
// ─── 씬 로드/언로드 (기존 로직 100% 유지) ───
private void ToggleExcludeFromWeb(BackgroundSceneInfo sceneInfo)
{
sceneInfo.excludeFromWeb = !sceneInfo.excludeFromWeb;
EditorUtility.SetDirty(_database);
AssetDatabase.SaveAssets();
Debug.Log($"[BackgroundSceneLoader] '{sceneInfo.sceneName}' 웹 업로드 {(sceneInfo.excludeFromWeb ? "" : "")}");
RebuildContent();
}
private void LoadScene(BackgroundSceneInfo sceneInfo, LoadSceneMode mode = LoadSceneMode.Additive)
{
if (_isLoading)
{
Debug.LogWarning("이미 씬을 로드 중입니다.");
return;
}
if (!Application.isPlaying && EditorSceneManager.GetActiveScene().isDirty)
{
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return;
}
try
{
_isLoading = true;
if (Application.isPlaying)
LoadSceneRuntime(sceneInfo, mode);
else
LoadSceneEditor(sceneInfo, mode);
}
catch (Exception ex)
{
Debug.LogError($"씬 로드 실패: {ex.Message}");
}
finally
{
_isLoading = false;
UpdateCurrentSceneStatus();
RebuildContent();
}
}
private void LoadSceneEditor(BackgroundSceneInfo sceneInfo, LoadSceneMode mode)
{
if (IsSceneAlreadyLoaded(sceneInfo.scenePath))
{
Debug.Log($"[BackgroundSceneLoader] 이미 로드된 씬입니다: {sceneInfo.sceneName}");
var existingScene = SceneManager.GetSceneByPath(sceneInfo.scenePath);
_loadedBackgroundScene = existingScene;
_currentSceneName = sceneInfo.sceneName;
return;
}
if (mode == LoadSceneMode.Additive) UnloadAllBackgroundScenes();
var openMode = mode == LoadSceneMode.Additive ? OpenSceneMode.Additive : OpenSceneMode.Single;
var scene = EditorSceneManager.OpenScene(sceneInfo.scenePath, openMode);
if (mode == LoadSceneMode.Additive)
{
_loadedBackgroundScene = scene;
if (_copyLighting)
{
var activeScene = SceneManager.GetActiveScene();
if (activeScene.isLoaded && activeScene.path != scene.path)
{
if (!_hasBackup)
{
_originalLightingSettings.Capture(activeScene);
_hasBackup = true;
Debug.Log("원래 라이팅 세팅이 백업되었습니다.");
}
_originalLightingSettings.DisableOriginalLighting();
CopyLightingElements(scene, activeScene);
Debug.Log($"라이팅 자동 복사됨: {sceneInfo.sceneName}");
}
}
}
_currentSceneName = sceneInfo.sceneName;
Debug.Log($"배경 씬 로드됨: {sceneInfo.sceneName}");
}
private void LoadSceneRuntime(BackgroundSceneInfo sceneInfo, LoadSceneMode mode)
{
if (IsSceneAlreadyLoaded(sceneInfo.scenePath))
{
Debug.Log($"[BackgroundSceneLoader] [PlayMode] 이미 로드된 씬입니다: {sceneInfo.sceneName}");
var existingScene = SceneManager.GetSceneByPath(sceneInfo.scenePath);
_loadedBackgroundScene = existingScene;
_currentSceneName = sceneInfo.sceneName;
return;
}
if (mode == LoadSceneMode.Additive) UnloadAllBackgroundScenes();
if (mode == LoadSceneMode.Additive && _copyLighting)
{
var activeScene = SceneManager.GetActiveScene();
if (activeScene.isLoaded)
{
if (!_hasBackup)
{
_originalLightingSettings.Capture(activeScene);
_hasBackup = true;
Debug.Log("[PlayMode] 원래 라이팅 세팅이 백업되었습니다.");
}
_originalLightingSettings.DisableOriginalLighting();
}
}
bool sceneInBuild = false;
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
{
if (SceneUtility.GetScenePathByBuildIndex(i) == sceneInfo.scenePath)
{
sceneInBuild = true;
break;
}
}
if (sceneInBuild)
SceneManager.LoadSceneAsync(sceneInfo.scenePath, mode);
else
{
Debug.Log($"[PlayMode] 빌드 세팅에 없는 씬을 에디터 모드로 로드: {sceneInfo.sceneName}");
EditorSceneManager.LoadSceneInPlayMode(sceneInfo.scenePath, new LoadSceneParameters(mode));
}
SceneManager.sceneLoaded += OnRuntimeSceneLoaded;
_currentSceneName = sceneInfo.sceneName;
}
private void OnRuntimeSceneLoaded(Scene scene, LoadSceneMode mode)
{
SceneManager.sceneLoaded -= OnRuntimeSceneLoaded;
var sceneInfo = _database?.FindByPath(scene.path);
if (sceneInfo != null)
{
_loadedBackgroundScene = scene;
if (_copyLighting && mode == LoadSceneMode.Additive)
{
var activeScene = SceneManager.GetActiveScene();
if (activeScene.isLoaded && activeScene.path != scene.path)
{
CopyLightingElements(scene, activeScene);
Debug.Log($"[PlayMode] 라이팅 자동 복사됨: {sceneInfo.sceneName}");
}
}
Debug.Log($"[PlayMode] 배경 씬 로드됨: {sceneInfo.sceneName}");
}
RebuildContent();
}
private void UpdateCurrentSceneStatus()
{
if (_database == null) return;
var loadedBackgroundScenes = new List<Scene>();
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (!scene.isLoaded) continue;
if (_database.FindByPath(scene.path) != null)
loadedBackgroundScenes.Add(scene);
}
if (loadedBackgroundScenes.Count > 1)
{
Debug.LogWarning($"[BackgroundSceneLoader] 중복 배경 씬 감지: {loadedBackgroundScenes.Count}개. 첫 번째만 유지합니다.");
for (int i = 1; i < loadedBackgroundScenes.Count; i++)
{
var duplicateScene = loadedBackgroundScenes[i];
Debug.Log($"[BackgroundSceneLoader] 중복 씬 언로드: {duplicateScene.name}");
if (Application.isPlaying)
SceneManager.UnloadSceneAsync(duplicateScene);
else
EditorSceneManager.CloseScene(duplicateScene, true);
}
}
if (loadedBackgroundScenes.Count > 0)
{
var firstScene = loadedBackgroundScenes[0];
_loadedBackgroundScene = firstScene;
_currentSceneName = _database.FindByPath(firstScene.path)?.sceneName ?? firstScene.name;
}
else
{
_loadedBackgroundScene = null;
_currentSceneName = "없음";
}
}
private void UnloadAllBackgroundScenes()
{
if (_database == null) return;
var scenesToUnload = new List<Scene>();
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (!scene.isLoaded) continue;
if (_database.FindByPath(scene.path) != null)
scenesToUnload.Add(scene);
}
foreach (var scene in scenesToUnload)
{
Debug.Log($"[BackgroundSceneLoader] 기존 배경 씬 언로드: {scene.name}");
if (Application.isPlaying)
SceneManager.UnloadSceneAsync(scene);
else
EditorSceneManager.CloseScene(scene, true);
}
}
private bool IsSceneAlreadyLoaded(string scenePath)
{
for (int i = 0; i < SceneManager.sceneCount; i++)
{
var scene = SceneManager.GetSceneAt(i);
if (scene.path == scenePath && scene.isLoaded) return true;
}
return false;
}
private void SelectInProject(BackgroundSceneInfo sceneInfo)
{
var asset = AssetDatabase.LoadAssetAtPath<SceneAsset>(sceneInfo.scenePath);
if (asset != null)
{
Selection.activeObject = asset;
EditorGUIUtility.PingObject(asset);
}
}
private void RevealInFinder(BackgroundSceneInfo sceneInfo) => EditorUtility.RevealInFinder(sceneInfo.scenePath);
private void CreateThumbnail(BackgroundSceneInfo sceneInfo)
{
EditorUtility.DisplayDialog("썸네일 생성",
"씬을 로드한 후 Game 뷰에서 원하는 앵글을 설정하고\n" +
"Streamingle > Capture Thumbnail 메뉴를 사용하세요.",
"확인");
}
// ─── 빌드 세팅 ───
private void ShowBuildSettingsMenu()
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("모든 배경 씬 빌드 세팅에 추가"), false, AddAllScenesToBuildSettings);
menu.AddItem(new GUIContent("빌드 세팅에 없는 씬만 추가"), false, AddMissingScenesToBuildSettings);
menu.AddSeparator("");
menu.AddItem(new GUIContent("빌드 세팅에서 배경 씬 제거"), false, RemoveAllScenesFromBuildSettings);
menu.AddSeparator("");
menu.AddItem(new GUIContent("빌드 세팅 열기"), false,
() => EditorWindow.GetWindow(Type.GetType("UnityEditor.BuildPlayerWindow,UnityEditor")));
menu.ShowAsContext();
}
private void AddAllScenesToBuildSettings()
{
if (_database == null || _database.scenes.Count == 0)
{
EditorUtility.DisplayDialog("오류", "추가할 배경 씬이 없습니다.", "확인");
return;
}
var currentScenes = EditorBuildSettings.scenes.ToList();
int addedCount = 0;
foreach (var sceneInfo in _database.scenes)
{
if (!currentScenes.Any(s => s.path == sceneInfo.scenePath))
{
currentScenes.Add(new EditorBuildSettingsScene(sceneInfo.scenePath, true));
addedCount++;
}
}
EditorBuildSettings.scenes = currentScenes.ToArray();
EditorUtility.DisplayDialog("완료",
$"빌드 세팅에 {addedCount}개의 배경 씬이 추가되었습니다.\n(총 {_database.scenes.Count}개 중 {addedCount}개 새로 추가)", "확인");
}
private void AddMissingScenesToBuildSettings()
{
if (_database == null || _database.scenes.Count == 0)
{
EditorUtility.DisplayDialog("오류", "추가할 배경 씬이 없습니다.", "확인");
return;
}
var currentScenes = EditorBuildSettings.scenes.ToList();
int addedCount = 0;
foreach (var sceneInfo in _database.scenes)
{
if (!currentScenes.Any(s => s.path == sceneInfo.scenePath))
{
currentScenes.Add(new EditorBuildSettingsScene(sceneInfo.scenePath, true));
addedCount++;
}
}
if (addedCount == 0)
{
EditorUtility.DisplayDialog("완료", "모든 배경 씬이 이미 빌드 세팅에 있습니다.", "확인");
return;
}
EditorBuildSettings.scenes = currentScenes.ToArray();
EditorUtility.DisplayDialog("완료", $"빌드 세팅에 {addedCount}개의 배경 씬이 추가되었습니다.", "확인");
}
private void RemoveAllScenesFromBuildSettings()
{
if (_database == null || _database.scenes.Count == 0)
{
EditorUtility.DisplayDialog("오류", "제거할 배경 씬이 없습니다.", "확인");
return;
}
if (!EditorUtility.DisplayDialog("확인",
"빌드 세팅에서 모든 배경 씬을 제거하시겠습니까?\n이 작업은 런타임에서 배경 씬 로드를 불가능하게 만듭니다.",
"제거", "취소")) return;
var backgroundPaths = new HashSet<string>(_database.scenes.Select(s => s.scenePath));
var currentScenes = EditorBuildSettings.scenes.ToList();
int removedCount = currentScenes.RemoveAll(s => backgroundPaths.Contains(s.path));
EditorBuildSettings.scenes = currentScenes.ToArray();
EditorUtility.DisplayDialog("완료", $"빌드 세팅에서 {removedCount}개의 배경 씬이 제거되었습니다.", "확인");
}
// ─── 라이팅 복사/복원 (기존 로직 100% 유지) ───
private void CopyLightingFromCurrentBackground()
{
if (!_loadedBackgroundScene.HasValue || !_loadedBackgroundScene.Value.isLoaded)
{
EditorUtility.DisplayDialog("오류", "배경 씬이 로드되지 않았습니다.", "확인");
return;
}
var activeScene = SceneManager.GetActiveScene();
if (!activeScene.isLoaded)
{
EditorUtility.DisplayDialog("오류", "활성 씬이 없습니다.", "확인");
return;
}
if (activeScene.path == _loadedBackgroundScene.Value.path)
{
EditorUtility.DisplayDialog("오류", "배경 씬과 활성 씬이 동일합니다.", "확인");
return;
}
if (!_hasBackup)
{
_originalLightingSettings.Capture(activeScene);
_hasBackup = true;
Debug.Log("원래 라이팅 세팅이 백업되었습니다.");
}
CopyLightingElements(_loadedBackgroundScene.Value, activeScene);
UpdateStatusBar();
EditorUtility.DisplayDialog("완료", "라이팅 세팅이 복사되었습니다.\n'원래대로 복원' 버튼으로 원래 세팅을 복원할 수 있습니다.", "확인");
}
private void CopyLightingElements(Scene sourceScene, Scene targetScene)
{
var previousActiveScene = SceneManager.GetActiveScene();
SceneManager.SetActiveScene(sourceScene);
if (_copySkybox)
{
if (RenderSettings.skybox != null)
{
var sourceSkybox = RenderSettings.skybox;
SceneManager.SetActiveScene(targetScene);
RenderSettings.skybox = sourceSkybox;
SceneManager.SetActiveScene(sourceScene);
Debug.Log($"Skybox 복사됨: {sourceSkybox.name}");
}
else
{
var sourceRoots = sourceScene.GetRootGameObjects();
foreach (var root in sourceRoots)
{
var cameras = root.GetComponentsInChildren<Camera>(true);
foreach (var cam in cameras)
{
var skyboxComp = cam.GetComponent<Skybox>();
if (skyboxComp != null && skyboxComp.material != null)
{
SceneManager.SetActiveScene(targetScene);
RenderSettings.skybox = skyboxComp.material;
SceneManager.SetActiveScene(sourceScene);
Debug.Log($"Skybox 복사됨 (카메라): {skyboxComp.material.name}");
break;
}
}
}
}
}
if (_copyAmbient)
{
var am = RenderSettings.ambientMode;
var asc = RenderSettings.ambientSkyColor;
var aec = RenderSettings.ambientEquatorColor;
var agc = RenderSettings.ambientGroundColor;
var al = RenderSettings.ambientLight;
var ai = RenderSettings.ambientIntensity;
var ssc = RenderSettings.subtractiveShadowColor;
SceneManager.SetActiveScene(targetScene);
RenderSettings.ambientMode = am;
RenderSettings.ambientSkyColor = asc;
RenderSettings.ambientEquatorColor = aec;
RenderSettings.ambientGroundColor = agc;
RenderSettings.ambientLight = al;
RenderSettings.ambientIntensity = ai;
RenderSettings.subtractiveShadowColor = ssc;
SceneManager.SetActiveScene(sourceScene);
Debug.Log("Ambient 설정 복사됨");
}
if (_copyFog)
{
var f = RenderSettings.fog;
var fc = RenderSettings.fogColor;
var fm = RenderSettings.fogMode;
var fd = RenderSettings.fogDensity;
var fsd = RenderSettings.fogStartDistance;
var fed = RenderSettings.fogEndDistance;
SceneManager.SetActiveScene(targetScene);
RenderSettings.fog = f;
RenderSettings.fogColor = fc;
RenderSettings.fogMode = fm;
RenderSettings.fogDensity = fd;
RenderSettings.fogStartDistance = fsd;
RenderSettings.fogEndDistance = fed;
SceneManager.SetActiveScene(sourceScene);
Debug.Log($"Fog 설정 복사됨 (활성화: {f})");
}
SceneManager.SetActiveScene(previousActiveScene);
Debug.Log($"라이팅 복사됨: {sourceScene.name} → {targetScene.name}");
}
private void RestoreOriginalLighting()
{
if (!_hasBackup)
{
EditorUtility.DisplayDialog("오류", "백업된 라이팅 세팅이 없습니다.", "확인");
return;
}
var activeScene = SceneManager.GetActiveScene();
if (!activeScene.isLoaded)
{
EditorUtility.DisplayDialog("오류", "활성 씬이 없습니다.", "확인");
return;
}
_originalLightingSettings.Restore();
_hasBackup = false;
EditorSceneManager.MarkSceneDirty(activeScene);
UpdateStatusBar();
Debug.Log("원래 라이팅 세팅이 복원되었습니다.");
EditorUtility.DisplayDialog("완료", "원래 라이팅 세팅이 복원되었습니다.", "확인");
}
private void UnloadCurrentBackground()
{
if (!_loadedBackgroundScene.HasValue) return;
try
{
if (_hasBackup)
{
if (_autoRestoreOnUnload)
{
_originalLightingSettings.Restore();
Debug.Log("배경 언로드: 원래 라이팅 세팅이 자동 복원되었습니다.");
}
else
{
_originalLightingSettings.RestoreOriginalLighting();
}
_hasBackup = false;
if (!Application.isPlaying)
{
var activeScene = SceneManager.GetActiveScene();
if (activeScene.isLoaded) EditorSceneManager.MarkSceneDirty(activeScene);
}
}
if (Application.isPlaying)
SceneManager.UnloadSceneAsync(_loadedBackgroundScene.Value);
else
EditorSceneManager.CloseScene(_loadedBackgroundScene.Value, true);
_loadedBackgroundScene = null;
_currentSceneName = "없음";
RebuildContent();
}
catch (Exception ex)
{
Debug.LogError($"씬 언로드 실패: {ex.Message}");
}
}
}
}