331 lines
12 KiB
C#
331 lines
12 KiB
C#
using UnityEngine;
|
|
|
|
[DisallowMultipleComponent]
|
|
public class MazeMixedUIPanelFollower : MonoBehaviour
|
|
{
|
|
public enum PanelFollowMode
|
|
{
|
|
None,
|
|
Wrist,
|
|
CameraFront,
|
|
CameraFrontOnShow
|
|
}
|
|
|
|
[Header("Canvas")]
|
|
[Tooltip("비워두면 이 오브젝트 또는 부모에서 Canvas를 자동으로 찾습니다. Render Mode는 World Space여야 합니다.")]
|
|
[SerializeField] private Canvas targetCanvas;
|
|
|
|
[Header("Follow Targets")]
|
|
[Tooltip("손목 HUD가 따라갈 Transform입니다. 컨트롤러 자체보다 WristUIAnchor 빈 오브젝트를 넣는 것을 추천합니다.")]
|
|
[SerializeField] private Transform wristTarget;
|
|
|
|
[Tooltip("Guide/Result가 기준으로 삼을 카메라 Transform입니다. XR Origin 안의 Main Camera를 넣으세요.")]
|
|
[SerializeField] private Transform cameraTarget;
|
|
|
|
[Tooltip("Camera Target이 비어 있으면 Camera.main을 자동으로 찾습니다.")]
|
|
[SerializeField] private bool autoFindMainCameraIfMissing = true;
|
|
|
|
[Header("Panels In One Canvas")]
|
|
[SerializeField] private RectTransform hudPanel;
|
|
[SerializeField] private RectTransform guidePanel;
|
|
[SerializeField] private RectTransform resultPanel;
|
|
|
|
[Header("Panel Follow Mode")]
|
|
[SerializeField] private PanelFollowMode hudFollowMode = PanelFollowMode.Wrist;
|
|
[Tooltip("CameraFrontOnShow: 켜지는 순간 카메라 앞에 배치 후 고정됩니다.")]
|
|
[SerializeField] private PanelFollowMode guideFollowMode = PanelFollowMode.CameraFrontOnShow;
|
|
[Tooltip("CameraFrontOnShow: 켜지는 순간 카메라 앞에 배치 후 고정됩니다.")]
|
|
[SerializeField] private PanelFollowMode resultFollowMode = PanelFollowMode.CameraFrontOnShow;
|
|
|
|
[Header("HUD Wrist Placement")]
|
|
[Tooltip("손목 기준 위치 오프셋입니다. WristUIAnchor를 쓰면 0,0,0 권장")]
|
|
[SerializeField] private Vector3 hudWristPositionOffset = Vector3.zero;
|
|
|
|
[Tooltip("손목 기준 회전 오프셋입니다. WristUIAnchor를 쓰면 0,0,0 권장")]
|
|
[SerializeField] private Vector3 hudWristRotationOffset = Vector3.zero;
|
|
|
|
[Header("Guide Camera Placement")]
|
|
[SerializeField] private float guideDistance = 1.6f;
|
|
[SerializeField] private float guideHeightOffset = -0.12f;
|
|
[SerializeField] private Vector3 guideRotationOffset = Vector3.zero;
|
|
|
|
[Header("Result Camera Placement")]
|
|
[SerializeField] private float resultDistance = 1.7f;
|
|
[SerializeField] private float resultHeightOffset = -0.05f;
|
|
[SerializeField] private Vector3 resultRotationOffset = Vector3.zero;
|
|
|
|
[Header("Billboard")]
|
|
[Tooltip("Guide/Result가 표시될 때 카메라를 바라보게 합니다. UI가 뒤집혀 보이면 Rotation Offset Y=180을 시도하세요.")]
|
|
[SerializeField] private bool frontPanelsLookAtCamera = true;
|
|
|
|
[Header("Smooth Follow")]
|
|
[SerializeField] private bool useSmoothFollow = true;
|
|
[SerializeField] private float positionSmooth = 14f;
|
|
[SerializeField] private float rotationSmooth = 14f;
|
|
|
|
[Header("Scale Preset")]
|
|
[Tooltip("체크하면 시작 시 패널별 월드 스케일을 적용합니다. 이미 크기를 맞췄다면 끄세요.")]
|
|
[SerializeField] private bool applyScaleOnStart = false;
|
|
[SerializeField] private Vector3 hudWorldScale = new Vector3(0.0016f, 0.0016f, 0.0016f);
|
|
[SerializeField] private Vector3 guideWorldScale = new Vector3(0.0024f, 0.0024f, 0.0024f);
|
|
[SerializeField] private Vector3 resultWorldScale = new Vector3(0.0026f, 0.0026f, 0.0026f);
|
|
|
|
private bool hudWasActive;
|
|
private bool guideWasActive;
|
|
private bool resultWasActive;
|
|
|
|
private bool hudPlacedOnce;
|
|
private bool guidePlacedOnce;
|
|
private bool resultPlacedOnce;
|
|
|
|
private enum PanelKind
|
|
{
|
|
HUD,
|
|
Guide,
|
|
Result
|
|
}
|
|
|
|
private void Reset()
|
|
{
|
|
targetCanvas = GetComponent<Canvas>();
|
|
|
|
if (targetCanvas == null)
|
|
targetCanvas = GetComponentInParent<Canvas>();
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
ResolveReferences();
|
|
|
|
if (targetCanvas != null && targetCanvas.renderMode != RenderMode.WorldSpace)
|
|
{
|
|
Debug.LogWarning(
|
|
"MazeMixedUIPanelFollower: Canvas Render Mode는 World Space여야 손목/카메라 앞 위치 제어가 자연스럽습니다.",
|
|
targetCanvas
|
|
);
|
|
}
|
|
|
|
if (applyScaleOnStart)
|
|
ApplyScalePreset();
|
|
|
|
CacheActiveStates();
|
|
}
|
|
|
|
private void ResolveReferences()
|
|
{
|
|
if (targetCanvas == null)
|
|
targetCanvas = GetComponent<Canvas>();
|
|
|
|
if (targetCanvas == null)
|
|
targetCanvas = GetComponentInParent<Canvas>();
|
|
|
|
if (autoFindMainCameraIfMissing && cameraTarget == null && Camera.main != null)
|
|
cameraTarget = Camera.main.transform;
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
ResolveReferences();
|
|
|
|
FollowPanel(hudPanel, hudFollowMode, PanelKind.HUD, ref hudWasActive, ref hudPlacedOnce);
|
|
FollowPanel(guidePanel, guideFollowMode, PanelKind.Guide, ref guideWasActive, ref guidePlacedOnce);
|
|
FollowPanel(resultPanel, resultFollowMode, PanelKind.Result, ref resultWasActive, ref resultPlacedOnce);
|
|
}
|
|
|
|
private void CacheActiveStates()
|
|
{
|
|
hudWasActive = hudPanel != null && hudPanel.gameObject.activeInHierarchy;
|
|
guideWasActive = guidePanel != null && guidePanel.gameObject.activeInHierarchy;
|
|
resultWasActive = resultPanel != null && resultPanel.gameObject.activeInHierarchy;
|
|
}
|
|
|
|
private void FollowPanel(RectTransform panel, PanelFollowMode mode, PanelKind kind, ref bool wasActive, ref bool placedOnce)
|
|
{
|
|
if (panel == null)
|
|
return;
|
|
|
|
bool isActive = panel.gameObject.activeInHierarchy;
|
|
|
|
if (!isActive)
|
|
{
|
|
wasActive = false;
|
|
placedOnce = false;
|
|
return;
|
|
}
|
|
|
|
if (mode == PanelFollowMode.None)
|
|
{
|
|
wasActive = isActive;
|
|
return;
|
|
}
|
|
|
|
bool justActivated = !wasActive && isActive;
|
|
|
|
Vector3 targetPosition;
|
|
Quaternion targetRotation;
|
|
|
|
if (mode == PanelFollowMode.Wrist)
|
|
{
|
|
if (wristTarget == null)
|
|
return;
|
|
|
|
GetWristPose(out targetPosition, out targetRotation);
|
|
ApplyTransform(panel, targetPosition, targetRotation, useSmoothFollow);
|
|
}
|
|
else
|
|
{
|
|
if (cameraTarget == null)
|
|
return;
|
|
|
|
GetCameraFrontPose(kind, out targetPosition, out targetRotation);
|
|
|
|
if (mode == PanelFollowMode.CameraFront)
|
|
{
|
|
ApplyTransform(panel, targetPosition, targetRotation, useSmoothFollow);
|
|
}
|
|
else if (mode == PanelFollowMode.CameraFrontOnShow)
|
|
{
|
|
if (justActivated || !placedOnce)
|
|
{
|
|
panel.SetPositionAndRotation(targetPosition, targetRotation);
|
|
placedOnce = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
wasActive = isActive;
|
|
}
|
|
|
|
private void GetWristPose(out Vector3 targetPosition, out Quaternion targetRotation)
|
|
{
|
|
targetPosition =
|
|
wristTarget.position +
|
|
wristTarget.right * hudWristPositionOffset.x +
|
|
wristTarget.up * hudWristPositionOffset.y +
|
|
wristTarget.forward * hudWristPositionOffset.z;
|
|
|
|
targetRotation = wristTarget.rotation * Quaternion.Euler(hudWristRotationOffset);
|
|
}
|
|
|
|
private void GetCameraFrontPose(PanelKind kind, out Vector3 targetPosition, out Quaternion targetRotation)
|
|
{
|
|
float distance = kind == PanelKind.Result ? resultDistance : guideDistance;
|
|
float heightOffset = kind == PanelKind.Result ? resultHeightOffset : guideHeightOffset;
|
|
Vector3 rotationOffset = kind == PanelKind.Result ? resultRotationOffset : guideRotationOffset;
|
|
|
|
Vector3 forward = cameraTarget.forward;
|
|
|
|
if (forward.sqrMagnitude < 0.001f)
|
|
forward = Vector3.forward;
|
|
|
|
targetPosition = cameraTarget.position + forward.normalized * distance + Vector3.up * heightOffset;
|
|
|
|
if (frontPanelsLookAtCamera)
|
|
{
|
|
Vector3 lookDirection = targetPosition - cameraTarget.position;
|
|
|
|
if (lookDirection.sqrMagnitude < 0.001f)
|
|
lookDirection = cameraTarget.forward;
|
|
|
|
targetRotation = Quaternion.LookRotation(lookDirection.normalized, Vector3.up);
|
|
}
|
|
else
|
|
{
|
|
targetRotation = cameraTarget.rotation;
|
|
}
|
|
|
|
targetRotation *= Quaternion.Euler(rotationOffset);
|
|
}
|
|
|
|
private void ApplyTransform(RectTransform panel, Vector3 targetPosition, Quaternion targetRotation, bool smooth)
|
|
{
|
|
if (smooth)
|
|
{
|
|
panel.position = Vector3.Lerp(panel.position, targetPosition, Time.deltaTime * positionSmooth);
|
|
panel.rotation = Quaternion.Slerp(panel.rotation, targetRotation, Time.deltaTime * rotationSmooth);
|
|
}
|
|
else
|
|
{
|
|
panel.SetPositionAndRotation(targetPosition, targetRotation);
|
|
}
|
|
}
|
|
|
|
private void ApplyScalePreset()
|
|
{
|
|
if (hudPanel != null)
|
|
hudPanel.localScale = hudWorldScale;
|
|
|
|
if (guidePanel != null)
|
|
guidePanel.localScale = guideWorldScale;
|
|
|
|
if (resultPanel != null)
|
|
resultPanel.localScale = resultWorldScale;
|
|
}
|
|
|
|
public void SetWristTarget(Transform target)
|
|
{
|
|
wristTarget = target;
|
|
}
|
|
|
|
public void SetCameraTarget(Transform target)
|
|
{
|
|
cameraTarget = target;
|
|
}
|
|
|
|
public void SnapPanelsNow()
|
|
{
|
|
bool previousSmooth = useSmoothFollow;
|
|
useSmoothFollow = false;
|
|
|
|
FollowPanel(hudPanel, hudFollowMode, PanelKind.HUD, ref hudWasActive, ref hudPlacedOnce);
|
|
FollowPanel(guidePanel, guideFollowMode, PanelKind.Guide, ref guideWasActive, ref guidePlacedOnce);
|
|
FollowPanel(resultPanel, resultFollowMode, PanelKind.Result, ref resultWasActive, ref resultPlacedOnce);
|
|
|
|
useSmoothFollow = previousSmooth;
|
|
}
|
|
|
|
public void PlaceGuideInFrontNow()
|
|
{
|
|
ResolveReferences();
|
|
|
|
if (guidePanel == null || cameraTarget == null)
|
|
return;
|
|
|
|
GetCameraFrontPose(PanelKind.Guide, out Vector3 position, out Quaternion rotation);
|
|
guidePanel.SetPositionAndRotation(position, rotation);
|
|
guidePlacedOnce = true;
|
|
}
|
|
|
|
public void PlaceResultInFrontNow()
|
|
{
|
|
ResolveReferences();
|
|
|
|
if (resultPanel == null || cameraTarget == null)
|
|
return;
|
|
|
|
GetCameraFrontPose(PanelKind.Result, out Vector3 position, out Quaternion rotation);
|
|
resultPanel.SetPositionAndRotation(position, rotation);
|
|
resultPlacedOnce = true;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnValidate()
|
|
{
|
|
guideDistance = Mathf.Max(0.1f, guideDistance);
|
|
resultDistance = Mathf.Max(0.1f, resultDistance);
|
|
positionSmooth = Mathf.Max(0.01f, positionSmooth);
|
|
rotationSmooth = Mathf.Max(0.01f, rotationSmooth);
|
|
|
|
if (hudFollowMode == PanelFollowMode.Wrist && wristTarget == null)
|
|
Debug.LogWarning("MazeMixedUIPanelFollower: HUD가 Wrist 모드인데 Wrist Target이 비어 있습니다. WristUIAnchor를 연결하세요.", this);
|
|
|
|
if ((guideFollowMode == PanelFollowMode.CameraFront || guideFollowMode == PanelFollowMode.CameraFrontOnShow ||
|
|
resultFollowMode == PanelFollowMode.CameraFront || resultFollowMode == PanelFollowMode.CameraFrontOnShow) &&
|
|
cameraTarget == null && !autoFindMainCameraIfMissing)
|
|
{
|
|
Debug.LogWarning("MazeMixedUIPanelFollower: CameraFront 계열 모드를 쓰려면 Camera Target을 연결하세요.", this);
|
|
}
|
|
}
|
|
#endif
|
|
}
|