정보창, 에디트 구조 작성

This commit is contained in:
logonkhi
2025-06-19 19:24:30 +09:00
parent 21020f590f
commit a6ac3e6b78
1486 changed files with 697081 additions and 1507 deletions

View File

@@ -0,0 +1,33 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.Edit
{
public class DropZone : MonoBehaviour, IDropHandler
{
public void OnDrop(PointerEventData eventData)
{
if (LibraryItem.DraggedPrefab != null)
{
GameObject newObj = Instantiate(LibraryItem.DraggedPrefab, GetDropPosition(eventData), Quaternion.identity);
// 여기서 newObj의 EditableObject 컴포넌트를 찾아 초기화 로직을 실행할 수 있습니다.
// newObj.GetComponent<EditableObject>()?.Initialize(System.Guid.NewGuid().ToString());
}
}
private Vector3 GetDropPosition(PointerEventData eventData)
{
// 3D Stage의 경우, 화면 좌표를 월드 좌표로 변환해야 합니다.
Ray ray = Camera.main.ScreenPointToRay(eventData.position);
if (new Plane(Vector3.up, Vector3.zero).Raycast(ray, out float enter))
{
return ray.GetPoint(enter);
}
return Vector3.zero;
// UI Canvas의 경우, RectTransformUtility를 사용합니다.
// RectTransformUtility.ScreenPointToLocalPointInRectangle(...)
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ea2b0e8422a022f4a9a6df278a857d4c

View File

@@ -0,0 +1,19 @@
using UnityEngine;
namespace UVC.Edit
{
public abstract class EditableObject : MonoBehaviour, ISelectable
{
[field: SerializeField]
public string ItemId { get; private set; }
public abstract void OnSelect();
public abstract void OnDeselect();
public virtual void Initialize(string id)
{
this.ItemId = id;
this.name = $"{GetType().Name}_{id}";
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5ddfd4794affdf44fb5e8dadcd13284d

View File

@@ -0,0 +1,22 @@
using UnityEngine;
namespace UVC.Edit
{
public class EditableObject3D : EditableObject
{
// 3D 객체 고유의 속성 및 로직
// 예: Material, Mesh 등
public override void OnSelect()
{
// 외곽선 표시 로직 호출
Debug.Log($"{name} selected.");
}
public override void OnDeselect()
{
// 외곽선 숨김 로직 호출
Debug.Log($"{name} deselected.");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2bbff96f4152cac46a16c5343f40e5fa

View File

@@ -0,0 +1,40 @@
using UnityEngine;
namespace UVC.Edit
{
public class GizmoController : MonoBehaviour
{
public Transform targetObject; // 선택된 객체의 Transform
// private RuntimeTransformHandle _transformHandle;
void OnEnable()
{
InteractionController.OnObjectSelected += OnObjectSelected;
}
void OnDisable()
{
InteractionController.OnObjectSelected -= OnObjectSelected;
}
void OnObjectSelected(EditableObject obj)
{
if (obj != null)
{
targetObject = obj.transform;
// _transformHandle.target = targetObject;
// _transformHandle.gameObject.SetActive(true);
}
else
{
targetObject = null;
// _transformHandle.gameObject.SetActive(false);
}
}
// 툴바에서 이동/회전/크기 툴 선택 시 아래와 같은 함수 호출
public void SetMoveMode()
{
// _transformHandle.type = HandleType.Position;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d8cd05d9b2d558f46aeeef881d1175e1

View File

@@ -0,0 +1,8 @@
namespace UVC.Edit
{
public interface ISelectable
{
void OnSelect();
void OnDeselect();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 958cc5e553c04b7429a00214ce6a263a

View File

@@ -0,0 +1,77 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.Edit
{
public class InteractionController : MonoBehaviour
{
public static event System.Action<EditableObject> OnObjectSelected;
public static event System.Action OnBackgroundClicked;
private Camera _mainCamera;
private EditableObject _selectedObject;
void Awake()
{
_mainCamera = Camera.main;
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
// UI 위에서 클릭했는지 먼저 확인
if (EventSystem.current.IsPointerOverGameObject())
{
return; // UI 클릭 시 월드 객체 선택 방지
}
HandleSelection();
}
}
private void HandleSelection()
{
Ray ray = _mainCamera.ScreenPointToRay(Input.mousePosition);
// 3D 객체 선택 (Physics Raycast)
if (Physics.Raycast(ray, out RaycastHit hit))
{
// EditableObject 컴포넌트를 가진 객체인지 확인
if (hit.collider.TryGetComponent<EditableObject>(out var target))
{
SetSelectedObject(target);
return;
}
}
// 2D 객체 선택 (Physics2D Raycast) - 필요 시 카메라 설정에 따라 추가
// RaycastHit2D hit2D = Physics2D.GetRayIntersection(ray);
// if (hit2D.collider != null && hit2D.collider.TryGetComponent<EditableObject>(out var target2D))
// {
// SetSelectedObject(target2D);
// return;
// }
// 아무것도 선택되지 않았을 경우
SetSelectedObject(null);
}
private void SetSelectedObject(EditableObject target)
{
if (_selectedObject == target) return;
_selectedObject?.OnDeselect();
_selectedObject = target;
_selectedObject?.OnSelect();
if (_selectedObject != null)
{
OnObjectSelected?.Invoke(_selectedObject);
}
else
{
OnBackgroundClicked?.Invoke();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ce86abe153e9a8742b1643cc28166c16

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.Edit
{
public class LibraryItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
public GameObject prefabToSpawn; // 이 아이템이 생성할 프리팹
public static GameObject DraggedPrefab; // 현재 드래그 중인 프리팹 (static으로 공유)
public void OnBeginDrag(PointerEventData eventData)
{
DraggedPrefab = prefabToSpawn;
// 드래그 시 시각적 효과 (예: 아이콘 반투명화)
}
public void OnDrag(PointerEventData eventData) { } // 드래그 중 로직 (필요 시)
public void OnEndDrag(PointerEventData eventData)
{
DraggedPrefab = null;
// 드래그 종료 시 시각적 효과 원상복구
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c02ea26fd607fed4aaa07ca57e73a451

View File

@@ -0,0 +1,35 @@
using System.Linq;
using UnityEngine;
namespace UVC.Edit
{
[RequireComponent(typeof(Renderer))]
public class OutlineEffect : MonoBehaviour
{
[SerializeField] private Material _outlineMaterial;
private Renderer _renderer;
private bool _isOutlined = false;
void Awake()
{
_renderer = GetComponent<Renderer>();
}
public void SetOutline(bool visible)
{
if (_isOutlined == visible) return;
_isOutlined = visible;
var materials = _renderer.sharedMaterials.ToList();
if (_isOutlined)
{
materials.Add(_outlineMaterial);
}
else
{
materials.Remove(_outlineMaterial);
}
_renderer.materials = materials.ToArray();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2659b57b886087046bbf148a542a83d1

View File

@@ -0,0 +1,49 @@
using SFB;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
namespace UVC.Edit
{
public class PropertyWindow : MonoBehaviour
{
public Image uiImageTarget; // UI 객체에 적용할 Image
public SpriteRenderer spriteTarget; // 2D 객체에 적용할 SpriteRenderer
public void OpenImageFileBrowser()
{
var extensions = new[] {
new ExtensionFilter("Image Files", "png", "jpg", "jpeg")
};
// 비동기 방식으로 파일 브라우저 열기
StandaloneFileBrowser.OpenFilePanelAsync("Select an Image", "", extensions, false, (string[] paths) =>
{
if (paths.Length > 0 && !string.IsNullOrEmpty(paths[0]))
{
LoadAndApplyTexture(paths[0]);
}
});
}
private void LoadAndApplyTexture(string path)
{
byte[] fileData = File.ReadAllBytes(path);
Texture2D texture = new Texture2D(2, 2);
// 이미지 데이터로 텍스처 로드
if (texture.LoadImage(fileData))
{
if (uiImageTarget != null)
{
Sprite newSprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
uiImageTarget.sprite = newSprite;
}
if (spriteTarget != null)
{
Sprite newSprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f));
spriteTarget.sprite = newSprite;
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 170fcace70e26454e80a7e0c972f1e6f

View File

@@ -0,0 +1,19 @@
using UnityEngine;
namespace UVC.Editor
{
public abstract class EditableObject : MonoBehaviour, ISelectable
{
[field: SerializeField]
public string ItemId { get; private set; }
public abstract void OnSelect();
public abstract void OnDeselect();
public virtual void Initialize(string id)
{
this.ItemId = id;
this.name = $"{GetType().Name}_{id}";
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c0224adc9ffbf484eadece57e5583624

View File

@@ -0,0 +1,22 @@
using UnityEngine;
namespace UVC.Editor
{
public class EditableObject3D : EditableObject
{
// 3D 객체 고유의 속성 및 로직
// 예: Material, Mesh 등
public override void OnSelect()
{
// 외곽선 표시 로직 호출
Debug.Log($"{name} selected.");
}
public override void OnDeselect()
{
// 외곽선 숨김 로직 호출
Debug.Log($"{name} deselected.");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4f0f7e0e4ee826a409789e44ac3584fb

View File

@@ -1,5 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace UVC.Extention
@@ -153,5 +156,159 @@ namespace UVC.Extention
}
return sb.ToString();
}
/// <summary>
/// 두 사전의 키와 값을 비교하여 두 사전이 같은지 확인합니다.
/// </summary>
/// <typeparam name="TKey">사전의 키 유형입니다.</typeparam>
/// <typeparam name="TValue">사전의 값 유형입니다.</typeparam>
/// <param name="dic">비교할 첫 번째 사전입니다.</param>
/// <param name="other">비교할 두 번째 사전입니다.</param>
/// <paramref name="dic"/>의 모든 키가 <paramref name="other"/>에 존재하고 해당 값이 같으면 <see langword="true"/>를 반환합니다.
/// 그렇지 않으면 <see langword="false"/>를 반환합니다.</returns>
public static bool IsSame<TKey, TValue>(this IDictionary<TKey, TValue> dic, IDictionary<TKey, TValue> other)
{
return !dic.Keys.Any(key => !object.Equals(dic[key], other[key]));
}
/// <summary>
/// 두 사전을 비교하여 두 번째 사전의 키와 값을 포함하는 새 사전을 반환합니다.
/// 두 사전의 값이 첫 번째 사전의 값과 다릅니다.
/// </summary>
/// <remarks>이 메서드는 <see
/// cref="object.Equals(object, object)"/>를 사용하여 얕은 값 비교를 수행합니다. 키가 <paramref name="oldDict"/>에는 있지만 <paramref
/// name="newDict"/>에는 없는 경우, 또는 그 반대의 경우, 해당 키는 무시됩니다.</remarks>
/// <typeparam name="TKey">사전에 있는 키의 타입입니다.</typeparam>
/// <typeparam name="TValue">사전에 있는 값의 타입입니다.</typeparam>
/// <param name="oldDict">비교할 원본 사전입니다.</param>
/// <param name="newDict">비교할 업데이트된 사전입니다.</param>
/// <returns> <paramref name="newDict"/>의 키가 포함된 사전입니다. 해당 키의 값이 <paramref name="oldDict"/>의 해당 값과 다른 경우입니다.
/// 반환된 사전의 값은 <paramref name="newDict"/>에서 가져옵니다.</returns>
public static IDictionary<TKey, TValue> Differences<TKey, TValue>(this IDictionary<TKey, TValue> oldDict, IDictionary<TKey, TValue> newDict)
{
var result = new Dictionary<TKey, TValue>();
foreach (var key in oldDict.Keys)
{
TValue oldValue = oldDict[key];
TValue newValue = newDict[key];
if (!object.Equals(oldValue, newValue))
{
result.Add(key, newValue);
}
}
return result;
}
/// <summary>
/// 두 사전을 비교하여 두 번째 사전의 키와 값을 포함하는 새 사전을 반환합니다.
/// 두 사전의 값이 첫 번째 사전의 값과 다릅니다.
/// </summary>
/// <remarks>이 메서드는 심층 비교를 사용하여 값이 다른지 확인합니다. 비교
/// 로직은 <c>DeepEquals</c> 메서드로 정의됩니다. <paramref name="dict1"/>에는 있지만
/// <paramref name="dict2"/>에는 없는 키는 결과에 포함되지 않습니다.</remarks>
/// <typeparam name="TKey">사전에 있는 키의 타입입니다.</typeparam>
/// <typeparam name="TValue">사전에 있는 값의 타입입니다.</typeparam>
/// <param name="dict1">비교할 첫 번째 사전입니다.</param>
/// <param name="dict2">비교할 두 번째 사전입니다.</param>
/// <returns><paramref name="dict2"/>의 키와 값을 포함하는 사전으로, 값이 <paramref name="dict1"/>의 값과 다른 경우
/// <paramref name="dict1"/>에 키가 있지만 값이 같지 않으면
/// <paramref name="dict2"/>의 키와 해당 값이 포함됩니다.</returns>
public static IDictionary<TKey, TValue> DeepDifferences<TKey, TValue>(this IDictionary<TKey, TValue> dict1, IDictionary<TKey, TValue> dict2)
{
var result = new Dictionary<TKey, TValue>();
foreach (var key in dict1.Keys)
{
TValue val1 = dict1[key];
TValue val2 = dict2[key];
if (!DeepEquals(val1, val2))
{
result.Add(key, val2);
}
}
return result;
}
/// <summary>
/// 두 객체의 값, 속성 및 중첩 구조를 비교하여 두 객체가 깊이 동일한지 여부를 확인합니다.
///
// </summary>
/// <remarks>이 메서드는 제공된 객체와 중첩 구조를 재귀적으로 비교합니다.
/// 기본 유형, 문자열, 소수, 컬렉션(예: 배열,리스트), 사전 및 복합 객체의 비교를 지원합니다.
/// 컬렉션의 경우, 이 메서드는 요소를 순서대로 비교합니다.
/// 사전의 경우, 키와 연관된 값을 비교합니다. 객체의 유형이 다르거나 일치하지 않는 구조를 가진 경우,
/// 이 메서드는 <see langword="false"/>를 반환합니다.</remarks>
/// <param name="obj1">비교할 첫 번째 객체입니다. null일 수 있습니다.</param>
/// <param name="obj2">비교할 두 번째 객체입니다. null일 수 있습니다.</param>
/// 객체가 깊이 동일하면 <returns><see langword="true"/>를 반환하고, 그렇지 않으면 <see langword="false"/>를 반환합니다. 깊이 동일성
///에는 기본 값, 문자열, 컬렉션, 사전 및 복합 객체의 공개 속성 비교가 포함됩니다.
///</returns>
private static bool DeepEquals(object obj1, object obj2)
{
if (ReferenceEquals(obj1, obj2))
return true;
if (obj1 == null || obj2 == null)
return false;
if (obj1.Equals(obj2))
return true;
var type1 = obj1.GetType();
var type2 = obj2.GetType();
if (type1 != type2)
return false;
// String or primitive
if (type1.IsPrimitive || obj1 is string || obj1 is decimal)
return obj1.Equals(obj2);
// List or Array
if (obj1 is IEnumerable enumerable1 && obj2 is IEnumerable enumerable2)
{
var list1 = enumerable1.Cast<object>().ToList();
var list2 = enumerable2.Cast<object>().ToList();
if (list1.Count != list2.Count)
return false;
for (int i = 0; i < list1.Count; i++)
{
if (!DeepEquals(list1[i], list2[i]))
return false;
}
return true;
}
// Dictionary
if (obj1 is IDictionary dict1 && obj2 is IDictionary dict2)
{
if (dict1.Count != dict2.Count)
return false;
foreach (var key in dict1.Keys)
{
if (!dict2.Contains(key)) return false;
if (!DeepEquals(dict1[key], dict2[key])) return false;
}
return true;
}
// Complex object
var props = type1.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in props)
{
var val1 = prop.GetValue(obj1);
var val2 = prop.GetValue(obj2);
if (!DeepEquals(val1, val2))
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace UVC.Editor
{
public interface ISelectable
{
void OnSelect();
void OnDeselect();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f9156b89e72147142831eb6d58934e49

View File

@@ -0,0 +1,77 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.Editor
{
public class InteractionController : MonoBehaviour
{
public static event System.Action<EditableObject> OnObjectSelected;
public static event System.Action OnBackgroundClicked;
private Camera _mainCamera;
private EditableObject _selectedObject;
void Awake()
{
_mainCamera = Camera.main;
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
// UI 위에서 클릭했는지 먼저 확인
if (EventSystem.current.IsPointerOverGameObject())
{
return; // UI 클릭 시 월드 객체 선택 방지
}
HandleSelection();
}
}
private void HandleSelection()
{
Ray ray = _mainCamera.ScreenPointToRay(Input.mousePosition);
// 3D 객체 선택 (Physics Raycast)
if (Physics.Raycast(ray, out RaycastHit hit))
{
// EditableObject 컴포넌트를 가진 객체인지 확인
if (hit.collider.TryGetComponent<EditableObject>(out var target))
{
SetSelectedObject(target);
return;
}
}
// 2D 객체 선택 (Physics2D Raycast) - 필요 시 카메라 설정에 따라 추가
// RaycastHit2D hit2D = Physics2D.GetRayIntersection(ray);
// if (hit2D.collider != null && hit2D.collider.TryGetComponent<EditableObject>(out var target2D))
// {
// SetSelectedObject(target2D);
// return;
// }
// 아무것도 선택되지 않았을 경우
SetSelectedObject(null);
}
private void SetSelectedObject(EditableObject target)
{
if (_selectedObject == target) return;
_selectedObject?.OnDeselect();
_selectedObject = target;
_selectedObject?.OnSelect();
if (_selectedObject != null)
{
OnObjectSelected?.Invoke(_selectedObject);
}
else
{
OnBackgroundClicked?.Invoke();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2f8a7b0a02822c942a8c2846d5a2432a

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1d91472c3f70d9d4c84c8b50283e0548
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,55 @@
#nullable enable
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UVC.Extention;
using UVC.UI.Info;
namespace UVC.Object3d
{
/// <summary>
/// 사용자의 마우스 클릭 입력을 감지하고 3D 객체와 상호작용합니다.
/// </summary>
public class InteractionController : InteractiveObject
{
// InfoWindow 인스턴스에 대한 참조
private InfoWindow infoWindow;
private Camera mainCamera;
private Dictionary<string, object>? infoData;
private void Awake()
{
mainCamera = Camera.main;
// 씬에 있는 InfoWindow 인스턴스를 동적으로 찾습니다.
// FindObjectOfType은 씬에서 해당 타입의 활성화된 첫 번째 객체를 반환합니다.
infoWindow = InfoWindow.Create();
if (infoWindow == null)
{
Debug.LogError("씬에서 InfoWindow 컴포넌트를 찾을 수 없습니다. InfoWindow가 씬에 존재하고 활성화되어 있는지 확인해주세요.");
enabled = false; // infoWindow가 없으면 이 스크립트를 비활성화합니다.
}
}
public override void OnPointerClick(PointerEventData eventData)
{
Dictionary<string, object> info = new Dictionary<string, object>
{
{ "objectName", gameObject.name },
{ "objectPosition", transform.position },
{ "objectRotation", transform.rotation },
{ "objectScale", transform.localScale }
};
// 변경 된 정보가 있을 때 클릭된 객체의 정보를 InfoWindow에 전달합니다.
if (!infoWindow.IsVisible && (infoData != null && !infoData.IsSame(info)))
{
infoWindow.Show(transform, info);
infoData = info;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 06c8fe736f6ab0d4787d720284f536ea

View File

@@ -0,0 +1,37 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace UVC.Object3d
{
/// <summary>
/// Unity의 Event System을 사용하여 마우스/포인터 상호작용을 처리하는 3D 객체를 위한 기본 클래스입니다.
/// 이 클래스를 상속받는 객체는 클릭, 포인터 진입/이탈 이벤트를 수신할 수 있습니다.
/// </summary>
/// <remarks>
/// 이 컴포넌트가 올바르게 작동하려면 다음 조건이 충족되어야 합니다:
/// 1. 이 컴포넌트가 부착된 게임 오브젝트에 반드시 <see cref="Collider"/> 컴포넌트가 있어야 합니다.
/// 2. 씬에 <see cref="EventSystem"/>이 존재해야 합니다.
/// 3. 메인 카메라에 <see cref="PhysicsRaycaster"/> 컴포넌트가 부착되어 있어야 합니다.
/// </remarks>
[RequireComponent(typeof(Collider))]
public class InteractiveObject : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
{
/// <summary>
/// 포인터로 이 객체를 클릭했을 때 호출됩니다.
/// </summary>
/// <param name="eventData">클릭 이벤트와 관련된 데이터입니다.</param>
public virtual void OnPointerClick(PointerEventData eventData) { }
/// <summary>
/// 포인터가 이 객체 위로 들어왔을 때 호출됩니다. 하이라이트 효과 등에 사용할 수 있습니다.
/// </summary>
/// <param name="eventData">포인터 이벤트와 관련된 데이터입니다.</param>
public virtual void OnPointerEnter(PointerEventData eventData) { }
/// <summary>
/// 포인터가 이 객체에서 벗어났을 때 호출됩니다.
/// </summary>
/// <param name="eventData">포인터 이벤트와 관련된 데이터입니다.</param>
public virtual void OnPointerExit(PointerEventData eventData) { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 369f10656ae555b4983fa3147fc56818

View File

@@ -0,0 +1,35 @@
using System.Linq;
using UnityEngine;
namespace UVC.Editor
{
[RequireComponent(typeof(Renderer))]
public class OutlineEffect : MonoBehaviour
{
[SerializeField] private Material _outlineMaterial;
private Renderer _renderer;
private bool _isOutlined = false;
void Awake()
{
_renderer = GetComponent<Renderer>();
}
public void SetOutline(bool visible)
{
if (_isOutlined == visible) return;
_isOutlined = visible;
var materials = _renderer.sharedMaterials.ToList();
if (_isOutlined)
{
materials.Add(_outlineMaterial);
}
else
{
materials.Remove(_outlineMaterial);
}
_renderer.materials = materials.ToArray();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 575f56d1e1229a543a23502e94149f71

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8822a5b71db85f64483ede945af84867
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,115 @@
#nullable enable
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Core;
namespace UVC.UI.Info
{
/// <summary>
/// 3D 객체를 따라다니며 정보를 표시하는 UI 창입니다.
/// 이 컴포넌트는 World Space Canvas 내의 UI 요소에 추가되어야 합니다.
/// </summary>
public class InfoWindow : MonoBehaviour
{
[Tooltip("InfoWindow의 프리팹")]
[SerializeField]
private static GameObject infoWindowPrefab;
[Tooltip("정보 텍스트를 표시할 UI 요소")]
[SerializeField]
private TextMeshProUGUI infoText;
[Tooltip("정보 창을 닫을 버튼")]
[SerializeField]
private Button closeButton;
[Tooltip("UI가 객체를 가리지 않도록 할 월드 좌표계 오프셋")]
[SerializeField]
private Vector3 worldOffset = new Vector3(0, 1.5f, 0);
// 정보 창이 따라다닐 3D 객체의 Transform
private Transform? target;
// 메인 카메라 참조
private Camera mainCamera;
/// <summary>
/// 정보 창이 현재 화면에 표시되고 있는지 여부를 반환합니다.
/// </summary>
public bool IsVisible => gameObject.activeSelf;
/// <summary>
/// 정보 창을 생성하여 반환합니다.
/// </summary>
public static InfoWindow? Create()
{
if(infoWindowPrefab == null)
{
Debug.LogError("InfoWindow 프리팹이 할당되지 않았습니다. Inspector에서 할당해주세요.");
return null;
}
return Instantiate(infoWindowPrefab).GetComponent<InfoWindow>();
}
private void Awake()
{
mainCamera = Camera.main;
// 닫기 버튼이 할당되었으면 클릭 이벤트를 연결합니다.
if (closeButton != null)
{
closeButton.onClick.AddListener(Hide);
}
// 처음에는 정보 창을 숨깁니다.
if (gameObject.activeSelf)
{
Hide();
}
}
private void LateUpdate()
{
// target이 설정되어 있고 활성화 상태일 때만 위치와 방향을 업데이트합니다.
if (target != null && gameObject.activeSelf)
{
// 위치 업데이트
transform.position = target.position + worldOffset;
// 항상 카메라를 바라보도록 방향 업데이트 (빌보드 효과)
transform.rotation = mainCamera.transform.rotation;
}
}
/// <summary>
/// 정보 창을 특정 대상에 대해 표시합니다.
/// </summary>
/// <param name="targetObject">정보를 표시할 3D 객체의 Transform</param>
/// <param name="information">표시할 정보 문자열</param>
public void Show(Transform targetObject, Dictionary<string, object> information)
{
target = targetObject;
if (infoText != null)
{
infoText.text = information.ToString();
}
gameObject.SetActive(true);
// 즉시 위치와 방향을 업데이트합니다.
LateUpdate();
}
/// <summary>
/// 정보 창을 숨깁니다.
/// </summary>
public void Hide()
{
gameObject.SetActive(false);
target = null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 620e52b408949c340adef1110323cb7c