Files
AZTECH_WB/Assets/Scripts/Manager/MachineStatusItemManager.cs
정영민 81ba9de085 [정영민] 층 조절 기능 추가 및 아이콘 오류 수정
26.03.18
- 층 자동 조절 기능 및 수동 조절 기능 추가
- 설비명 아이콘 안 보이는 오류 수정
- 카메라 2D/3D 위치 조정
2026-03-20 12:47:10 +09:00

302 lines
11 KiB
C#

using AZTECHWB.Command;
using AZTECHWB.Constants;
using AZTECHWB.Core;
using AZTECHWB.Extensions;
using Cysharp.Threading.Tasks;
using System.Collections.Generic;
using UnityEngine;
namespace AZTECHWB.Management
{
public class MachineStatusItemManager : Manager
{
[Header("References")]
private MachineStatusItem machineStatusItem;
[Header("Position Settings")]
public Vector3 worldOffset = new Vector3(0, 1.5f, 0);
public float verticalPadding = 5f;
public float overlapThreshold = 2f;
public float smoothSpeed = 10f;
[Header("Visibility Settings")]
public LayerMask occlusionLayer;
public float occlusionCheckInterval = 0.1f;
private Dictionary<MachineStatusItem, Vector2> smoothedPositions = new Dictionary<MachineStatusItem, Vector2>();
private float occlusionTimer;
[Header("Scale Settings")]
public float minScale = 0.5f; // 가장 멀 때 크기
public float maxScale = 1.2f; // 가장 가까울 때 크기
public float scaleSmoothing = 5f; // 스케일 변화 스무딩 속도
private float maxDistance = 50f; // 스케일 계산 기준 최대 거리
private Dictionary<MachineStatusItem, float> smoothedScales = new Dictionary<MachineStatusItem, float>();
private Dictionary<string, MachineStatusItem> machineIcons = new();
private Dictionary<MachineStatusItem, Machine> iconToMachines = new();
private OrbitalController controller;
private Machine[] machines;
private bool activeIconEnable;
public override async UniTask Init()
{
machineStatusItem = Resources.Load<MachineStatusItem>($"{ResourceURL.uiPrefabFolderPath}{nameof(MachineStatusItem)}");
controller = AZTECHAppMain.Instance.cameraController;
machines = AZTECHSceneMain.Instance.building.machineList.ToArray();
maxDistance = controller.maxDistance;
await UniTask.CompletedTask;
}
public void SetInteractableIcons(bool isActive)
{
foreach (var machineIcon in machineIcons.Values)
{
machineIcon.isInteractable = isActive;
}
}
public void SetActiveIcons(bool isActive)
{
activeIconEnable = isActive;
if(isActive)
return;
foreach (var machineIcon in machineIcons.Values)
{
machineIcon.Deactive();
}
}
public void SetMachineData(List<CompleteInfo> machineData)
{
foreach (var data in machineData)
{
var machine = FindMachineWithCode(data.worknm);
if (machine == null)
continue;
if (machineIcons.ContainsKey(data.worknm))
{
machineIcons[data.worknm].SetData(data);
continue;
}
var machineIcon = Instantiate(machineStatusItem, transform);
machineIcon.Init(machine);
machineIcon.SetData(data);
machineIcon.onClickIcon += OnClickMachineKPI;
machineIcons.Add(data.worknm, machineIcon);
machineIcon.Deactive();
machine.machineStatusItem = machineIcon;
iconToMachines.Add(machineIcon, machine);
}
}
private void OnClickMachineKPI(MachineStatusItem machineKPI)
{
machineKPI.transform.SetAsLastSibling();
var currentMachine = iconToMachines[machineKPI];
new OpenMachineDetailDashboardPanelCommand(currentMachine, machineKPI.data).Execute();
}
private Machine FindMachineWithCode(string worknm)
{
Machine machine = null;
for (int i = 0; i < machines.Length; i++)
{
if (machines[i].machineName == worknm)
{
machine = machines[i];
break;
}
}
return machine;
}
private void LateUpdate()
{
if (machineIcons.Values.Count == 0 || controller.Camera == null)
return;
List<MachineStatusItem> allLabels = new List<MachineStatusItem>(machineIcons.Values);
// 1단계: 전체 라벨 스크린 위치 & 거리 계산
UpdateScreenData(allLabels);
// 2단계: 활성화 여부 판정 (화면 이탈 + 가려짐)
occlusionTimer += Time.deltaTime;
if (occlusionTimer >= occlusionCheckInterval)
{
UpdateVisibility(allLabels);
occlusionTimer = 0f;
}
// 3단계: 활성화된 라벨만 카메라 거리순 정렬
List<MachineStatusItem> visibleLabels = allLabels.FindAll(l => l.gameObject.activeSelf);
visibleLabels.Sort((a, b) => a.distanceToCamera.CompareTo(b.distanceToCamera));
// 4단계: 거리에 따른 UI 크기 조정
UpdateScales(visibleLabels);
// 5단계: 겹침 해소 → 목표 위치 계산
Dictionary<MachineStatusItem, Vector2> targetPositions = ResolveOverlaps(visibleLabels);
// 6단계: 스무딩 적용 후 RectTransform에 반영
ApplyPositions(targetPositions);
}
void UpdateScreenData(List<MachineStatusItem> labels)
{
foreach (var label in labels)
{
if (label.targetTransform == null) continue;
Vector3 worldPos = label.targetTransform.GetMeshCenter() + worldOffset;
Vector3 screenPos = controller.Camera.WorldToScreenPoint(worldPos);
label.screenPosition = new Vector2(screenPos.x, screenPos.y);
label.distanceToCamera = Vector3.Distance(controller.Camera.transform.position, label.targetTransform.position);
}
}
void UpdateVisibility(List<MachineStatusItem> labels)
{
if (!activeIconEnable) return;
foreach (var label in labels)
{
// 카메라 뒤쪽 체크
Vector3 screenPos = controller.Camera.WorldToScreenPoint(label.targetTransform.position + worldOffset);
if (screenPos.z < 0)
{
label.Deactive();
continue;
}
// 화면 이탈 체크
if (!IsOnScreen(label.screenPosition))
{
label.Deactive();
continue;
}
// 특정 레이어에 가려짐 체크
if (IsOccluded(label.targetTransform.position))
{
label.Deactive();
continue;
}
label.Active();
}
}
bool IsOnScreen(Vector2 screenPos)
{
return screenPos.x >= 0 && screenPos.x <= Screen.width && screenPos.y >= 0 && screenPos.y <= Screen.height;
}
bool IsOccluded(Vector3 targetWorldPos)
{
Vector3 camPos = controller.Camera.transform.position;
Vector3 direction = targetWorldPos - camPos;
float distance = direction.magnitude;
return Physics.Raycast(camPos, direction.normalized, distance - 0.1f, occlusionLayer);
}
Dictionary<MachineStatusItem, Vector2> ResolveOverlaps(List<MachineStatusItem> visibleLabels)
{
Dictionary<MachineStatusItem, Vector2> targetPositions = new Dictionary<MachineStatusItem, Vector2>();
List<Rect> placedRects = new List<Rect>();
foreach (var label in visibleLabels)
{
Vector2 size = label.GetSize();
Vector2 targetPos = label.screenPosition;
bool overlapping = true;
int maxIterations = 20;
while (overlapping && maxIterations-- > 0)
{
overlapping = false;
Rect currentRect = GetRect(targetPos, size);
foreach (Rect placed in placedRects)
{
if (IsOverlapping(currentRect, placed))
{
// X는 겹친 라벨의 X로 고정, Y만 위로 밀어올림
targetPos.x = placed.center.x;
targetPos.y = placed.yMax + verticalPadding + size.y * 0.5f;
overlapping = true;
break;
}
}
}
targetPositions[label] = targetPos;
placedRects.Add(GetRect(targetPos, size));
}
return targetPositions;
}
void ApplyPositions(Dictionary<MachineStatusItem, Vector2> targetPositions)
{
foreach (var kvp in targetPositions)
{
MachineStatusItem label = kvp.Key;
Vector2 target = kvp.Value;
if (!smoothedPositions.ContainsKey(label))
smoothedPositions[label] = target;
smoothedPositions[label] = Vector2.Lerp(smoothedPositions[label],target,Time.deltaTime * smoothSpeed);
// Manager가 직접 RectTransform 조작
label.rectTransform.position = new Vector3(smoothedPositions[label].x, smoothedPositions[label].y, 0);
}
}
void UpdateScales(List<MachineStatusItem> visibleLabels)
{
foreach (var label in visibleLabels)
{
// 거리 비율 → 목표 스케일 계산 (원본과 동일한 방식)
float t = Mathf.InverseLerp(maxDistance, 0f, label.distanceToCamera);
float targetScale = Mathf.Lerp(minScale, maxScale, t);
// 스무딩 초기값 설정
if (!smoothedScales.ContainsKey(label))
smoothedScales[label] = targetScale;
// Lerp로 부드럽게 스케일 변화
smoothedScales[label] = Mathf.Lerp(
smoothedScales[label],
targetScale,
Time.deltaTime * scaleSmoothing
);
label.transform.localScale = Vector3.one * smoothedScales[label];
}
}
Rect GetRect(Vector2 centerPos, Vector2 size)
{
return new Rect(centerPos.x - size.x * 0.5f, centerPos.y - size.y * 0.5f, size.x, size.y);
}
bool IsOverlapping(Rect a, Rect b)
{
return a.xMin < b.xMax - overlapThreshold && a.xMax > b.xMin + overlapThreshold &&
a.yMin < b.yMax - overlapThreshold && a.yMax > b.yMin + overlapThreshold;
}
}
}