using System.Collections.Generic; using UnityEngine; using UVC.Data.Core; using UVC.Factory.Playback; namespace UVC.Factory.Component { /// /// 씬에 표시되는 개별 AGV(무인 운반차)를 제어하는 클래스입니다. /// FactoryObject를 상속받아, AGV의 데이터 처리, 3D 모델의 이동 및 회전, 정보 표시 기능을 구현합니다. /// /// /// 이 클래스는 AGVManager에 의해 동적으로 생성되고 관리됩니다. /// AGVManager로부터 실시간 데이터를 받아 ProcessData 메서드에서 처리하고, /// Unity의 Update 메서드에서 매 프레임마다 부드러운 시각적 이동을 구현합니다. /// public class AGV : FactoryObject { // 서버에서 받은 좌표(예: 밀리미터 단위)를 Unity 씬의 단위(미터)로 변환하기 위한 스케일 값입니다. // 예를 들어, 서버 좌표 1000이 Unity에서 1미터가 되려면 0.001f로 설정합니다. private float scaleFactor = 0.0005f; // Unity에서 사용하는 단위로 변환하기 위한 스케일 팩터 // 데이터로부터 수신한 AGV의 목표 위치와 목표 회전값입니다. // AGV는 현재 위치에서 이 목표 지점을 향해 부드럽게 움직입니다. private Vector3 targetPosition; private Quaternion targetRotation; // 움직임과 회전의 부드러움을 조절할 속도 변수입니다. // Unity 인스펙터 창에서 실시간으로 값을 조절하며 최적의 움직임을 찾을 수 있습니다. [Tooltip("목표 지점까지 도달하는 데 걸리는 시간(초)입니다. 작을수록 빠릅니다.")] [SerializeField] private float moveSpeed = 0.9f; [Tooltip("목표 방향까지 도달하는 데 걸리는 시간(초)입니다. 작을수록 빠릅니다.")] [SerializeField] private float rotationSpeed = 0.5f; [Tooltip("이 거리(미터)를 초과하면 보간 없이 즉시 위치를 변경합니다.")] [SerializeField] private float teleportDistanceThreshold = 2.0f; // 1미터 이상 차이나면 순간이동 [Tooltip("이 각도를 초과하면 보간 없이 즉시 회전각을 변경합니다.")] [SerializeField] private float teleportRotationThreshold = 45.0f; // 5도 이상 차이나면 순간이동 private Renderer? renderer; private bool isRed = false; private float timeScale = 1.0f; // 현재 시간 스케일, 기본값은 1.0f (실시간) // SmoothDamp 함수가 사용하는 현재 속도 값입니다. 0으로 초기화해야 합니다. private Vector3 velocity = Vector3.zero; private float angularVelocity = 0; /// /// AGV 객체가 생성될 때 처음 한 번 호출되는 초기화 메서드입니다. /// private void Start() { renderer = modelObject.GetComponent(); // 시작 시에는 현재 위치를 목표 위치로 설정하여 의도치 않은 움직임을 방지합니다. targetPosition = transform.position; targetRotation = transform.rotation; // 사용자가 AGV를 클릭했을 때 정보창에 표시될 데이터 항목과 순서를 정의합니다. DataOrderedMask = new List { "VHL_NAME", "NODE_ID", "REAL_ID", "VHL_STATE", "BATT", "JOB_ID", "TIMESTAMP", }; PlaybackService.Instance.OnChangeTimeScale += OnChangeTimeScaleHandler; } private void OnChangeTimeScaleHandler(float timeScale) { this.timeScale = timeScale; } protected override void OnDestroy() { PlaybackService.Instance.OnChangeTimeScale -= OnChangeTimeScaleHandler; base.OnDestroy(); } /// /// AGVManager로부터 새로운 데이터를 받았을 때 호출되는 핵심 메서드입니다. /// 받은 데이터를 기반으로 AGV의 내부 상태와 목표 위치를 갱신합니다. /// /// /// 이 메서드는 FactoryObject의 추상 메서드를 재정의한 것입니다. /// AGV의 위치, 회전, 상태 등 모든 동적인 정보는 이 메서드를 통해 업데이트됩니다. /// /// AGV의 최신 정보가 담긴 데이터 객체입니다. protected override void ProcessData(DataObject newData) { // 처음 데이터를 받는 경우 (data가 null일 때) if (data == null) { // 새 데이터로 위치와 회전을 즉시 설정합니다. UpdatePositionAndRotation(newData); // 받은 데이터를 내부 데이터 저장소에 저장합니다. data = newData; // 시작 시에는 현재 위치를 목표 위치로 설정하여 움직이지 않도록 합니다. targetPosition = transform.position; targetRotation = transform.rotation; } else // 이미 데이터가 있는 경우 (업데이트) { // 새 데이터를 기반으로 목표 위치와 회전을 갱신합니다. UpdatePositionAndRotation(newData); // 기존 데이터(data)에 새로운 데이터(newData)의 내용을 덮어씁니다. foreach (var keyValue in newData) { data[keyValue.Key] = keyValue.Value; } } } /// /// 데이터 객체로부터 위치(X, Y) 및 각도(DEGREE) 값을 읽어와 AGV의 목표 위치와 회전을 설정합니다. /// /// 위치와 각도 정보가 포함된 데이터 객체입니다. private void UpdatePositionAndRotation(DataObject newData) { // 처음 데이터를 받는 경우, 받은 데이터로 즉시 위치를 설정합니다. if (data == null) { float x = newData.GetFloat("X").Value * scaleFactor; float y = newData.GetFloat("Y").Value * scaleFactor; Quaternion rotation = Quaternion.Euler(0, newData.GetFloat("DEGREE").Value, 0); transform.position = new Vector3(x, 0, y); transform.rotation = rotation; } else // 이후 업데이트의 경우 { bool changed = false; float? newX = newData.GetFloat("X"); float? newY = newData.GetFloat("Y"); float x = data.GetFloat("X").Value; float y = data.GetFloat("Y").Value; if ((newX.HasValue && x != newX) || (newY.HasValue && y != newY)) { Vector3 newTargetPosition = transform.position; if (newX.HasValue && x != newX) newTargetPosition.x = newX.Value * scaleFactor; if (newY.HasValue && y != newY) newTargetPosition.z = newY.Value * scaleFactor; // 현재 위치와 새로운 목표 위치 사이의 거리를 계산합니다. float distanceToTarget = Vector3.Distance(transform.position, newTargetPosition); if (distanceToTarget > 0) { // 거리가 설정된 임계값을 초과하면, 보간을 건너뛰고 즉시 위치을 설정합니다. if (distanceToTarget > teleportDistanceThreshold) { transform.position = newTargetPosition; velocity = Vector3.zero; // 순간이동 후 속도 초기화 } // 새로운 목표 지점을 설정합니다. // (순간이동을 했든 안 했든, 다음 프레임부터의 보간을 위해 목표 지점은 항상 갱신되어야 합니다.) this.targetPosition = newTargetPosition; changed = true; } } float? newDegree = newData.GetFloat("DEGREE"); if (newDegree.HasValue && data.GetFloat("DEGREE").Value != newDegree.Value) { Quaternion newTargetRotation = Quaternion.Euler(0, newDegree.Value, 0); float distanceToTargetRotation = Quaternion.Angle(transform.rotation, newTargetRotation); // 현재 회전과 새로운 목표 회전 사이의 각도 차이를 계산합니다. if (distanceToTargetRotation > 0) { // 각도 차이가 설정된 임계값을 초과하면, 보간을 건너뛰고 즉시 회전을 설정합니다. if (distanceToTargetRotation > teleportRotationThreshold) { transform.rotation = newTargetRotation; angularVelocity = 0f; } } // 새로운 목표 지점을 설정합니다. // (순간이동을 했든 안 했든, 다음 프레임부터의 보간을 위해 목표 지점은 항상 갱신되어야 합니다.) this.targetRotation = newTargetRotation; changed = true; } if (changed) { ChangeColor(Color.red); } else { ChangeColor(Color.white); } } } private void ChangeColor(Color color) { if(color == Color.red && isRed && renderer != null) return; // 이미 빨간색이면 변경하지 않음 isRed = color == Color.red; if(renderer != null) renderer!.material.color = color; } /// /// Unity에 의해 매 프레임마다 호출되는 메서드입니다. /// AGV의 현재 위치/회전을 목표 위치/회전으로 부드럽게 이동시키는 시각적 처리를 담당합니다. /// void Update() { bool isMoving = Vector3.SqrMagnitude(transform.position - targetPosition) > 0.001f; bool isRotating = Quaternion.Angle(transform.rotation, targetRotation) > 0.01f; // 이동과 회전이 모두 끝났으면 더 이상 계산할 필요가 없습니다. if (!isMoving && !isRotating) { return; } float dampedTime = Time.deltaTime * timeScale; if (dampedTime <= 0) return; // 위치 업데이트 if (isMoving) { transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, moveSpeed, Mathf.Infinity, dampedTime); } else { transform.position = targetPosition; velocity = Vector3.zero; } // 회전 업데이트 if (isRotating) { float currentAngle = transform.eulerAngles.y; float targetAngle = targetRotation.eulerAngles.y; float newAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angularVelocity, rotationSpeed, Mathf.Infinity, dampedTime); transform.rotation = Quaternion.Euler(0, newAngle, 0); } else { transform.rotation = targetRotation; angularVelocity = 0f; } } } }