187 lines
8.8 KiB
C#
187 lines
8.8 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|