using UnityEngine; using UnityEngine.UI; using WL.Player; namespace WL.UI { /// /// 가상패드(플로팅 조이스틱) 표시. /// /// **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` 에서 복사해 재활용한다. /// 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(); _canvasRect = canvas != null ? canvas.GetComponent() : null; _selfRect = GetComponent(); _group = GetComponent(); if (_group == null) _group = gameObject.AddComponent(); // 패드 그림이 터치를 가로채면 이동 입력이 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(); } /// 패드가 읽을 플레이어를 바꾼다 (PD #711 캐릭터 교체). 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 실측 로그) /// 패드 그림이 실제로 화면에 보이는가(알파 기준). public bool IsPadVisible { get { return _alpha > 0.01f; } } /// 현재 알파값. public float CurrentAlpha { get { return _alpha; } } /// 패드 배경이 놓인 캔버스 좌표. public Vector2 BackgroundAnchoredPosition { get { return background != null ? background.anchoredPosition : Vector2.zero; } } /// 패드 배경의 화면(픽셀) 좌표. 터치 지점과 바로 비교하기 위한 역변환. 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; } /// 화면 픽셀 → 캔버스 단위 배율 (ScreenSpaceOverlay + CanvasScaler 기준) private float ScreenToCanvasScale() { if (_canvasRect == null || Screen.width <= 0) return 1f; return _canvasRect.sizeDelta.x / Screen.width; } /// 화면 좌표 → 이 오브젝트 rect 기준 로컬 앵커 좌표 (SafeArea 인셋이 걸려도 정확) 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; } /// 페이드 없이 즉시 알파를 맞춘다(PD #707 ① — 손을 떼면 그 프레임에 사라져야 한다). private void SnapAlpha(float target) { _alpha = target; if (_group != null) _group.alpha = _alpha; } } }