창 올라타기
This commit is contained in:
168
Assets/02_Scripts/Character/CharacterDragger.cs
Normal file
168
Assets/02_Scripts/Character/CharacterDragger.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 캐릭터를 마우스로 집어 옮긴다.
|
||||
///
|
||||
/// 구현 노트 1 — 왜 Unity Input 을 쓰지 않는가:
|
||||
/// 우리 창은 WS_EX_NOACTIVATE 라 포커스를 받지 않는다. 클릭 자체는 들어오지만
|
||||
/// 포커스 없는 상태의 입력은 신뢰도가 떨어지고, 커서가 창 밖으로 나가면 Unity 는
|
||||
/// 위치를 아예 모른다. GetAsyncKeyState 와 GetCursorPos 로 OS 에 직접 물으면
|
||||
/// 포커스와 무관하게 항상 정확하다.
|
||||
///
|
||||
/// 구현 노트 2 — 드래그 중 클릭 통과 잠금:
|
||||
/// 커서를 빠르게 움직이면 캐릭터 콜라이더를 벗어나는데, 그 순간 히트테스트가
|
||||
/// 클릭 통과를 다시 켜버리면 입력이 뒤 창으로 새면서 드래그가 끊긴다.
|
||||
/// 드래그 동안 ForceInteractive 로 히트테스트를 무력화한다.
|
||||
///
|
||||
/// 여기서 만든 "화면 좌표 -> 월드 위치" 배선은 창 올라타기에서도 그대로 쓴다.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(ClickThroughHitTest))]
|
||||
public class CharacterDragger : MonoBehaviour
|
||||
{
|
||||
[Header("참조")]
|
||||
[Tooltip("비우면 같은 오브젝트의 Camera 또는 Camera.main")]
|
||||
[SerializeField] Camera viewCamera;
|
||||
|
||||
[Tooltip("비우면 씬에서 탐색")]
|
||||
[SerializeField] TransparentWindow window;
|
||||
|
||||
[Tooltip("비우면 씬에서 탐색")]
|
||||
[SerializeField] VrmCharacterLoader loader;
|
||||
|
||||
[Header("동작")]
|
||||
[Tooltip("0 이면 커서에 즉시 붙는다. 값을 올리면 살짝 끌려오는 느낌이 난다")]
|
||||
[Range(0f, 30f)]
|
||||
[SerializeField] float followSpeed = 0f;
|
||||
|
||||
[Tooltip("이 거리 이상 움직여야 드래그로 인정한다. 클릭과 드래그를 구분한다")]
|
||||
[SerializeField] float dragThresholdPixels = 4f;
|
||||
|
||||
[Tooltip("화면 가장자리에서 추가로 띄울 여백(픽셀). 캐릭터가 잘리지 않도록 제한한다")]
|
||||
[SerializeField] float screenMargin = 0f;
|
||||
|
||||
ClickThroughHitTest hitTest;
|
||||
|
||||
Transform target;
|
||||
bool pressing; // 버튼이 눌린 상태(아직 드래그는 아닐 수 있음)
|
||||
bool dragging; // 임계값을 넘겨 실제 드래그 중
|
||||
Vector2 pressStartPos;
|
||||
Vector2 grabScreenOffset; // 캐릭터 원점 - 커서 (화면 좌표)
|
||||
Vector2 boundsOffMin, boundsOffMax; // 원점 기준 캐릭터의 화면상 범위
|
||||
float dragDepth;
|
||||
|
||||
/// <summary>드래그 중인지. 창 올라타기 등 자동 이동 로직이 이 동안 양보한다.</summary>
|
||||
public bool IsDragging => dragging;
|
||||
|
||||
/// <summary>드래그가 끝난 순간 호출. 창 올라타기가 착지 지점을 다시 계산할 때 쓴다.</summary>
|
||||
public event System.Action<Transform> DragEnded;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
hitTest = GetComponent<ClickThroughHitTest>();
|
||||
if (viewCamera == null) viewCamera = GetComponent<Camera>();
|
||||
if (viewCamera == null) viewCamera = Camera.main;
|
||||
if (window == null) window = FindFirstObjectByType<TransparentWindow>();
|
||||
if (loader == null) loader = FindFirstObjectByType<VrmCharacterLoader>();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
||||
bool buttonDown = Win32.IsKeyDown(Win32.VK_LBUTTON);
|
||||
#else
|
||||
var mouse = UnityEngine.InputSystem.Mouse.current;
|
||||
bool buttonDown = mouse != null && mouse.leftButton.isPressed;
|
||||
#endif
|
||||
|
||||
if (!pressing && buttonDown && hitTest.IsOverCharacter)
|
||||
{
|
||||
BeginPress();
|
||||
}
|
||||
else if (pressing && !buttonDown)
|
||||
{
|
||||
EndPress();
|
||||
}
|
||||
|
||||
if (pressing) UpdatePress();
|
||||
}
|
||||
|
||||
void BeginPress()
|
||||
{
|
||||
target = loader != null && loader.Current != null ? loader.Current.Root : null;
|
||||
if (target == null) return;
|
||||
if (!TryGetCursor(out Vector2 sp)) return;
|
||||
|
||||
pressing = true;
|
||||
dragging = false;
|
||||
pressStartPos = sp;
|
||||
|
||||
// 캐릭터가 놓인 깊이를 유지한다. 이 평면 위에서만 움직이므로 크기가 변하지 않는다.
|
||||
dragDepth = Vector3.Dot(target.position - viewCamera.transform.position,
|
||||
viewCamera.transform.forward);
|
||||
|
||||
// 깊이가 고정이라 화면상 크기도 고정이다. 시작할 때 한 번만 재면 된다.
|
||||
if (!CharacterScreenBounds.TryMeasure(viewCamera, target, out boundsOffMin, out boundsOffMax))
|
||||
{
|
||||
boundsOffMin = boundsOffMax = Vector2.zero;
|
||||
}
|
||||
|
||||
Vector2 originScreen = viewCamera.WorldToScreenPoint(target.position);
|
||||
grabScreenOffset = originScreen - sp;
|
||||
}
|
||||
|
||||
void UpdatePress()
|
||||
{
|
||||
if (target == null) { EndPress(); return; }
|
||||
if (!TryGetCursor(out Vector2 sp)) return;
|
||||
|
||||
if (!dragging)
|
||||
{
|
||||
// 임계값을 넘기 전까지는 클릭으로 본다. 제자리 클릭에 캐릭터가 튀지 않게.
|
||||
if (Vector2.Distance(sp, pressStartPos) < dragThresholdPixels) return;
|
||||
|
||||
dragging = true;
|
||||
hitTest.ForceInteractive = true;
|
||||
}
|
||||
|
||||
// 화면 좌표에서 제한한다. 월드에서 제한하면 카메라 각도에 따라 계산이 복잡해진다.
|
||||
Vector2 desiredScreen = sp + grabScreenOffset;
|
||||
desiredScreen = CharacterScreenBounds.Clamp(desiredScreen, boundsOffMin, boundsOffMax, screenMargin);
|
||||
|
||||
Vector3 desired = ScreenToWorld(desiredScreen);
|
||||
|
||||
if (followSpeed <= 0.01f)
|
||||
{
|
||||
target.position = desired;
|
||||
}
|
||||
else
|
||||
{
|
||||
float t = 1f - Mathf.Exp(-followSpeed * Time.deltaTime); // 프레임레이트 독립
|
||||
target.position = Vector3.Lerp(target.position, desired, t);
|
||||
}
|
||||
}
|
||||
|
||||
void EndPress()
|
||||
{
|
||||
bool wasDragging = dragging;
|
||||
Transform dragged = target;
|
||||
|
||||
pressing = false;
|
||||
dragging = false;
|
||||
hitTest.ForceInteractive = false;
|
||||
target = null;
|
||||
|
||||
if (wasDragging && dragged != null) DragEnded?.Invoke(dragged);
|
||||
}
|
||||
|
||||
bool TryGetCursor(out Vector2 screenPos)
|
||||
{
|
||||
System.IntPtr hwnd = window != null ? window.Hwnd : System.IntPtr.Zero;
|
||||
return DesktopCursor.TryGetScreenPosition(hwnd, out screenPos);
|
||||
}
|
||||
|
||||
/// <summary>화면 좌표를 드래그 평면(고정 깊이) 위의 월드 좌표로 변환한다.</summary>
|
||||
Vector3 ScreenToWorld(Vector2 screenPos)
|
||||
{
|
||||
return viewCamera.ScreenToWorldPoint(new Vector3(screenPos.x, screenPos.y, dragDepth));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user