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

187 lines
8.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}