알람 수정 중
This commit is contained in:
@@ -44,7 +44,8 @@ namespace UVC.Factory.Alarm
|
||||
//알람 데이터가 AGV와 관련 없는것이 많아서, AGV 이름을 미리 정의합니다.
|
||||
for (int i = 1; i <= 115; i++)
|
||||
{
|
||||
agvNames.Add($"HFF09CNA8{i.ToString("D3")}");
|
||||
//agvNames.Add($"HFF09CNA8{i.ToString("D3")}");
|
||||
agvNames.Add($"HFF09CNA8061");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,17 +187,35 @@ namespace UVC.Factory.Alarm
|
||||
return;
|
||||
}
|
||||
|
||||
// 없으면 새로 생성
|
||||
FactoryObject? targetObject = dataManager!.FindByName(data.GetString("TRANSPORT_EQP_ID")!);
|
||||
Debug.Log($"AlarmManager {targetObject==null} {data.Id}, {data.GetString("TRANSPORT_EQP_ID")}");
|
||||
if (targetObject != null)
|
||||
|
||||
string? eqpId = data.GetString("TRANSPORT_EQP_ID");
|
||||
if (eqpId == null) return;
|
||||
|
||||
FactoryObject? targetObject = dataManager!.FindByName(eqpId);
|
||||
Debug.Log($"AlarmManager {targetObject==null} {data.Id}, {eqpId}");
|
||||
if (targetObject == null) return;
|
||||
|
||||
if (activeAlarmUIs.ContainsKey(eqpId))
|
||||
{
|
||||
if(activeAlarmUIs[eqpId].ContainsAlarm(data))
|
||||
{
|
||||
activeAlarmUIs[eqpId].UpdateAlarm(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
activeAlarmUIs[eqpId].AddAlarm(data);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 없으면 새로 생성
|
||||
GameObject newUIObject = Instantiate(alarmUIPrefab, transform); // 매니저 하위에 생성
|
||||
AlarmUIController newUiController = newUIObject.GetComponent<AlarmUIController>();
|
||||
newUiController.Initialize(targetObject, data);
|
||||
|
||||
activeAlarmUIs.Add(data.Id!, newUiController);
|
||||
activeAlarmUIs.Add(eqpId, newUiController);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -207,11 +226,22 @@ namespace UVC.Factory.Alarm
|
||||
Debug.LogError($"Modify Alarm Received No data. {data}");
|
||||
return;
|
||||
}
|
||||
string? eqpId = data.GetString("TRANSPORT_EQP_ID");
|
||||
if (eqpId == null) return;
|
||||
|
||||
// 이미 해당 설비에 알람 UI가 떠 있는지 확인
|
||||
if (activeAlarmUIs.TryGetValue(data.Id!, out AlarmUIController uiController))
|
||||
if (activeAlarmUIs.TryGetValue(eqpId, out AlarmUIController uiController))
|
||||
{
|
||||
// 있으면 기존 UI에 알람 정보만 추가
|
||||
uiController.UpdateAlarm(data);
|
||||
// 있으면 기존 UI에 알람 정보 업데이트
|
||||
if (uiController.ContainsAlarm(data))
|
||||
{
|
||||
uiController.UpdateAlarm(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 없으면 새로 추가
|
||||
uiController.AddAlarm(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,13 +252,22 @@ namespace UVC.Factory.Alarm
|
||||
Debug.LogError($"Clear Alarm Received No data. {data}");
|
||||
return;
|
||||
}
|
||||
if (activeAlarmUIs.TryGetValue(data.Id!, out AlarmUIController uiController))
|
||||
|
||||
string? eqpId = data.GetString("TRANSPORT_EQP_ID");
|
||||
if (eqpId == null) return;
|
||||
|
||||
if (activeAlarmUIs.TryGetValue(eqpId, out AlarmUIController uiController))
|
||||
{
|
||||
uiController.RemoveAlarm(data);
|
||||
if (uiController.GetAlarmCount() == 0)
|
||||
// 있으면 기존 UI에 알람 정보 업데이트
|
||||
if (uiController.ContainsAlarm(data))
|
||||
{
|
||||
activeAlarmUIs.Remove(data.Id!);
|
||||
Destroy(uiController.gameObject);
|
||||
uiController.RemoveAlarm(data);
|
||||
|
||||
if (uiController.GetAlarmCount() == 0)
|
||||
{
|
||||
activeAlarmUIs.Remove(data.Id!);
|
||||
Destroy(uiController.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using UVC.Util;
|
||||
|
||||
namespace UVC.Factory.Alarm
|
||||
{
|
||||
public class SingleAlarmIcon : MonoBehaviour
|
||||
public class AlarmSingleIcon : MonoBehaviour
|
||||
{
|
||||
|
||||
[Tooltip("알람 내용을 표시하는 텍스트입니다.")]
|
||||
@@ -18,12 +18,12 @@ namespace UVC.Factory.Alarm
|
||||
[Tooltip("확장된 알람 뷰입니다. 개별 알람 아이콘을 표시합니다.")]
|
||||
[SerializeField] private GameObject expandedView;
|
||||
[Tooltip("개별 알람 아이콘 프리팹입니다. 이 프리팹은 개별 알람 정보를 표시하는 UI 요소를 포함해야 합니다.")]
|
||||
[SerializeField] private GameObject singleAlarmIconPrefab; // 개별 알람 아이콘
|
||||
[SerializeField] private GameObject alarmSingleIconPrefab; // 개별 알람 아이콘
|
||||
|
||||
private Transform targetObject;
|
||||
private List<DataObject> alarms = new List<DataObject>();
|
||||
private bool isExpanded = false;
|
||||
private SingleAlarmIcon singleAlarmIcon1 = null;
|
||||
private AlarmSingleIcon singleAlarmIcon1 = null;
|
||||
|
||||
private RectTransform rectTransform;
|
||||
private Canvas mainCanvas;
|
||||
@@ -99,6 +99,11 @@ namespace UVC.Factory.Alarm
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsAlarm(DataObject alarm)
|
||||
{
|
||||
return alarms.Exists(a => a.Id == alarm.Id);
|
||||
}
|
||||
|
||||
public void AddAlarm(DataObject alarm)
|
||||
{
|
||||
alarms.Add(alarm);
|
||||
@@ -156,7 +161,7 @@ namespace UVC.Factory.Alarm
|
||||
// 여기에 단일 알람 아이콘을 보여주는 로직 추가
|
||||
if (singleAlarmIcon1 == null)
|
||||
{
|
||||
singleAlarmIcon1 = Instantiate(singleAlarmIconPrefab, transform).GetComponent<SingleAlarmIcon>();
|
||||
singleAlarmIcon1 = Instantiate(alarmSingleIconPrefab, transform).GetComponent<AlarmSingleIcon>();
|
||||
}
|
||||
singleAlarmIcon1.gameObject.SetActive(true);
|
||||
singleAlarmIcon1.SetData(alarms[0], targetObject);
|
||||
@@ -205,16 +210,17 @@ namespace UVC.Factory.Alarm
|
||||
}
|
||||
|
||||
// 원형으로 아이콘 배치
|
||||
float radius = 100.0f; // Canvas 좌표계에 맞는 반지름 값
|
||||
float radius = (alarms.Count - 1) * 20.0f; // Canvas 좌표계에 맞는 반지름 값
|
||||
|
||||
for (int i = 0; i < alarms.Count; i++)
|
||||
{
|
||||
float angle = i * Mathf.PI * 2f / alarms.Count;
|
||||
Vector3 pos = new Vector3(Mathf.Cos(angle), Mathf.Sin(angle), 0) * radius;
|
||||
|
||||
GameObject iconObj = Instantiate(singleAlarmIconPrefab, expandedView.transform);
|
||||
GameObject iconObj = Instantiate(alarmSingleIconPrefab, expandedView.transform);
|
||||
iconObj.transform.localPosition = pos;
|
||||
// iconObj의 SingleAlarmIcon 스크립트에 알람 데이터 전달
|
||||
iconObj.GetComponent<SingleAlarmIcon>().SetData(alarms[i], targetObject);
|
||||
// iconObj의 AlarmSingleIcon 스크립트에 알람 데이터 전달
|
||||
iconObj.GetComponent<AlarmSingleIcon>().SetData(alarms[i], targetObject);
|
||||
}
|
||||
|
||||
//AnimateUISpace(0f); // 간격을 0으로 애니메이션
|
||||
|
||||
8
Assets/Scripts/UVC/Factory/Camera.meta
Normal file
8
Assets/Scripts/UVC/Factory/Camera.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef255ae14c2a7a647ab9e652029763b2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
502
Assets/Scripts/UVC/Factory/Camera/CameraController.cs
Normal file
502
Assets/Scripts/UVC/Factory/Camera/CameraController.cs
Normal file
@@ -0,0 +1,502 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UVC.Core;
|
||||
|
||||
namespace UVC.Factory
|
||||
{
|
||||
/// <summary>
|
||||
/// 마우스 입력에 따라 카메라를 이동, 회전, 줌하는 컨트롤러입니다.
|
||||
/// - 마우스 가운데 버튼 드래그: 카메라 평행 이동 (Pan)
|
||||
/// - 마우스 오른쪽 버튼 드래그: 카메라 회전 (Orbit)
|
||||
/// - 마우스 휠 스크롤: 카메라 줌 (Zoom)
|
||||
/// </summary>
|
||||
public class CameraController : SingletonScene<CameraController>
|
||||
{
|
||||
[Header("Panning Speed")]
|
||||
[Tooltip("카메라 높이가 임계값보다 낮을 때의 평행 이동 속도")]
|
||||
public float lowAltitudePanSpeed = 0.5f;
|
||||
|
||||
[Tooltip("카메라 높이가 임계값보다 높을 때의 평행 이동 속도")]
|
||||
public float highAltitudePanSpeed = 10f;
|
||||
|
||||
[Tooltip("카메라 회전 속도")]
|
||||
public float rotationSpeed = 300f;
|
||||
|
||||
[Tooltip("카메라 줌 속도")]
|
||||
public float zoomSpeed = 10f;
|
||||
|
||||
[Header("Movement Smoothing")]
|
||||
[Tooltip("패닝 시 마우스 이동량의 최대값을 제한하여, 프레임 드랍 시 카메라가 급격하게 튀는 현상을 방지합니다.")]
|
||||
public float maxPanDelta = 50f;
|
||||
|
||||
[Header("Camera")]
|
||||
[Tooltip("카메라 최소 높이")]
|
||||
public float minCameraY = 2f;
|
||||
|
||||
[Tooltip("카메라 최대 높이")]
|
||||
public float maxCameraY = 50f;
|
||||
|
||||
[Tooltip("카메라의 최소 수직 회전 각도 (X축)")]
|
||||
public float minPitch = 20f;
|
||||
|
||||
[Tooltip("카메라의 최대 수직 회전 각도 (X축)")]
|
||||
public float maxPitch = 85f;
|
||||
|
||||
[Tooltip("카메라의 최소 수평 회전 각도 (y축)")]
|
||||
public float minYaw = -45f;
|
||||
|
||||
[Tooltip("카메라의 최대 수평 회전 각도 (y축)")]
|
||||
public float maxYaw = 45f;
|
||||
|
||||
/// <summary>
|
||||
/// 카메라의 변형이 변경될 때 발생합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 이벤트는 카메라의 변형이 업데이트될 때마다 트리거되며,
|
||||
/// 구독자는 위치, 회전 또는 크기 변경에 응답할 수 있습니다. 이 이벤트를 사용하여
|
||||
/// UI 요소 업데이트 또는 종속 값 재계산과 같은 작업을 수행할 수 있습니다.</remarks>
|
||||
public event Action<Transform> OnCameraChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 카메라 위치가 변경될 때 발생합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 이벤트는 카메라 위치가 업데이트될 때마다 트리거됩니다. 구독자는
|
||||
/// 이 이벤트를 사용하여 UI 요소를 업데이트하거나 새 위치를 기반으로 계산을 수행하는 등 카메라 위치 변경에 대응할 수 있습니다.
|
||||
///</remarks>
|
||||
public event Action<Vector3> OnCameraPositionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 카메라의 회전이 변경될 때 발생합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 이벤트는 카메라의 회전이 업데이트될 때마다 트리거됩니다. 구독자는
|
||||
/// 이 이벤트를 사용하여 카메라 방향의 변경에 응답할 수 있습니다.</remarks>
|
||||
public event Action<Quaternion> OnCameraRotionChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 정의된 범위 내에서 카메라의 정규화된 수직 위치를 가져옵니다.
|
||||
/// </summary>
|
||||
/// <remarks>값은 카메라의 현재 수직 위치를 기반으로 계산되며
|
||||
/// [0, 1] 범위로 고정됩니다.</remarks>
|
||||
public float CameraYRate
|
||||
{
|
||||
get
|
||||
{
|
||||
// 카메라 높이에 따라 0~1 사이의 값을 반환합니다.
|
||||
return Mathf.Clamp01((transform.position.y - minCameraY) / (maxCameraY - minCameraY));
|
||||
}
|
||||
}
|
||||
|
||||
private Transform prevTransform; // 이전 카메라 거리
|
||||
|
||||
private Vector3 lastPanPosition;
|
||||
private Vector3 rotationPivot;
|
||||
private bool isRotating = false;
|
||||
|
||||
private Coroutine focusCoroutine; // 현재 실행 중인 포커싱 코루틴을 저장할 변수
|
||||
|
||||
void Start()
|
||||
{
|
||||
// 스크립트 시작 시, 회전의 기준이 되는 중심점을 카메라 앞쪽으로 초기화합니다.
|
||||
rotationPivot = transform.position + transform.forward * 10f;
|
||||
this.prevTransform = transform; // 초기 카메라 위치 저장
|
||||
}
|
||||
|
||||
private void StopFocusCoroutine()
|
||||
{
|
||||
if (focusCoroutine != null)
|
||||
{
|
||||
StopCoroutine(focusCoroutine);
|
||||
focusCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void DispatchEvnet()
|
||||
{
|
||||
if (prevTransform != null && (prevTransform.position != transform.position || prevTransform.rotation != transform.rotation))
|
||||
{
|
||||
OnCameraChanged?.Invoke(transform);
|
||||
if (prevTransform.position != transform.position)
|
||||
{
|
||||
OnCameraPositionChanged?.Invoke(transform.position);
|
||||
}
|
||||
if (prevTransform.rotation != transform.rotation)
|
||||
{
|
||||
OnCameraRotionChanged?.Invoke(transform.rotation);
|
||||
}
|
||||
}
|
||||
prevTransform = transform; // 현재 카메라 위치 저장
|
||||
}
|
||||
|
||||
private void ValidateCameraTransform()
|
||||
{
|
||||
// 카메라의 위치가 최소/최대 높이 범위를 벗어나지 않도록 합니다.
|
||||
Vector3 currentPosition = transform.position;
|
||||
currentPosition.y = Mathf.Clamp(currentPosition.y, minCameraY, maxCameraY);
|
||||
transform.position = currentPosition;
|
||||
ValidateCameraRotation();
|
||||
|
||||
// 카메라의 위치가 너무 멀리 떨어지지 않도록 합니다.
|
||||
//float distance = Vector3.Distance(transform.position, rotationPivot);
|
||||
//if (distance > 100f) // 예시로 100f를 최대 거리로 설정
|
||||
//{
|
||||
// transform.position = rotationPivot + (transform.position - rotationPivot).normalized * 100f;
|
||||
//}
|
||||
}
|
||||
|
||||
private void ValidateCameraRotation()
|
||||
{
|
||||
// 현재 회전값을 오일러 각으로 가져옵니다.
|
||||
Vector3 eulerAngles = transform.eulerAngles;
|
||||
// 오일러 각의 X축(Pitch) 값을 정규화하고 제한합니다.
|
||||
// 각도가 180도를 넘어가면 음수 값으로 변환하여 처리합니다. (예: 350도 -> -10도)
|
||||
float angleX = eulerAngles.x;
|
||||
if (angleX > 180f) angleX -= 360f;
|
||||
angleX = Mathf.Clamp(angleX, minPitch, maxPitch);
|
||||
eulerAngles.x = angleX;
|
||||
|
||||
float angleY = eulerAngles.y;
|
||||
if (angleY > 180f) angleY -= 360f;
|
||||
angleY = Mathf.Clamp(angleY, minYaw, maxYaw);
|
||||
eulerAngles.y = angleY;
|
||||
|
||||
// Z축 회전(롤)을 0으로 설정하여 카메라가 옆으로 기울어지는 것을 방지합니다.
|
||||
eulerAngles.z = 0f;
|
||||
|
||||
// 수정된 오일러 각을 다시 쿼터니언으로 변환하여 적용합니다.
|
||||
transform.rotation = Quaternion.Euler(eulerAngles);
|
||||
}
|
||||
|
||||
private Quaternion ValidateRotation(Quaternion rotation)
|
||||
{
|
||||
// 현재 회전값을 오일러 각으로 가져옵니다.
|
||||
Vector3 eulerAngles = rotation.eulerAngles;
|
||||
// 오일러 각의 X축(Pitch) 값을 정규화하고 제한합니다.
|
||||
// 각도가 180도를 넘어가면 음수 값으로 변환하여 처리합니다. (예: 350도 -> -10도)
|
||||
float angleX = eulerAngles.x;
|
||||
if (angleX > 180f) angleX -= 360f;
|
||||
angleX = Mathf.Clamp(angleX, minPitch, maxPitch);
|
||||
eulerAngles.x = angleX;
|
||||
|
||||
float angleY = eulerAngles.y;
|
||||
if (angleY > 180f) angleY -= 360f;
|
||||
angleY = Mathf.Clamp(angleY, minYaw, maxYaw);
|
||||
eulerAngles.y = angleY;
|
||||
|
||||
// Z축 회전(롤)을 0으로 설정하여 카메라가 옆으로 기울어지는 것을 방지합니다.
|
||||
eulerAngles.z = 0f;
|
||||
|
||||
// 수정된 오일러 각을 다시 쿼터니언으로 변환하여 적용합니다.
|
||||
return Quaternion.Euler(eulerAngles);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Update 대신 LateUpdate를 사용하여 카메라 움직임이 다른 모든 업데이트 이후에 처리되도록 합니다.
|
||||
// 이를 통해 카메라의 떨림이나 끊김 현상을 줄일 수 있습니다.
|
||||
void LateUpdate()
|
||||
{
|
||||
HandlePanning();
|
||||
HandleRotation();
|
||||
HandleZoom();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 가운데 버튼으로 카메라를 평행 이동시킵니다.
|
||||
/// 프레임 지연으로 인한 급격한 이동을 방지하기 위해 이동량을 제한합니다.
|
||||
/// </summary>
|
||||
private void HandlePanning()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(2))
|
||||
{
|
||||
StopFocusCoroutine();
|
||||
lastPanPosition = Input.mousePosition;
|
||||
}
|
||||
|
||||
if (Input.GetMouseButton(2))
|
||||
{
|
||||
Vector3 delta = Input.mousePosition - lastPanPosition;
|
||||
|
||||
// 프레임 드랍 시 델타 값이 너무 커져서 카메라가 튀는 것을 방지하기 위해 최대값을 제한합니다.
|
||||
if (delta.magnitude > maxPanDelta)
|
||||
{
|
||||
delta = delta.normalized * maxPanDelta;
|
||||
}
|
||||
|
||||
// 높이에 따라 동적으로 패닝 속도 결정.
|
||||
float currentPanSpeed = Mathf.Lerp(lowAltitudePanSpeed, highAltitudePanSpeed, CameraYRate); // 현재 카메라 높이에 따라 패닝 속도를 보간합니다.
|
||||
// 카메라의 로컬 좌표계를 기준으로 이동량을 변환하여 월드 좌표계에서 이동시킵니다.
|
||||
transform.Translate(transform.right * -delta.x * currentPanSpeed * Time.deltaTime, Space.World);
|
||||
transform.Translate(transform.up * -delta.y * currentPanSpeed * Time.deltaTime, Space.World);
|
||||
|
||||
ValidateCameraTransform();
|
||||
|
||||
lastPanPosition = Input.mousePosition;
|
||||
DispatchEvnet();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 오른쪽 버튼으로 카메라를 회전시킵니다.
|
||||
/// 회전 축이 변하는 것을 방지하여 안정적인 회전을 구현합니다.
|
||||
/// </summary>
|
||||
private void HandleRotation()
|
||||
{
|
||||
if (Input.GetMouseButtonDown(1))
|
||||
{
|
||||
StopFocusCoroutine();
|
||||
isRotating = true;
|
||||
// 마우스 클릭 지점으로 Ray를 쏴서 회전의 중심점(pivot)을 설정합니다.
|
||||
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, 1000f))
|
||||
{
|
||||
rotationPivot = hit.point;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ray가 아무 오브젝트에도 맞지 않았다면, 카메라 앞쪽의 특정 거리를 중심점으로 사용합니다.
|
||||
rotationPivot = transform.position + transform.forward * 10f;
|
||||
}
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonUp(1))
|
||||
{
|
||||
isRotating = false;
|
||||
}
|
||||
|
||||
if (isRotating && Input.GetMouseButton(1))
|
||||
{
|
||||
float mouseX = Input.GetAxis("Mouse X") * rotationSpeed * Time.deltaTime;
|
||||
float mouseY = Input.GetAxis("Mouse Y") * rotationSpeed * Time.deltaTime;
|
||||
|
||||
// 현재 X축 회전 각도를 가져와서 -180 ~ 180 범위로 정규화합니다.
|
||||
float currentPitch = transform.eulerAngles.x;
|
||||
if (currentPitch > 180f) currentPitch -= 360f;
|
||||
|
||||
// 현재 Y축 회전 각도를 가져와서 -180 ~ 180 범위로 정규화합니다.
|
||||
float currentYaw = transform.eulerAngles.y;
|
||||
if (currentYaw > 180f) currentYaw -= 360f;
|
||||
|
||||
// 마우스 입력으로 인해 Pitch 또는 Yaw 각도가 한계를 벗어나는지 확인합니다.
|
||||
if ((mouseY > 0 && currentPitch >= maxPitch) || (mouseY < 0 && currentPitch <= minPitch)
|
||||
|| (mouseX < 0 && currentYaw >= maxYaw) || (mouseX > 0 && currentYaw <= minYaw))
|
||||
{
|
||||
return; // 한계를 넘어서는 회전은 막습니다.
|
||||
}
|
||||
|
||||
// 수평 회전으로 인해 수직 회전 축(transform.right)이 변질되는 것을 방지하기 위해
|
||||
// 회전 전의 right 벡터를 미리 저장해 둡니다.
|
||||
Vector3 verticalRotationAxis = transform.right;
|
||||
|
||||
// 설정된 중심점을 기준으로 카메라를 회전시킵니다.
|
||||
// 1. 수평 회전 (월드 Y축 기준)
|
||||
transform.RotateAround(rotationPivot, Vector3.up, -mouseX);
|
||||
// 2. 수직 회전 (미리 저장해 둔 카메라의 오른쪽 축 기준)
|
||||
transform.RotateAround(rotationPivot, verticalRotationAxis, mouseY);
|
||||
|
||||
ValidateCameraTransform();
|
||||
|
||||
DispatchEvnet();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마우스 휠로 카메라를 줌 인/아웃합니다.
|
||||
/// </summary>
|
||||
private void HandleZoom()
|
||||
{
|
||||
float scroll = Input.GetAxis("Mouse ScrollWheel");
|
||||
if (scroll != 0f)
|
||||
{
|
||||
// 현재 X축 회전 각도를 확인하여 한계 범위 밖이면 줌을 막습니다.
|
||||
float currentPitch = transform.eulerAngles.x;
|
||||
if (currentPitch > 180f) currentPitch -= 360f;
|
||||
if (currentPitch < minPitch || currentPitch > maxPitch) return;
|
||||
|
||||
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
Vector3 zoomTarget;
|
||||
|
||||
if (Physics.Raycast(ray, out RaycastHit hit))
|
||||
{
|
||||
zoomTarget = hit.point;
|
||||
}
|
||||
else
|
||||
{
|
||||
zoomTarget = ray.GetPoint(1000);
|
||||
}
|
||||
|
||||
Vector3 direction = zoomTarget - transform.position;
|
||||
Vector3 moveVector = direction.normalized * scroll * zoomSpeed;
|
||||
|
||||
// 카메라가 아래로 움직이려 하고(moveVector.y < 0), 이미 최소 높이에 도달했다면 중단합니다.
|
||||
if (moveVector.y < 0 && transform.position.y <= minCameraY) return;
|
||||
|
||||
// 카메라가 위로 움직이려 하고(moveVector.y > 0), 이미 최대 높이에 도달했다면 중단합니다.
|
||||
if (moveVector.y > 0 && transform.position.y >= maxCameraY) return;
|
||||
|
||||
StopFocusCoroutine();
|
||||
|
||||
// 줌 실행
|
||||
transform.position += direction.normalized * scroll * zoomSpeed;
|
||||
|
||||
ValidateCameraTransform();
|
||||
|
||||
DispatchEvnet();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 Transform을 중심으로 카메라를 포커싱합니다.
|
||||
/// </summary>
|
||||
/// <param name="equipmentPosition">포커스할 대상의 Transform</param>
|
||||
/// <param name="distance">대상과의 거리</param>
|
||||
public void FocusOnTargetFast(Vector3 equipmentPosition, float distance)
|
||||
{
|
||||
if (equipmentPosition == null) return;
|
||||
|
||||
Vector3 position = equipmentPosition;
|
||||
if (position.y < minCameraY)
|
||||
{
|
||||
position.y = minCameraY; // 최소 높이 제한
|
||||
}
|
||||
else if (position.y > maxCameraY)
|
||||
{
|
||||
position.y = maxCameraY; // 최대 높이 제한
|
||||
}
|
||||
|
||||
|
||||
// 카메라가 바라볼 대상의 중심점
|
||||
Vector3 targetPosition = equipmentPosition;
|
||||
|
||||
// 현재 카메라의 회전각을 유지하면서 타겟을 바라보는 방향 설정
|
||||
Vector3 directionToTarget = (targetPosition - transform.position).normalized;
|
||||
|
||||
// 타겟으로부터 지정된 거리만큼 떨어진 위치 계산
|
||||
Vector3 newPosition = targetPosition - directionToTarget * distance;
|
||||
|
||||
// 카메라 위치 설정 및 타겟을 바라보도록 함
|
||||
transform.position = newPosition;
|
||||
transform.LookAt(targetPosition);
|
||||
|
||||
// 회전 피봇 포인트 업데이트
|
||||
rotationPivot = targetPosition;
|
||||
|
||||
ValidateCameraTransform();
|
||||
|
||||
DispatchEvnet();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 Transform을 중심으로 카메라를 포커싱합니다.
|
||||
/// </summary>
|
||||
/// <param name="equipmentPosition">포커스할 대상의 Transform</param>
|
||||
/// <param name="distance">대상과의 거리</param>
|
||||
/// <param name="duration">이동에 걸리는 시간(초), 기본값 1초</param>
|
||||
public void FocusOnTarget(Vector3 equipmentPosition, float distance, float duration = 1.0f)
|
||||
{
|
||||
if (equipmentPosition == null) return;
|
||||
|
||||
StopFocusCoroutine();
|
||||
|
||||
Vector3 position = equipmentPosition;
|
||||
if (position.y < minCameraY)
|
||||
{
|
||||
position.y = minCameraY; // 최소 높이 제한
|
||||
}
|
||||
else if (position.y > maxCameraY)
|
||||
{
|
||||
position.y = maxCameraY; // 최대 높이 제한
|
||||
}
|
||||
|
||||
// 코루틴을 사용하여 부드러운 이동 구현
|
||||
focusCoroutine = StartCoroutine(SmoothFocusOnTarget(position, distance, duration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카메라를 앞쪽 방향으로 지정된 거리만큼 바깥쪽으로 부드럽게 이동합니다.
|
||||
/// </summary>
|
||||
/// <remarks>이 메서드는 코루틴을 사용하여 카메라의 위치를 앞쪽 방향으로 바깥쪽으로 부드럽게 전환합니다.
|
||||
/// 전환은 지정된 시간 동안 수행되므로
|
||||
/// 시각적으로 부드러운 움직임을 구현할 수 있습니다.</remarks>
|
||||
/// <param name="distance">카메라를 현재 위치에서 바깥쪽으로 이동할 거리(단위)입니다.</param>
|
||||
/// <param name="duration">카메라 전환이 발생하는 시간(초)입니다. 기본값은 1.0초입니다.</param>
|
||||
public void FocusOut(float distance, float duration = 1.0f)
|
||||
{
|
||||
|
||||
StopFocusCoroutine();
|
||||
|
||||
// 현재 카메라 위치와 회전값을 저장
|
||||
Vector3 startPosition = transform.position;
|
||||
Quaternion startRotation = transform.rotation;
|
||||
// 카메라가 바라보는 방향으로 지정된 거리만큼 이동
|
||||
Vector3 targetPosition = transform.position + transform.forward * distance;
|
||||
// 코루틴을 사용하여 부드러운 이동 구현
|
||||
focusCoroutine = StartCoroutine(SmoothFocusOnTarget(targetPosition, distance, duration));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 부드럽게 타겟까지 이동하는 코루틴
|
||||
/// </summary>
|
||||
private IEnumerator SmoothFocusOnTarget(Vector3 targetTransform, float distance, float duration)
|
||||
{
|
||||
// 카메라가 바라볼 대상의 중심점
|
||||
Vector3 targetPosition = targetTransform;
|
||||
|
||||
// 시작 위치와 회전 저장
|
||||
Vector3 startPosition = transform.position;
|
||||
Quaternion startRotation = transform.rotation;
|
||||
|
||||
// 타겟을 보는 최종 위치와 회전 계산
|
||||
Vector3 directionToTarget = (targetPosition - startPosition).normalized;
|
||||
Vector3 endPosition = targetPosition - directionToTarget * distance;
|
||||
|
||||
// 최종 회전값 계산
|
||||
Quaternion endRotation = Quaternion.LookRotation(targetPosition - endPosition);
|
||||
endRotation = ValidateRotation(endRotation); // 회전값 검증 및 수정
|
||||
|
||||
// 이동 시간 계산을 위한 변수
|
||||
float elapsedTime = 0f;
|
||||
|
||||
while (elapsedTime < duration)
|
||||
{
|
||||
elapsedTime += Time.deltaTime;
|
||||
float t = Mathf.Clamp01(elapsedTime / duration);
|
||||
|
||||
// 부드러운 이동을 위한 Easing 함수 적용
|
||||
float smoothT = EaseInOutCubic(t);
|
||||
|
||||
// 위치와 회전 보간
|
||||
transform.position = Vector3.Lerp(startPosition, endPosition, smoothT);
|
||||
transform.rotation = Quaternion.Slerp(startRotation, endRotation, smoothT);
|
||||
|
||||
ValidateCameraTransform();
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 정확한 최종 위치와 회전 설정
|
||||
transform.position = endPosition;
|
||||
transform.LookAt(targetPosition);
|
||||
|
||||
// 회전 피봇 포인트 업데이트
|
||||
rotationPivot = targetPosition;
|
||||
|
||||
ValidateCameraTransform();
|
||||
|
||||
DispatchEvnet();
|
||||
focusCoroutine = null; // 코루틴 완료 후 참조를 null로 설정
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cubic ease-in/out 함수로 부드러운 이동 효과를 줍니다.
|
||||
/// </summary>
|
||||
private float EaseInOutCubic(float t)
|
||||
{
|
||||
return t < 0.5f ? 4f * t * t * t : 1f - Mathf.Pow(-2f * t + 2f, 3f) / 2f;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61deddb674c074049a9b43fd58f1b355
|
||||
@@ -3,8 +3,8 @@ using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UVC.Data;
|
||||
using UVC.Factory.Modal;
|
||||
using UVC.Object3d;
|
||||
using UVC.UI.Info;
|
||||
|
||||
namespace UVC.Factory.Component
|
||||
{
|
||||
|
||||
8
Assets/Scripts/UVC/Factory/Modal.meta
Normal file
8
Assets/Scripts/UVC/Factory/Modal.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 331531bab0bc0974c89e2eb1560a5043
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
206
Assets/Scripts/UVC/Factory/Modal/InfoWindow.cs
Normal file
206
Assets/Scripts/UVC/Factory/Modal/InfoWindow.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Core;
|
||||
using UVC.UI.Modal;
|
||||
|
||||
namespace UVC.Factory.Modal
|
||||
{
|
||||
/// <summary>
|
||||
/// 3D 객체를 따라다니며 정보를 표시하는 UI 창입니다.
|
||||
/// 이 컴포넌트는 World Space Canvas 내의 UI 요소에 추가되어야 합니다.
|
||||
/// </summary>
|
||||
public class InfoWindow : SingletonScene<InfoWindow>
|
||||
{
|
||||
|
||||
[Tooltip("Label 정보 텍스트를 표시할 UI 요소")]
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI text;
|
||||
|
||||
[Tooltip("정보 창을 닫는 버튼")]
|
||||
[SerializeField]
|
||||
private Button closeButton;
|
||||
|
||||
[Tooltip("정보 창의 내용을 클립보드에 복사할 버튼")]
|
||||
[SerializeField]
|
||||
private Button copyButton;
|
||||
|
||||
[Tooltip("UI가 객체를 가리지 않도록 할 월드 좌표계 오프셋")]
|
||||
[SerializeField]
|
||||
private Vector2 screenOffset = new Vector2(10f, 10f);
|
||||
|
||||
[Tooltip("UI가 객체를 가리지 않도록 할 메뉴바 높이")]
|
||||
[SerializeField]
|
||||
private float menuBarHeight = 70f;
|
||||
|
||||
// 정보 창이 따라다닐 3D 객체의 Transform
|
||||
private Transform? target;
|
||||
|
||||
private string message = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 정보 창이 현재 화면에 표시되고 있는지 여부를 반환합니다.
|
||||
/// </summary>
|
||||
public bool IsVisible => gameObject.activeSelf;
|
||||
|
||||
private RectTransform? rectTransform;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
|
||||
// 닫기 버튼이 할당되었으면 클릭 이벤트를 연결합니다.
|
||||
if (closeButton != null)
|
||||
{
|
||||
closeButton.onClick.AddListener(Hide);
|
||||
}
|
||||
|
||||
if (copyButton != null)
|
||||
{
|
||||
copyButton.onClick.AddListener(CopyToClipboard);
|
||||
}
|
||||
|
||||
|
||||
// 처음에는 정보 창을 숨깁니다.
|
||||
if (gameObject.activeSelf)
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyToClipboard()
|
||||
{
|
||||
// 클립보드에 현재 메시지를 복사합니다.
|
||||
if (!string.IsNullOrEmpty(message))
|
||||
{
|
||||
GUIUtility.systemCopyBuffer = message;
|
||||
Toast.Show("정보가 클립보드에 복사되었습니다.", 2f, 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
// target이 설정되어 있고 활성화 상태일 때만 위치와 방향을 업데이트합니다.
|
||||
if (target != null && gameObject.activeSelf && Camera.main != null)
|
||||
{
|
||||
// 타겟의 렌더러 또는 콜라이더를 가져와 화면 상의 크기를 계산
|
||||
Bounds bounds = new Bounds(target.position, Vector3.one);
|
||||
Renderer? renderer = target.GetComponent<Renderer>();
|
||||
Collider? collider = target.GetComponent<Collider>();
|
||||
|
||||
// 렌더러가 있으면 렌더러의 바운드를, 없으면 콜라이더의 바운드를 사용
|
||||
if (renderer != null)
|
||||
{
|
||||
bounds = renderer.bounds;
|
||||
}
|
||||
else if (collider != null)
|
||||
{
|
||||
bounds = collider.bounds;
|
||||
}
|
||||
|
||||
// 바운드의 오른쪽 지점을 월드 좌표로 계산
|
||||
Vector3 rightPoint = bounds.center + new Vector3(bounds.extents.x, 0, 0);
|
||||
|
||||
// 바운드의 오른쪽 지점을 스크린 좌표로 변환
|
||||
Vector3 screenPosRight = Camera.main.WorldToScreenPoint(rightPoint);
|
||||
|
||||
// 추가 오프셋 적용
|
||||
screenPosRight.x += screenOffset.x;
|
||||
screenPosRight.y += screenOffset.y;
|
||||
// 메뉴바 영역 고려 및 화면 밖으로 나가지 않도록 제한
|
||||
screenPosRight.x = Mathf.Clamp(screenPosRight.x, 0, Screen.width - rectTransform!.rect.width);
|
||||
screenPosRight.y = Mathf.Clamp(screenPosRight.y, rectTransform!.rect.height, Screen.height - menuBarHeight);
|
||||
|
||||
// RectTransform을 사용하여 UI 위치 설정
|
||||
// 캔버스의 렌더링 모드에 따라 다르게 처리
|
||||
Canvas canvas = GetComponentInParent<Canvas>();
|
||||
if (canvas != null)
|
||||
{
|
||||
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
|
||||
{
|
||||
rectTransform.position = screenPosRight;
|
||||
}
|
||||
else if (canvas.renderMode == RenderMode.ScreenSpaceCamera ||
|
||||
canvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
// 스크린 좌표를 캔버스 상의 로컬 좌표로 변환
|
||||
Vector2 localPoint;
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
canvas.GetComponent<RectTransform>(),
|
||||
screenPosRight,
|
||||
canvas.renderMode == RenderMode.ScreenSpaceCamera ? canvas.worldCamera : Camera.main,
|
||||
out localPoint);
|
||||
rectTransform.localPosition = new Vector3(localPoint.x, localPoint.y, rectTransform.localPosition.z);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// UI가 항상 보이도록 카메라를 향하게 설정 (World Space Canvas인 경우에만 필요)
|
||||
Canvas parentCanvas = GetComponentInParent<Canvas>();
|
||||
if (parentCanvas != null && parentCanvas.renderMode == RenderMode.WorldSpace)
|
||||
{
|
||||
transform.LookAt(transform.position + Camera.main.transform.rotation * Vector3.forward,
|
||||
Camera.main.transform.rotation * Vector3.up);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정보 창을 특정 대상에 대해 표시합니다.
|
||||
/// </summary>
|
||||
/// <param name="targetObject">정보를 표시할 3D 객체의 Transform</param>
|
||||
/// <param name="information">표시할 정보 문자열</param>
|
||||
public void Show(Transform targetObject, Dictionary<string, object> information)
|
||||
{
|
||||
target = targetObject;
|
||||
|
||||
UpdateInformation(information);
|
||||
|
||||
gameObject.SetActive(true);
|
||||
|
||||
// 즉시 위치와 방향을 업데이트합니다.
|
||||
LateUpdate();
|
||||
}
|
||||
|
||||
public void UpdateInformation(Dictionary<string, object> information)
|
||||
{
|
||||
if (target == null) return;
|
||||
if (text != null)
|
||||
{
|
||||
message = string.Empty;
|
||||
string combinedString = string.Empty;
|
||||
foreach (var kvp in information)
|
||||
{
|
||||
// <indent> 태그를 사용하여 줄바꿈 시에도 정렬이 유지되도록 합니다.
|
||||
combinedString += $"{kvp.Key}<pos=40%><indent=40%>{kvp.Value ?? "null"}</indent>\n";
|
||||
message += $"{kvp.Key}: {kvp.Value ?? "null"}\n"; // 메시지에 추가
|
||||
}
|
||||
combinedString = combinedString.TrimEnd('\n'); // 마지막 줄바꿈 제거
|
||||
message = message.TrimEnd('\n'); // 마지막 줄바꿈 제거
|
||||
text.text = combinedString;
|
||||
}
|
||||
// size를 text에 맞게 조정합니다.
|
||||
RectTransform? rect = GetComponent<RectTransform>();
|
||||
if (rect != null && text != null)
|
||||
{
|
||||
RectTransform textRect = text.GetComponent<RectTransform>();
|
||||
float marginHeight = rect.rect.height - textRect.rect.height; // 상하 여백
|
||||
rect.sizeDelta = new Vector2(rect.rect.width, text.preferredHeight + marginHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정보 창을 숨깁니다.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
target = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Factory/Modal/InfoWindow.cs.meta
Normal file
2
Assets/Scripts/UVC/Factory/Modal/InfoWindow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 620e52b408949c340adef1110323cb7c
|
||||
Reference in New Issue
Block a user