2026.06.24
This commit is contained in:
330
Assets/My project/maze Scripts/Ui/MazeMixedUIPanelFollower.cs
Normal file
330
Assets/My project/maze Scripts/Ui/MazeMixedUIPanelFollower.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user