배경 색상, Volume 조절

This commit is contained in:
logonkhi
2025-12-23 18:23:44 +09:00
parent 192d6963c0
commit 54d8157203
5 changed files with 309 additions and 106 deletions

View File

@@ -985,7 +985,7 @@ Light:
serializedVersion: 11
m_Type: 1
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_Intensity: 2
m_Intensity: 1.21
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
@@ -997,7 +997,7 @@ Light:
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_NearPlane: 0.1
m_CullingMatrixOverride:
e00: 1
e01: 0
@@ -1024,11 +1024,11 @@ Light:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_Lightmapping: 1
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 5000
m_BounceIntensity: 5
m_ColorTemperature: 6668
m_UseColorTemperature: 1
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
@@ -1071,7 +1071,7 @@ MonoBehaviour:
m_CustomShadowLayers: 0
m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 1
m_SoftShadowQuality: 3
m_RenderingLayersMask:
serializedVersion: 0
m_Bits: 1
@@ -5364,8 +5364,8 @@ MonoBehaviour:
mRotationSwitchSmoothSpeed: 15
mBGStyle:
mVisible: 1
mTopColor: {r: 0.8862745, g: 0.8745098, b: 0.85882354, a: 1}
mBottomColor: {r: 0.8862745, g: 0.8745098, b: 0.85882354, a: 1}
mTopColor: {r: 0.78431374, g: 0.7764706, b: 0.7647059, a: 1}
mBottomColor: {r: 0.78431374, g: 0.7764706, b: 0.7647059, a: 1}
mGradientOffset: 0
--- !u!1 &2030316708
GameObject:
@@ -5659,7 +5659,7 @@ PrefabInstance:
m_Modifications:
- target: {fileID: 2021477798026863722, guid: 8103add27aa0de64a992e9faa892eea1, type: 3}
propertyPath: m_IsOn
value: 0
value: 1
objectReference: {fileID: 0}
- target: {fileID: 8781706204188846678, guid: 8103add27aa0de64a992e9faa892eea1, type: 3}
propertyPath: m_Pivot.x

View File

@@ -111,6 +111,16 @@ namespace UVC.UI.Buttons
Set(value, false);
}
/// <summary>
/// isOn 값을 이벤트 없이 강제로 설정하고 시각을 업데이트합니다.
/// 풀에서 재사용 시 동일 값이어도 시각 갱신이 필요할 때 사용합니다.
/// </summary>
public void ForceSetIsOnWithoutNotify(bool value)
{
m_IsOn = value;
UpdateVisual();
}
/// <summary>
/// 내부 설정 로직. 상태가 변경되면 비주얼과 콜백을 처리합니다.
/// </summary>

View File

@@ -6,6 +6,7 @@ using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UVC.Pool;
using UVC.UI.Modal.DatePicker;
namespace UVC.UI.List.Tree
@@ -114,6 +115,26 @@ namespace UVC.UI.List.Tree
/// </summary>
public TreeListDragDropManager DragDropManager => dragDropManager;
#region (Object Pooling)
/// <summary>
/// TreeListItem 오브젝트 풀
/// </summary>
protected GameObjectPool<TreeListItem>? _itemPool;
/// <summary>
/// 재활용 컨테이너 (비활성 아이템 보관용)
/// </summary>
[SerializeField]
protected Transform? recycledContainer;
/// <summary>
/// 오브젝트 풀 읽기 전용 접근자
/// </summary>
public GameObjectPool<TreeListItem>? ItemPool => _itemPool;
#endregion
/// <summary>
/// 아이템 선택 상태 변경 이벤트 (data, isSelected)
/// TreeListItem에서 호출됩니다.
@@ -166,6 +187,73 @@ namespace UVC.UI.List.Tree
{
dragDropManager.OnDropped += HandleItemDropped;
}
// 오브젝트 풀 초기화
InitializePool();
}
/// <summary>
/// 오브젝트 풀 초기화
/// </summary>
protected virtual void InitializePool()
{
if (itemPrefab == null) return;
// recycledContainer가 없으면 동적 생성
if (recycledContainer == null)
{
var recycledGO = new GameObject("_RecycledItems");
recycledGO.transform.SetParent(transform, false);
recycledGO.SetActive(false); // 비활성 상태로 유지
recycledContainer = recycledGO.transform;
}
_itemPool = new GameObjectPool<TreeListItem>(
itemPrefab.gameObject,
root,
recycledContainer
);
}
/// <summary>
/// 풀에서 TreeListItem 획득
/// </summary>
/// <param name="key">아이템 고유 키 (TreeListItemData.Id.ToString())</param>
/// <param name="parent">부모 Transform (null이면 root 사용)</param>
/// <returns>풀에서 획득한 TreeListItem</returns>
public TreeListItem? GetPooledItem(string key, Transform? parent = null)
{
if (_itemPool == null)
{
InitializePool();
}
var item = _itemPool!.GetItem(key, true, parent ?? root);
return item;
}
/// <summary>
/// TreeListItem을 풀에 반환
/// </summary>
/// <param name="item">반환할 TreeListItem</param>
/// <param name="key">아이템 고유 키</param>
public void ReturnItemToPool(TreeListItem item, string key)
{
if (_itemPool == null || item == null) return;
_itemPool.ReturnItem(key, true);
}
/// <summary>
/// 모든 아이템을 풀에 반환
/// </summary>
public void ReturnAllToPool()
{
_itemPool?.ReturnAll();
allItemFlattened.Clear();
allItemDataFlattened.Clear();
selectedItems.Clear();
_viewMap.Clear();
}
protected void OnDestroy()
@@ -187,6 +275,9 @@ namespace UVC.UI.List.Tree
allItemDataFlattened.Clear();
selectedItems.Clear();
items.Clear();
// 풀 정리
_itemPool?.ClearRecycledItems();
}
#region (Data Modification Methods)
@@ -909,9 +1000,9 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 아이템 삭제
///
///
/// 동작:
/// 1. 데이터 리스트에서 제거
/// 1. 데이터 리스트에서 제거 (루트 또는 부모의 Children)
/// 2. 선택 리스트에서 제거
/// 3. UI 컴포넌트 삭제
/// 4. 데이터 메모리 해제
@@ -919,7 +1010,15 @@ namespace UVC.UI.List.Tree
/// <param name="data">삭제할 아이템 데이터</param>
public void DeleteItem(TreeListItemData data)
{
items.Remove(data);
// 루트 레벨이면 items에서 제거, 자식이면 부모의 Children에서 제거
if (data.Parent != null)
{
data.Parent.RemoveChild(data);
}
else
{
items.Remove(data);
}
// selectedItems에서도 제거
if (selectedItems.Contains(data))
@@ -927,10 +1026,19 @@ namespace UVC.UI.List.Tree
selectedItems.Remove(data);
}
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
if (item != null)
// UI 삭제 - _viewMap에서 먼저 찾기 (풀링 적용 후 더 정확함)
if (_viewMap.TryGetValue(data, out var viewItem) && viewItem != null)
{
item.Delete(true);
viewItem.Delete(true);
}
else
{
// fallback: allItemFlattened에서 찾기
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
if (item != null)
{
item.Delete(true);
}
}
}

View File

@@ -149,16 +149,23 @@ namespace UVC.UI.List.Tree
childRootLayoutGroup = childRoot.GetComponent<VerticalLayoutGroup>();
}
if (data.Children.Count > 0)
// 성능 최적화: 펼쳐진 상태에서만 자식 UI 생성 (지연 생성)
if (data.IsExpanded && data.Children.Count > 0)
{
// 각 자식 데이터에 대해 UI 생성
foreach (var childData in data.Children)
{
CreateItem(childData); // 재귀적으로 트리 구조 생성
}
childRoot.gameObject.SetActive(true);
}
// 화살표 방향 설정 (초기에는 펼쳐짐)
else
{
// 접힌 상태면 자식 컨테이너 비활성화
childRoot.gameObject.SetActive(false);
}
// 화살표 방향 설정
SetExpand();
// 4. 버튼 클릭 이벤트 구독
@@ -170,7 +177,8 @@ namespace UVC.UI.List.Tree
if(visibleToggle != null)
{
visibleToggle.isOn = data.IsVisible;
// 풀에서 재사용 시 동일 값이어도 시각 업데이트를 위해 강제 갱신
visibleToggle.ForceSetIsOnWithoutNotify(data.IsVisible);
visibleToggle.OnValueChanged.AddListener(OnItemVisibilityChanged);
}
@@ -217,7 +225,18 @@ namespace UVC.UI.List.Tree
if (changedType == ChangedType.Expanded)
{
childRoot.gameObject.SetActive(data.IsExpanded);
if (data.IsExpanded)
{
// 펼침 시: 자식 UI가 없으면 생성 (지연 생성)
EnsureChildrenCreated();
childRoot.gameObject.SetActive(true);
}
else
{
// 접힘 시: 자식 UI를 풀에 반환 (메모리 절약)
ReturnChildrenToPool();
childRoot.gameObject.SetActive(false);
}
// 펼침/접힘 상태 변경 처리
SetExpand();
return;
@@ -504,6 +523,8 @@ namespace UVC.UI.List.Tree
protected void OnItemVisibilityChanged(bool isVisible)
{
if (data == null) return;
// 데이터에 가시성 상태 저장 (풀에서 재사용 시 복원을 위해)
data.IsVisible = isVisible;
treeList.OnItemVisibilityChanged?.Invoke(data, isVisible);
}
@@ -555,6 +576,26 @@ namespace UVC.UI.List.Tree
#region (Child Management)
/// <summary>
/// 자식 UI가 필요할 때만 생성 (Lazy Instantiation)
/// </summary>
protected void EnsureChildrenCreated()
{
if (data == null || treeList == null) return;
// 데이터에 있지만 UI가 없는 자식들 생성
foreach (var childData in data.Children)
{
// 이미 뷰가 있는지 확인
if (treeList.ItemPool?.ActiveItems.ContainsKey(childData.Id.ToString()) == true)
{
continue;
}
CreateItem(childData);
}
}
/// <summary>
/// 새로운 자식 아이템을 추가하고 UI를 갱신합니다.
/// </summary>
@@ -573,31 +614,35 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 자식 데이터를 받아 UI TreeListItem을 생성합니다.
/// 자식 데이터를 받아 UI TreeListItem을 생성합니다. (풀에서 획득)
/// </summary>
/// <param name="data">생성할 아이템 데이터</param>
/// <returns>생성된 TreeListItem</returns>
protected TreeListItem CreateItem(TreeListItemData data)
{
// 1. 프리팹을 복제해서 새로운 TreeListItem 생성
// Instantiate<T>(원본, 부모, 옵션)
// treeList.ItemPrefab: UI 아이템 템플릿
// childRoot: 새 아이템의 부모 Transform
TreeListItem item = GameObject.Instantiate<TreeListItem>(
treeList.ItemPrefab, // 복제할 프리팹
childRoot // 부모로 배치할 위치
);
//item.layout의 너비를 childRootLayoutGroup의 padding.left 만큼 줄이기
// 1. 풀에서 아이템 획득 (ID를 키로 사용)
string key = data.Id.ToString();
TreeListItem? item = treeList.GetPooledItem(key, childRoot);
if (item == null)
{
// 풀에서 가져올 수 없으면 기존 방식으로 생성 (fallback)
item = GameObject.Instantiate<TreeListItem>(
treeList.ItemPrefab,
childRoot
);
}
// 2. layout의 너비를 childRootLayoutGroup의 padding.left 만큼 줄이기
if (item != null && item.layout != null && childRootLayoutGroup != null)
{
item.layout.sizeDelta = new Vector2(layout.sizeDelta.x - childRootLayoutGroup.padding.left, item.layout.sizeDelta.y);
}
// 2. 생성된 아이템 초기화
item.Init(data, treeList, treeList.DragDropManager);
// 3. 생성된 아이템 초기화
item!.Init(data, treeList, treeList.DragDropManager);
// 3. 생성된 아이템 반환
// 4. 생성된 아이템 반환
return item;
}
@@ -606,10 +651,116 @@ namespace UVC.UI.List.Tree
#region (Deletion)
/// <summary>
/// 이벤트 구독 해제, 애니메이션 중지 후 GameObject를 삭제합니다.
/// 이벤트 구독 해제, 애니메이션 중지 후 GameObject를 풀에 반환합니다.
/// </summary>
/// <param name="deleteData">데이터도 삭제할지 여부</param>
public void Delete(bool deleteData = false)
{
if (data == null || treeList == null)
{
// 데이터나 treeList가 없으면 기존 방식으로 삭제
CleanupAndDestroy();
return;
}
string key = data.Id.ToString();
// 자식들 먼저 풀에 반환
ReturnChildrenToPool();
// 데이터 정리 (선택적)
if (deleteData)
{
data.Dispose();
}
// 풀에 등록된 아이템인지 확인
bool isPooledItem = treeList.ItemPool?.ActiveItems.ContainsKey(key) == true;
if (isPooledItem)
{
// 풀에 반환
ResetForPool();
treeList.ReturnItemToPool(this, key);
}
else
{
// 풀에 없는 아이템 (루트 레벨 등)은 직접 삭제
CleanupAndDestroy();
}
}
/// <summary>
/// 풀에서 재사용 시 초기화를 위한 메서드
/// </summary>
public virtual void ResetForPool()
{
// 이벤트 리스너 해제
if (itemButton != null)
{
itemButton.onClick.RemoveListener(OnItemClicked);
}
if (visibleToggle != null)
{
visibleToggle.OnValueChanged.RemoveAllListeners();
}
// 뷰 맵에서 해제
if (data != null && treeList != null)
{
treeList.UnregisterView(data, this);
}
// 데이터 참조만 해제 (Dispose 안 함 - 데이터는 외부에서 관리)
data = null;
// 애니메이션 정리
if (childExpand != null)
{
childExpand.transform.DOKill();
}
// 상태 초기화
isAnimating = false;
_lastClickTime = 0f;
// 선택 배경 숨기기
if (selectedBg != null)
{
selectedBg.gameObject.SetActive(false);
}
}
/// <summary>
/// 자식 TreeListItem들을 풀에 반환 (재귀)
/// </summary>
protected void ReturnChildrenToPool()
{
if (childRoot == null || treeList == null) return;
for (int i = childRoot.childCount - 1; i >= 0; i--)
{
var childTransform = childRoot.GetChild(i);
var childItem = childTransform.GetComponent<TreeListItem>();
if (childItem != null && childItem.data != null)
{
string childKey = childItem.data.Id.ToString();
// 재귀적으로 자식의 자식도 반환
childItem.ReturnChildrenToPool();
// 풀에 반환
childItem.ResetForPool();
treeList.ReturnItemToPool(childItem, childKey);
}
}
}
/// <summary>
/// 정리 후 GameObject 삭제 (풀 미사용 시 fallback)
/// </summary>
protected void CleanupAndDestroy()
{
// 맵에서 해제
if (data != null && treeList != null)
@@ -617,14 +768,14 @@ namespace UVC.UI.List.Tree
treeList.UnregisterView(data, this);
}
// 1. 데이터 정리
// 데이터 정리
if (data != null)
{
data.Dispose();
data = null;
}
// 2. 버튼 클릭 이벤트 구독 해제
// 버튼 클릭 이벤트 구독 해제
if (itemButton != null)
{
itemButton.onClick.RemoveListener(OnItemClicked);
@@ -637,13 +788,11 @@ namespace UVC.UI.List.Tree
if (childExpand != null)
{
childExpand.transform.DOKill(); // 진행 중인 회전 애니메이션 중단
childExpand.transform.DOKill();
}
// 3. 참조 정리
treeList = null!;
// 4. 이 GameObject 삭제
GameObject.Destroy(gameObject);
}

View File

@@ -12,7 +12,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0b2db86121404754db890f4c8dfe81b2, type: 3}
m_Name: Bloom
m_EditorClassIdentifier:
active: 0
active: 1
skipIterations:
m_OverrideState: 1
m_Value: 1
@@ -47,52 +47,6 @@ MonoBehaviour:
dirtIntensity:
m_OverrideState: 1
m_Value: 0
--- !u!114 &-4984583903286764093
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c01700fd266d6914ababb731e09af2eb, type: 3}
m_Name: DepthOfField
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.DepthOfField
active: 1
mode:
m_OverrideState: 1
m_Value: 0
gaussianStart:
m_OverrideState: 1
m_Value: 16.41
gaussianEnd:
m_OverrideState: 1
m_Value: 30
gaussianMaxRadius:
m_OverrideState: 1
m_Value: 0.939
highQualitySampling:
m_OverrideState: 1
m_Value: 1
focusDistance:
m_OverrideState: 1
m_Value: 10
aperture:
m_OverrideState: 1
m_Value: 5.6
focalLength:
m_OverrideState: 1
m_Value: 50
bladeCount:
m_OverrideState: 1
m_Value: 5
bladeCurvature:
m_OverrideState: 1
m_Value: 1
bladeRotation:
m_OverrideState: 1
m_Value: 0
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -106,11 +60,9 @@ MonoBehaviour:
m_Name: StudioSceneProfile
m_EditorClassIdentifier:
components:
- {fileID: 849379129802519247}
- {fileID: -7893295128165547882}
- {fileID: 2197063094728681049}
- {fileID: 4954787062854874018}
- {fileID: -4984583903286764093}
- {fileID: 849379129802519247}
--- !u!114 &849379129802519247
MonoBehaviour:
m_ObjectHideFlags: 3
@@ -151,22 +103,6 @@ MonoBehaviour:
maxNits:
m_OverrideState: 0
m_Value: 1000
--- !u!114 &2197063094728681049
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 81180773991d8724ab7f2d216912b564, type: 3}
m_Name: ChromaticAberration
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.ChromaticAberration
active: 1
intensity:
m_OverrideState: 1
m_Value: 0.138
--- !u!114 &4954787062854874018
MonoBehaviour:
m_ObjectHideFlags: 3