[한수빈] MachineUIManager.cs 리팩토링
26.01.21 천일 설비 UI 매니저 코드에서, 불필요한 프레임 연산 줄이고 성능 최적화
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using UnityEngine.Profiling;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -11,233 +12,114 @@ namespace CHN
|
||||
public class MachineUIManager : MonoBehaviour, ISingle
|
||||
{
|
||||
private OrbitalController cam;
|
||||
|
||||
private Building building;
|
||||
public UI_MachineKPI prefab_MachineKPI;
|
||||
|
||||
private Machine[] machines;
|
||||
private List<Machine> matchingMachines = new();
|
||||
private Dictionary<string, UI_MachineKPI> machineKPIs = new();
|
||||
private Dictionary<UI_MachineKPI, Machine> kpiToMachines = new();
|
||||
private List<UI_MachineKPI> kpis = new List<UI_MachineKPI>();
|
||||
//private List<UI_MachineKPI> kpis = new List<UI_MachineKPI>();
|
||||
|
||||
public Action<Machine> onClickKPIToMachine;
|
||||
public Action<MachineDataSet, Machine> onClickKPIToData;
|
||||
|
||||
public float defaultNameHeight;
|
||||
public Vector3 originScale;
|
||||
public float defaultNameHeight = 2f;
|
||||
[Range(0.1f, 2f)] public float updateInterval = 0.05f;
|
||||
//public Vector3 originScale;
|
||||
|
||||
[Range(0.1f, 0.8f)]
|
||||
public float minScale;
|
||||
[Range(0.5f, 1.5f)]
|
||||
public float maxScale;
|
||||
[Range(0.1f, 2f)]
|
||||
public float scaleClamp;
|
||||
[Header("Scale Settings")]
|
||||
[Range(0.1f, 0.8f)] public float minScale;
|
||||
[Range(0.5f, 1.5f)] public float maxScale;
|
||||
[Range(0.1f, 2f)] public float scaleClamp;
|
||||
|
||||
private List<RectTransform> activeUIElements = new();
|
||||
private Vector3[] worldCorners = new Vector3[4]; // 개선사항: GC 방지용 미리 할당
|
||||
|
||||
public void Clear()
|
||||
private void Start()
|
||||
{
|
||||
uiElements.Clear();
|
||||
matchingMachines.Clear();
|
||||
kpiToMachines.Clear();
|
||||
cam = FindSingle<OrbitalController>(); // 개선사항: Start에서 한 번만 호출하여 캐싱
|
||||
building = FindSingle<Building>();
|
||||
// 주기적으로 무거운 연산을 실행 (매 프레임 X)
|
||||
StartCoroutine(UpdateLoop());
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator UpdateLoop()
|
||||
{
|
||||
var wait = new WaitForSeconds(updateInterval); // 개선사항: 매 프레임 실행하던 로직을 updateInterval마다 실행하도록 변경
|
||||
while (true)
|
||||
{
|
||||
Profiler.BeginSample("MachineKPI_TotalUpdate");
|
||||
if (matchingMachines.Count > 0)
|
||||
{
|
||||
UpdateMachineVisibilityAndScale();
|
||||
GroupOverlappingUIElements();
|
||||
}
|
||||
Profiler.EndSample();
|
||||
yield return wait; // 개선사항: 루프 밖에서 딱 한번 생성하여 캐싱해둔 것을 재사용 -> 메모리 할당 0
|
||||
}
|
||||
}
|
||||
|
||||
#region Machine
|
||||
public void SetMachineKPI(List<MachineDataSet> machineData)
|
||||
{
|
||||
cam = FindSingle<OrbitalController>();
|
||||
prefab_MachineKPI = Resources.Load<UI_MachineKPI>("Prefabs/UI/PRF_UI_MachineKPI");
|
||||
|
||||
var building = FindSingle<Building>();
|
||||
machines = building.floors.SelectMany(f => f.machines).ToArray();
|
||||
|
||||
Clear();
|
||||
|
||||
foreach (var data in machineData)
|
||||
{
|
||||
var machineName = machines.ToList().Find(machine => machine.code == data.machineName).machineName;
|
||||
var targetMachine = Array.Find(machines, m => m.code == data.machineName);
|
||||
if (targetMachine == null) continue;
|
||||
|
||||
if (machineKPIs.ContainsKey(data.machineName))
|
||||
if (!machineKPIs.TryGetValue(data.machineName, out var machineKPI))
|
||||
{
|
||||
machineKPIs[data.machineName].SetData(data, machineName);
|
||||
uiElements.Add(machineKPIs[data.machineName].rectTransform);
|
||||
continue;
|
||||
machineKPI = Instantiate(prefab_MachineKPI, transform);
|
||||
machineKPI.onClickKPI += OnClickMachineKPI;
|
||||
machineKPI.name = data.machineName;
|
||||
machineKPIs.Add(data.machineName, machineKPI);
|
||||
}
|
||||
|
||||
var machineKPI = Instantiate(prefab_MachineKPI, transform);
|
||||
machineKPI.SetData(data, machineName);
|
||||
machineKPI.onClickKPI += OnClickMachineKPI;
|
||||
machineKPI.name = data.machineName;
|
||||
machineKPI.SetActive(false);
|
||||
uiElements.Add(machineKPI.rectTransform);
|
||||
|
||||
kpis.Add(machineKPI);
|
||||
machineKPIs.Add(data.machineName, machineKPI);
|
||||
machineKPI.SetData(data, targetMachine.machineName);
|
||||
machineKPI.SetActive(false);
|
||||
}
|
||||
|
||||
foreach (var machine in machines)
|
||||
{
|
||||
if (machineKPIs.ContainsKey(machine.code))
|
||||
if (machineKPIs.TryGetValue(machine.code, out var kpi))
|
||||
{
|
||||
machine.machineKPI = machineKPIs[machine.code];
|
||||
machine.machineKPI = kpi;
|
||||
matchingMachines.Add(machine);
|
||||
kpiToMachines.Add(machine.machineKPI, machine);
|
||||
kpiToMachines.Add(kpi, machine);
|
||||
machine.SetAnimationSpeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
private void OnClickMachineKPI(UI_MachineKPI machineKPI)
|
||||
|
||||
private void UpdateMachineVisibilityAndScale()
|
||||
{
|
||||
machineKPI.transform.SetAsLastSibling();
|
||||
var currentMachine = kpiToMachines[machineKPI];
|
||||
onClickKPIToMachine?.Invoke(currentMachine);
|
||||
onClickKPIToData?.Invoke(machineKPI.data, currentMachine);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void Update()
|
||||
{
|
||||
GroupOverlappingUIElements();
|
||||
RangeDetection();
|
||||
}
|
||||
|
||||
public List<RectTransform> uiElements = new List<RectTransform>();
|
||||
public List<List<RectTransform>> groupedElements = new List<List<RectTransform>>();
|
||||
|
||||
void GroupOverlappingUIElements()
|
||||
{
|
||||
foreach(var matchingMachine in matchingMachines)
|
||||
{
|
||||
var machinePos = matchingMachine.centerPos;
|
||||
var screenPos = cam.camera.WorldToScreenPoint(new Vector3(machinePos.x, machinePos.y + defaultNameHeight, machinePos.z));
|
||||
|
||||
matchingMachine.machineKPI.transform.position = screenPos;
|
||||
}
|
||||
|
||||
// 그룹화된 UI 요소들을 초기화
|
||||
groupedElements.Clear();
|
||||
|
||||
// UI 요소들을 겹침 여부에 따라 그룹화
|
||||
var uncheckedElements = new HashSet<RectTransform>(uiElements); // 겹침 여부 체크 안된 UI 요소들
|
||||
|
||||
// 겹침을 확인할 UI 요소들을 순차적으로 그룹화
|
||||
while (uncheckedElements.Count > 0)
|
||||
{
|
||||
var currentElement = uncheckedElements.First();
|
||||
uncheckedElements.Remove(currentElement);
|
||||
var group = new List<RectTransform> { currentElement };
|
||||
|
||||
// 그룹화된 UI 요소들을 추가
|
||||
var overlappingElements = uncheckedElements.Where(element => AreRectanglesOverlapping(currentElement, element)).ToList();
|
||||
foreach (var overlappingElement in overlappingElements)
|
||||
{
|
||||
uncheckedElements.Remove(overlappingElement);
|
||||
group.Add(overlappingElement);
|
||||
}
|
||||
|
||||
groupedElements.Add(group);
|
||||
}
|
||||
|
||||
foreach (var group in groupedElements)
|
||||
{
|
||||
var centerPos = GroupCenterCalculate(group);
|
||||
|
||||
for (int i = 0; i < group.Count; i++)
|
||||
{
|
||||
var kpi = group[i];
|
||||
var newPos = new Vector3(centerPos.x, centerPos.y + kpi.rect.height * i * kpi.transform.localScale.y, centerPos.z);
|
||||
|
||||
kpi.transform.localPosition = newPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
private bool AreRectanglesOverlapping(RectTransform rectA, RectTransform rectB)
|
||||
{
|
||||
if (!rectB.gameObject.activeSelf)
|
||||
return false;
|
||||
|
||||
if (!rectA.gameObject.activeSelf)
|
||||
return false;
|
||||
|
||||
Rect rectAWorld = GetWorldRect(rectA);
|
||||
Rect rectBWorld = GetWorldRect(rectB);
|
||||
|
||||
return rectAWorld.Overlaps(rectBWorld);
|
||||
}
|
||||
private Rect GetWorldRect(RectTransform rectTransform)
|
||||
{
|
||||
Vector3[] worldCorners = new Vector3[4];
|
||||
rectTransform.GetWorldCorners(worldCorners);
|
||||
|
||||
Vector2 min = new Vector2(worldCorners[0].x, worldCorners[0].y);
|
||||
Vector2 max = new Vector2(worldCorners[2].x, worldCorners[2].y);
|
||||
|
||||
return new Rect(min, max - min);
|
||||
}
|
||||
private Vector3 GroupCenterCalculate(List<RectTransform> group)
|
||||
{
|
||||
var centerPos = Vector3.zero;
|
||||
group.Sort((a, b) => a.transform.localPosition.y.CompareTo(b.transform.localPosition.y));
|
||||
|
||||
foreach (var kpi in group)
|
||||
{
|
||||
centerPos += kpi.transform.localPosition;
|
||||
}
|
||||
centerPos /= group.Count;
|
||||
|
||||
return centerPos;
|
||||
}
|
||||
void RangeDetection()
|
||||
{
|
||||
var layerMask = LayerMask.GetMask("Camera", "Floor Wall");
|
||||
|
||||
// 개선사항: 기존 RangeDetection 함수 -> 코루틴 안에서 실행되는 가시성 업데이트 루프에 포함
|
||||
int layerMask = LayerMask.GetMask("Camera", "Floor Wall");
|
||||
float t = Mathf.InverseLerp(cam.option.maxDistance, 0f, cam.option.currentDistance);
|
||||
float scale = Mathf.Lerp(minScale, maxScale, t);
|
||||
var newScale = new Vector3(scale, scale, scale);
|
||||
float scaleValue = Mathf.Lerp(minScale, maxScale, t);
|
||||
Vector3 newScale = new Vector3(scaleValue, scaleValue, scaleValue);
|
||||
|
||||
Floor currentFloor = building.currentFloor;
|
||||
activeUIElements.Clear();
|
||||
|
||||
foreach (var machine in matchingMachines)
|
||||
{
|
||||
MachineKPIsActive(machine, layerMask);
|
||||
var machineKPI = machine.machineKPI;
|
||||
machineKPI.transform.localScale = newScale;
|
||||
}
|
||||
}
|
||||
bool IsScreenRange(Machine machine)
|
||||
{
|
||||
Vector3 viewPos = cam.camera.WorldToViewportPoint(machine.centerPos);
|
||||
bool shouldBeActive = IsMachineVisible(machine, currentFloor, layerMask);
|
||||
|
||||
if (viewPos.x >= 0 && viewPos.x <= 1 && viewPos.y >= 0 && viewPos.y <= 1 && viewPos.z > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void MachineKPIsActive(Machine machine, LayerMask layerMask)
|
||||
{
|
||||
var currentFloor = FindSingle<Building>().currentFloor;
|
||||
var machineInFloor = machine.GetComponentInParent<Floor>();
|
||||
|
||||
var dir = cam.camera.transform.position - machine.centerPos;
|
||||
var hit = new RaycastHit();
|
||||
|
||||
if (machineInFloor != currentFloor)
|
||||
{
|
||||
machine.machineKPI.Deactive();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Physics.Raycast(machine.centerPos, dir, out hit, Mathf.Infinity, layerMask))
|
||||
{
|
||||
var hitCameraLayer = hit.collider.gameObject.layer.Equals(LayerMask.NameToLayer("Camera"));
|
||||
|
||||
if (hitCameraLayer)
|
||||
if (shouldBeActive)
|
||||
{
|
||||
if (!IsScreenRange(machine))
|
||||
{
|
||||
machine.machineKPI.Deactive();
|
||||
}
|
||||
else
|
||||
{
|
||||
machine.machineKPI.Active();
|
||||
}
|
||||
machine.machineKPI.Active();
|
||||
machine.machineKPI.transform.localScale = newScale;
|
||||
// 월드 좌표를 스크린 좌표로 변환하여 1차 배치
|
||||
Vector3 screenPos = cam.camera.WorldToScreenPoint(machine.centerPos + Vector3.up * defaultNameHeight);
|
||||
machine.machineKPI.transform.position = screenPos;
|
||||
activeUIElements.Add(machine.machineKPI.rectTransform);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -245,5 +127,79 @@ namespace CHN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsMachineVisible(Machine machine, Floor currentFloor, int layerMask)
|
||||
{
|
||||
// 개선사항: 가장 가벼운 연산부터 수행 -> 실패 시 바로 리턴하도록 (조기 리턴)
|
||||
|
||||
// 층 검사 (기존 MachineKPIsActive 함수)
|
||||
if (machine.GetComponentInParent<Floor>() != currentFloor) return false;
|
||||
|
||||
// 화면 안에 있는지 검사 (기존 IsScreenRange 함수)
|
||||
Vector3 viewPos = cam.camera.WorldToViewportPoint(machine.centerPos);
|
||||
if (!(viewPos.x >= 0 && viewPos.x <= 1 && viewPos.y >= 0 && viewPos.y <= 1 && viewPos.z > 0)) return false;
|
||||
|
||||
// 레이캐스트 (기존 MachineKPIsActive 함수)
|
||||
Vector3 dir = cam.camera.transform.position - machine.centerPos;
|
||||
if (Physics.Raycast(machine.centerPos, dir, out RaycastHit hit, Mathf.Infinity, layerMask))
|
||||
{
|
||||
return hit.collider.gameObject.layer == LayerMask.NameToLayer("Camera");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
void GroupOverlappingUIElements()
|
||||
{
|
||||
if (activeUIElements.Count == 0) return;
|
||||
|
||||
// 개선사항: uncheckedElements.Where(...) 같은 할당 연산 제거 -> 복잡한 그룹 계산 없이 정렬
|
||||
// 정렬: Y좌표 기준으로 정렬하여 아래서부터 쌓음 (LINQ 대신 List.Sort로 할당 방지)
|
||||
activeUIElements.Sort((a, b) => a.position.y.CompareTo(b.position.y));
|
||||
|
||||
// 단순화된 중첩 방지 로직 (그리드나 그룹핑 대신 순차적 체크)
|
||||
for (int i = 0; i < activeUIElements.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < activeUIElements.Count; j++)
|
||||
{
|
||||
RectTransform rectA = activeUIElements[i];
|
||||
RectTransform rectB = activeUIElements[j];
|
||||
|
||||
if (AreRectsOverlapping(rectA, rectB))
|
||||
{
|
||||
// 겹치면 B를 A 위로 올림
|
||||
float offset = rectA.rect.height * rectA.lossyScale.y;
|
||||
rectB.position = new Vector3(rectB.position.x, rectA.position.y + offset, rectB.position.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private bool AreRectsOverlapping(RectTransform rectA, RectTransform rectB)
|
||||
{
|
||||
return GetWorldRect(rectA).Overlaps(GetWorldRect(rectB));
|
||||
}
|
||||
private Rect GetWorldRect(RectTransform rectTransform)
|
||||
{
|
||||
rectTransform.GetWorldCorners(worldCorners); // 개선사항: worldCorners를 멤버 변수로 캐싱 -> GetWorldRect 호출 시마다 발생하는 배열 생성 막음
|
||||
return new Rect(worldCorners[0], worldCorners[2] - worldCorners[0]);
|
||||
}
|
||||
|
||||
private void OnClickMachineKPI(UI_MachineKPI machineKPI)
|
||||
{
|
||||
machineKPI.transform.SetAsLastSibling();
|
||||
if (kpiToMachines.TryGetValue(machineKPI, out var machine))
|
||||
{
|
||||
onClickKPIToMachine?.Invoke(machine);
|
||||
onClickKPIToData?.Invoke(machineKPI.data, machine);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
activeUIElements.Clear();
|
||||
matchingMachines.Clear();
|
||||
kpiToMachines.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user