Project_WL/Assets/WL/Scripts/UI/VirtualPadView.cs

187 lines
8.8 KiB
C#
Raw Normal View History

using UnityEngine;
using UnityEngine.UI;
using WL.Player;
namespace WL.UI
{
/// <summary>
/// 가상패드(플로팅 조이스틱) 표시.
///
/// **PD #706 ②** — `PlayerTouchSettings.floatingPadEverywhere = true`(기본)이면
/// 화면 어디를 눌러도 **누른 그 지점**에 패드가 나타나고, 손을 떼면 **즉시 사라진다**.
/// 토글을 끄면 구 동작(좌하단 고정 영역 + 대기 힌트 상시 표시)으로 돌아간다.
///
/// 입력 판정은 이미 있는 `TouchInputProvider`(패드 영역·데드존·정규화)가 전담하고,
/// 이 컴포넌트는 **그 상태를 그림으로 보여주기만** 한다(로직 이중화 금지).
///
/// 힘민지 참조: `Assets/Script/UI/JoyStick/UIJoystick.cs` 는 참조 0건의 사장 코드라
/// 코드는 가져오지 않고 **규약만** 따랐다 —
/// · 핸들 이동 반경 클램프 `Vector2.ClampMagnitude(v, radius)` (UIJoystick.cs:225)
/// · 데드존 이내는 0 처리 (UIJoystick.cs:220~222)
/// · 뗄 때 중심 복귀
/// 스프라이트는 힘민지 `Assets/ThirdParty/Joystick Pack/Sprites` 에서 복사해 재활용한다.
/// </summary>
public class VirtualPadView : MonoBehaviour
{
[Header("참조")]
[Tooltip("비우면 씬에서 활성 PlayerController 를 찾는다")]
[SerializeField] private PlayerController player;
[SerializeField] private RectTransform background;
[SerializeField] private RectTransform handle;
[Tooltip("비우면 부모 Canvas 를 자동으로 찾는다")]
[SerializeField] private Canvas canvas;
[Header("표시 옵션 (임시값 · 직렬화 노출)")]
[Tooltip("누르지 않은 동안에도 패드 영역 중앙에 흐리게 표시한다")]
[SerializeField] private bool showIdleHint = true;
[Tooltip("대기 상태 투명도 · 임시 0.25")]
[Range(0f, 1f)][SerializeField] private float idleAlpha = 0.25f;
[Tooltip("누른 상태 투명도 · 임시 0.85")]
[Range(0f, 1f)][SerializeField] private float activeAlpha = 0.85f;
[Tooltip("투명도 전환 속도(초당) · 임시 8")]
[SerializeField] private float alphaLerpSpeed = 8f;
[Tooltip("배경 지름 = 패드 반경 × 이 배수 · 임시 2.0")]
[SerializeField] private float backgroundDiameterScale = 2.0f;
[Tooltip("핸들 지름 = 패드 반경 × 이 배수 · 임시 0.9")]
[SerializeField] private float handleDiameterScale = 0.9f;
private RectTransform _canvasRect;
private RectTransform _selfRect; // 좌표 변환 기준 — SafeArea 안에 놓여도 어긋나지 않게 자기 rect 를 쓴다
private CanvasGroup _group;
private float _alpha;
private bool _sizedOnce;
private void Awake()
{
if (canvas == null) canvas = GetComponentInParent<Canvas>();
_canvasRect = canvas != null ? canvas.GetComponent<RectTransform>() : null;
_selfRect = GetComponent<RectTransform>();
_group = GetComponent<CanvasGroup>();
if (_group == null) _group = gameObject.AddComponent<CanvasGroup>();
// 패드 그림이 터치를 가로채면 이동 입력이 UI 로 먹힌다(TouchInputProvider.IsBlockedByUI)
_group.blocksRaycasts = false;
_group.interactable = false;
_alpha = 0f;
_group.alpha = 0f;
}
private void Start() { ResolvePlayer(); }
private void ResolvePlayer()
{
if (player == null) player = Object.FindFirstObjectByType<PlayerController>();
}
/// <summary>패드가 읽을 플레이어를 바꾼다 (PD #711 캐릭터 교체).</summary>
public void RebindPlayer(PlayerController pc)
{
player = pc;
if (player == null) ResolvePlayer();
}
private void LateUpdate()
{
if (player == null) { ResolvePlayer(); if (player == null) { Fade(0f); return; } }
var touch = player.Touch;
var settings = player.TouchSettings;
if (touch == null || settings == null || _selfRect == null || background == null || handle == null) { Fade(0f); return; }
float w = Screen.width, h = Screen.height;
float radiusPx = settings.GetPadRadiusPixels(w, h);
ApplySize(radiusPx);
bool padActive = touch.IsPressed && !touch.IsBlockedByUI
&& touch.CurrentMode == TouchInputProvider.Mode.VirtualPad;
if (padActive)
{
Vector2 center = ToCanvas(touch.StartPosition);
background.anchoredPosition = center;
// 핸들 = 드래그 벡터를 반경으로 클램프 (힘민지 UIJoystick.cs:225 규약)
Vector2 dragPx = touch.CurrentPosition - touch.StartPosition;
Vector2 dragClamped = Vector2.ClampMagnitude(dragPx, radiusPx);
Vector2 handlePos = ToCanvas(touch.StartPosition + dragClamped) - center;
handle.anchoredPosition = handlePos;
Fade(activeAlpha);
}
else
{
// PD #706 ② — 플로팅 모드에서는 손을 떼면 **즉시 사라진다**(고정 위치 대기 힌트 없음).
// 토글을 끄면 구 동작(패드 영역 중앙에 흐리게 상시 표시)으로 되돌아간다.
if (settings.floatingPadEverywhere)
{
handle.anchoredPosition = Vector2.zero; // 뗄 때 중심 복귀
SnapAlpha(0f); // PD #707 ① — 해제 시 '즉시' 숨김(페이드 대기 없음)
}
else
{
Rect pad = settings.GetPadRectPixels(w, h);
background.anchoredPosition = ToCanvas(pad.center);
handle.anchoredPosition = Vector2.zero;
Fade(showIdleHint ? idleAlpha : 0f);
}
}
}
// ── 검증용 노출 (PD #706 실측 로그)
/// <summary>패드 그림이 실제로 화면에 보이는가(알파 기준).</summary>
public bool IsPadVisible { get { return _alpha > 0.01f; } }
/// <summary>현재 알파값.</summary>
public float CurrentAlpha { get { return _alpha; } }
/// <summary>패드 배경이 놓인 캔버스 좌표.</summary>
public Vector2 BackgroundAnchoredPosition { get { return background != null ? background.anchoredPosition : Vector2.zero; } }
/// <summary>패드 배경의 화면(픽셀) 좌표. 터치 지점과 바로 비교하기 위한 역변환.</summary>
public Vector2 BackgroundScreenPosition
{
get
{
if (background == null) return Vector2.zero;
var cam = canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay ? canvas.worldCamera : null;
return RectTransformUtility.WorldToScreenPoint(cam, background.position);
}
}
private void ApplySize(float radiusPx)
{
if (_sizedOnce || _canvasRect == null || background == null || handle == null) return;
float scale = ScreenToCanvasScale();
float r = radiusPx * scale;
background.sizeDelta = Vector2.one * (r * backgroundDiameterScale);
handle.sizeDelta = Vector2.one * (r * handleDiameterScale);
_sizedOnce = true;
}
/// <summary>화면 픽셀 → 캔버스 단위 배율 (ScreenSpaceOverlay + CanvasScaler 기준)</summary>
private float ScreenToCanvasScale()
{
if (_canvasRect == null || Screen.width <= 0) return 1f;
return _canvasRect.sizeDelta.x / Screen.width;
}
/// <summary>화면 좌표 → 이 오브젝트 rect 기준 로컬 앵커 좌표 (SafeArea 인셋이 걸려도 정확)</summary>
private Vector2 ToCanvas(Vector2 screenPos)
{
Vector2 local;
var cam = canvas != null && canvas.renderMode != RenderMode.ScreenSpaceOverlay ? canvas.worldCamera : null;
RectTransformUtility.ScreenPointToLocalPointInRectangle(_selfRect, screenPos, cam, out local);
return local;
}
private void Fade(float target)
{
_alpha = Mathf.MoveTowards(_alpha, target, alphaLerpSpeed * Time.unscaledDeltaTime);
if (_group != null) _group.alpha = _alpha;
}
/// <summary>페이드 없이 즉시 알파를 맞춘다(PD #707 ① — 손을 떼면 그 프레임에 사라져야 한다).</summary>
private void SnapAlpha(float target)
{
_alpha = target;
if (_group != null) _group.alpha = _alpha;
}
}
}