370 lines
16 KiB
C#
370 lines
16 KiB
C#
#nullable enable
|
|
using EPOOutline;
|
|
using System;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UVC.Data;
|
|
using UVC.Data.Core;
|
|
using UVC.Factory.Playback;
|
|
|
|
namespace UVC.Factory.Component
|
|
{
|
|
/// <summary>
|
|
/// 씬에 표시되는 개별 AGV(무인 운반차)를 제어하는 클래스입니다.
|
|
/// FactoryObject를 상속받아, AGV의 데이터 처리, 3D 모델의 이동 및 회전, 정보 표시 기능을 구현합니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 클래스는 AGVManager에 의해 동적으로 생성되고 관리됩니다.
|
|
/// AGVManager로부터 실시간 데이터를 받아 ProcessData 메서드에서 처리하고,
|
|
/// Unity의 Update 메서드에서 매 프레임마다 부드러운 시각적 이동을 구현합니다.
|
|
/// </remarks>
|
|
public class AGV : FactoryObject
|
|
{
|
|
// 서버에서 받은 좌표(예: 밀리미터 단위)를 Unity 씬의 단위(미터)로 변환하기 위한 스케일 값입니다.
|
|
// 예를 들어, 서버 좌표 1000이 Unity에서 1미터가 되려면 0.001f로 설정합니다.
|
|
private float scaleFactor = 0.001f; // 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? modelRenderer;
|
|
|
|
private bool isRed = false;
|
|
|
|
private float timeScale = 1.0f; // 현재 시간 스케일, 기본값은 1.0f (실시간)
|
|
|
|
// SmoothDamp 함수가 사용하는 현재 속도 값입니다. 0으로 초기화해야 합니다.
|
|
private Vector3 velocity = Vector3.zero;
|
|
private float angularVelocity = 0;
|
|
|
|
private Outlinable? outlinable;
|
|
|
|
private float agvFloorOffset = 200f; //agv의 층을 구별하기 위한 값
|
|
private float agvZOffset = -197.5f; //층을 옮긴 agv의 위치를 잡아주기 위한 값
|
|
|
|
/// <summary>
|
|
/// AGV 객체가 생성될 때 처음 한 번 호출되는 초기화 메서드입니다.
|
|
/// </summary>
|
|
private void Start()
|
|
{
|
|
if (modelObject != null)
|
|
{
|
|
modelRenderer = modelObject.GetComponent<Renderer>();
|
|
outlinable = modelObject.GetComponent<Outlinable>();
|
|
if (outlinable != null) outlinable.enabled = false;
|
|
}
|
|
|
|
// 시작 시에는 현재 위치를 목표 위치로 설정하여 의도치 않은 움직임을 방지합니다.
|
|
targetPosition = transform.position;
|
|
targetRotation = transform.rotation;
|
|
|
|
PlaybackService.Instance.OnChangeTimeScale += OnChangeTimeScaleHandler;
|
|
}
|
|
|
|
public override void OnPointerClick(PointerEventData eventData)
|
|
{
|
|
// 사용자가 AGV를 클릭했을 때 정보창에 표시될 데이터 항목과 순서를 정의합니다.
|
|
if (dataDisplaySetting == null) dataDisplaySetting = UserSetting.Get("AGV");
|
|
base.OnPointerClick(eventData);
|
|
}
|
|
|
|
private void OnChangeTimeScaleHandler(float timeScale)
|
|
{
|
|
this.timeScale = timeScale;
|
|
}
|
|
|
|
protected override void OnDestroy()
|
|
{
|
|
PlaybackService.Instance.OnChangeTimeScale -= OnChangeTimeScaleHandler;
|
|
base.OnDestroy();
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// 선택 된 효과로 외곽선을 표시합니다.
|
|
/// </summary>
|
|
public override void ShowOutLine()
|
|
{
|
|
if (outlinable != null) outlinable.enabled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 외곽선을 숨깁니다.
|
|
/// </summary>
|
|
public override void HideOutLine()
|
|
{
|
|
if (outlinable != null) outlinable.enabled = false;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// AGVManager로부터 새로운 데이터를 받았을 때 호출되는 핵심 메서드입니다.
|
|
/// 받은 데이터를 기반으로 AGV의 내부 상태와 목표 위치를 갱신합니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 메서드는 FactoryObject의 추상 메서드를 재정의한 것입니다.
|
|
/// AGV의 위치, 회전, 상태 등 모든 동적인 정보는 이 메서드를 통해 업데이트됩니다.
|
|
/// </remarks>
|
|
/// <param name="newData">AGV의 최신 정보가 담긴 데이터 객체입니다.</param>
|
|
protected override void ProcessData(DataObject newData)
|
|
{
|
|
// 새 데이터로 위치와 회전을 즉시 설정합니다.
|
|
UpdatePositionAndRotation(newData);
|
|
if (newData.IsUpdateImmediately)
|
|
{
|
|
transform.position = targetPosition;
|
|
transform.rotation = targetRotation;
|
|
}
|
|
|
|
// 처음 데이터를 받는 경우 (data가 null일 때)
|
|
if (data == null)
|
|
{
|
|
// 받은 데이터를 내부 데이터 저장소에 저장합니다.
|
|
data = newData;
|
|
}
|
|
else // 이미 데이터가 있는 경우 (업데이트)
|
|
{
|
|
// 기존 데이터(data)에 새로운 데이터(newData)의 내용을 덮어씁니다.
|
|
foreach (var keyValue in newData)
|
|
{
|
|
data[keyValue.Key] = keyValue.Value;
|
|
}
|
|
}
|
|
|
|
UpdateStats();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 데이터 객체로부터 위치(X, Y) 및 각도(DEGREE) 값을 읽어와 AGV의 목표 위치와 회전을 설정합니다.
|
|
/// 이전 데이터의 timestamp를 이용해 업데이트 간격을 계산하고, 속도와 회전 속도를 조절합니다.
|
|
/// </summary>
|
|
/// <param name="newData">위치와 각도 정보가 포함된 데이터 객체입니다.</param>
|
|
private void UpdatePositionAndRotation(DataObject newData)
|
|
{
|
|
// 처음 데이터를 받는 경우, 받은 데이터로 즉시 위치를 설정합니다.
|
|
if (data == null)
|
|
{
|
|
float? xVal = newData.GetFloat("X");
|
|
float? yVal = newData.GetFloat("Y");
|
|
float? degreeVal = newData.GetFloat("DEGREE");
|
|
|
|
if (xVal.HasValue && yVal.HasValue && degreeVal.HasValue)
|
|
{
|
|
float x = xVal.Value * scaleFactor;
|
|
float y = yVal.Value * scaleFactor;
|
|
if (y > 200) y -= 102.5f;
|
|
Quaternion rotation = Quaternion.Euler(0, degreeVal.Value, 0);
|
|
transform.position = targetPosition = new Vector3(x, 0, y);
|
|
transform.rotation = targetRotation = rotation;
|
|
}
|
|
}
|
|
else // 이후 업데이트의 경우
|
|
{
|
|
// 타임스탬프를 이용해 데이터 업데이트 간격을 계산합니다.
|
|
DateTime? newTimestamp = newData.GetDateTime("TIMESTAMP");
|
|
DateTime? currentTimestamp = data.GetDateTime("TIMESTAMP");
|
|
float updateInterval = 1.0f; // 기본 간격은 1초로 설정
|
|
|
|
if (newTimestamp.HasValue && currentTimestamp.HasValue)
|
|
{
|
|
// 두 타임스탬프의 차이를 초 단위로 계산합니다.
|
|
TimeSpan interval = newTimestamp.Value - currentTimestamp.Value;
|
|
updateInterval = (float)interval.TotalSeconds;
|
|
|
|
// 간격이 0 이하일 경우(오류 방지), 기본값 1초를 사용합니다.
|
|
if (updateInterval <= 0)
|
|
{
|
|
updateInterval = 1.0f;
|
|
}
|
|
}
|
|
|
|
bool changed = false;
|
|
|
|
float? newX = newData.GetFloat("X");
|
|
float? newY = newData.GetFloat("Y");
|
|
float? currentX = data.GetFloat("X");
|
|
float? currentY = data.GetFloat("Y");
|
|
if (currentX.HasValue && currentY.HasValue)
|
|
{
|
|
float x = currentX.Value;
|
|
float y = currentY.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)
|
|
{
|
|
float newY2 = newY.Value * scaleFactor;
|
|
if (newY2 > 200) newY2 -= 102.5f; // y 좌표가 200을 초과하면 102.5를 빼줍니다.
|
|
newTargetPosition.z = newY2;
|
|
}
|
|
// 현재 위치와 새로운 목표 위치 사이의 거리를 계산합니다.
|
|
float distanceToTarget = Vector3.Distance(transform.position, newTargetPosition);
|
|
if (distanceToTarget > 0)
|
|
{
|
|
// 이동 속도(moveSpeed)를 계산된 시간 간격을 기반으로 설정합니다.
|
|
moveSpeed = updateInterval * 0.9f; // 90%의 시간 동안 도달하도록 설정
|
|
|
|
// 거리가 설정된 임계값을 초과하면, 보간을 건너뛰고 즉시 위치을 설정합니다.
|
|
if (distanceToTarget > teleportDistanceThreshold)
|
|
{
|
|
transform.position = newTargetPosition;
|
|
velocity = Vector3.zero; // 순간이동 후 속도 초기화
|
|
}
|
|
|
|
// 새로운 목표 지점을 설정합니다.
|
|
// (순간이동을 했든 안 했든, 다음 프레임부터의 보간을 위해 목표 지점은 항상 갱신되어야 합니다.)
|
|
this.targetPosition = newTargetPosition;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
float? newDegree = newData.GetFloat("DEGREE");
|
|
float? currentDegree = data.GetFloat("DEGREE");
|
|
|
|
if (newDegree.HasValue && currentDegree.HasValue && currentDegree.Value != newDegree.Value)
|
|
{
|
|
Quaternion newTargetRotation = Quaternion.Euler(0, newDegree.Value, 0);
|
|
|
|
float distanceToTargetRotation = Quaternion.Angle(transform.rotation, newTargetRotation);
|
|
|
|
// 현재 회전과 새로운 목표 회전 사이의 각도 차이를 계산합니다.
|
|
if (distanceToTargetRotation > 0)
|
|
{
|
|
// 회전 속도(rotationSpeed)를 계산된 시간 간격을 기반으로 설정합니다.
|
|
rotationSpeed = updateInterval * 0.5f; // 50%의 시간 동안 회전하도록 설정
|
|
|
|
// 각도 차이가 설정된 임계값을 초과하면, 보간을 건너뛰고 즉시 회전을 설정합니다.
|
|
if (distanceToTargetRotation > teleportRotationThreshold)
|
|
{
|
|
transform.rotation = newTargetRotation;
|
|
angularVelocity = 0f;
|
|
}
|
|
}
|
|
|
|
// 새로운 목표 지점을 설정합니다.
|
|
// (순간이동을 했든 안 했든, 다음 프레임부터의 보간을 위해 목표 지점은 항상 갱신되어야 합니다.)
|
|
this.targetRotation = newTargetRotation;
|
|
changed = true;
|
|
}
|
|
|
|
//if (changed)
|
|
//{
|
|
// ChangeColor(Color.red);
|
|
//}
|
|
//else
|
|
//{
|
|
// ChangeColor(Color.white);
|
|
//}
|
|
}
|
|
}
|
|
|
|
private void UpdateStats()
|
|
{
|
|
if (data == null) return;
|
|
string? stats = data.GetString("VHL_STATE");
|
|
if (stats == null) return;
|
|
if (stats == "91" || stats == "92" || stats == "93" || stats == "94")//CHARGING
|
|
{
|
|
ChangeColor(new Color(0.7f, 0.12f, 0.0f, 1.0f));//orange
|
|
}
|
|
else if (stats == "200")//EMERGENCY
|
|
{
|
|
ChangeColor(Color.red);
|
|
}
|
|
else
|
|
{
|
|
string? trayID = data.GetString("CARRIER_ID");
|
|
if (string.IsNullOrEmpty(trayID))
|
|
{
|
|
ChangeColor(Color.yellow); // 트레이 ID가 없으면 기본 색상으로 설정
|
|
}
|
|
else
|
|
{
|
|
ChangeColor(Color.green); // 트레이 ID가 있으면 녹색으로 설정
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ChangeColor(Color color)
|
|
{
|
|
if (color == Color.red && isRed && modelRenderer != null) return; // 이미 빨간색이면 변경하지 않음
|
|
isRed = color == Color.red;
|
|
if (modelRenderer != null) modelRenderer!.material.color = color;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unity에 의해 매 프레임마다 호출되는 메서드입니다.
|
|
/// AGV의 현재 위치/회전을 목표 위치/회전으로 부드럽게 이동시키는 시각적 처리를 담당합니다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
public override void UnregisterFactoryObject()
|
|
{
|
|
base.UnregisterFactoryObject();
|
|
}
|
|
}
|
|
}
|