Files
WhaleAdventure_VR/Assets/My project/maze Scripts/Ui/MazeMixedUIPanelFollower.cs
2026-06-24 17:13:40 +09:00

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
}