채팅 추가
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
@@ -13,6 +15,10 @@
|
||||
/// 투명도와 공존하며, SetLayeredWindowAttributes 호출은 필요하지 않았다.
|
||||
///
|
||||
/// LAYERED 는 init 시 한 번만 걸고, 매 프레임 토글하는 것은 TRANSPARENT 뿐이다.
|
||||
///
|
||||
/// 캐릭터 말고도 클릭을 받아야 하는 것이 생기면(채팅창 같은 UI) 그 영역을
|
||||
/// RegisterInteractiveRegion 으로 등록한다. 히트테스트는 3D 콜라이더와 등록된
|
||||
/// 영역을 모두 본다.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(TransparentWindow))]
|
||||
public class ClickThroughHitTest : MonoBehaviour
|
||||
@@ -32,6 +38,9 @@ public class ClickThroughHitTest : MonoBehaviour
|
||||
// 정상 수신된다. 사용자가 쓰던 앱의 포커스를 뺏지 않는 쪽이 데스크톱 비서로서
|
||||
// 명백히 낫기 때문에 항상 켠다.
|
||||
//
|
||||
// 예외는 채팅창이다. 키보드 입력은 포커스가 없으면 들어오지 않으므로
|
||||
// 채팅창이 열려 있는 동안만 SetNoActivate(false) 로 잠시 내린다.
|
||||
//
|
||||
// SerializeField 가 아닌 이유: 씬에 예전 false 값이 직렬화돼 있어
|
||||
// 인스펙터 값이 코드 기본값을 이겨버린다. 런타임 토글은 Ctrl+Alt+N 으로 유지.
|
||||
bool useNoActivate = true;
|
||||
@@ -53,9 +62,15 @@ public class ClickThroughHitTest : MonoBehaviour
|
||||
Vector3 hoveredBaseScale;
|
||||
GUIStyle hudStyle;
|
||||
|
||||
/// <summary>캐릭터 외에 클릭을 받아야 하는 화면 영역들. 채팅창 등이 등록한다.</summary>
|
||||
readonly List<Func<Vector2, bool>> interactiveRegions = new List<Func<Vector2, bool>>();
|
||||
|
||||
/// <summary>현재 OS 커서가 캐릭터 위에 있는지.</summary>
|
||||
public bool IsOverCharacter { get; private set; }
|
||||
|
||||
/// <summary>현재 OS 커서가 등록된 UI 영역 위에 있는지.</summary>
|
||||
public bool IsOverUi { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// true 인 동안 커서 위치와 무관하게 클릭을 받는다.
|
||||
///
|
||||
@@ -66,7 +81,9 @@ public class ClickThroughHitTest : MonoBehaviour
|
||||
public bool ForceInteractive { get; set; }
|
||||
|
||||
int clickCount;
|
||||
uint currentExStyle;
|
||||
// Windows 빌드에서만 갱신된다. 에디터에서는 0 으로 남으므로 명시적으로 초기화해
|
||||
// "한 번도 대입되지 않았다"는 경고를 없앤다.
|
||||
uint currentExStyle = 0;
|
||||
string lastAction = "-";
|
||||
|
||||
void Awake()
|
||||
@@ -76,26 +93,48 @@ void Awake()
|
||||
if (hitTestCamera == null) hitTestCamera = Camera.main;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 화면 좌표를 받아 "여기는 클릭을 받아야 한다"를 판정하는 함수를 등록한다.
|
||||
/// 등록한 쪽이 사라질 때 반드시 해제한다.
|
||||
/// </summary>
|
||||
public void RegisterInteractiveRegion(Func<Vector2, bool> region)
|
||||
{
|
||||
if (region != null && !interactiveRegions.Contains(region)) interactiveRegions.Add(region);
|
||||
}
|
||||
|
||||
public void UnregisterInteractiveRegion(Func<Vector2, bool> region)
|
||||
{
|
||||
if (region != null) interactiveRegions.Remove(region);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NOACTIVATE 를 켜고 끈다. 끄면 창이 포커스를 받을 수 있게 되어 키보드 입력이 들어온다.
|
||||
/// 채팅창이 열고 닫을 때 부른다.
|
||||
/// </summary>
|
||||
public void SetNoActivate(bool enable)
|
||||
{
|
||||
if (useNoActivate == enable) return;
|
||||
useNoActivate = enable;
|
||||
lastAction = $"NOACTIVATE = {useNoActivate}";
|
||||
|
||||
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
||||
// true = 클릭이 뒤 창으로 통과하는 상태
|
||||
bool clickThrough = true;
|
||||
bool styleInitialized;
|
||||
bool prevN;
|
||||
if (window != null && window.IsReady && window.Hwnd != IntPtr.Zero) ApplyBaseStyle();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!window.IsReady || window.Hwnd == System.IntPtr.Zero) return;
|
||||
IntPtr hwnd = window != null ? window.Hwnd : IntPtr.Zero;
|
||||
|
||||
if (!styleInitialized)
|
||||
{
|
||||
ApplyBaseStyle();
|
||||
styleInitialized = true;
|
||||
}
|
||||
// 좌표 변환은 DesktopCursor 에 모아뒀다. 시선 추적도 같은 변환을 쓴다.
|
||||
// 에디터에서는 일반 마우스 위치로 대체되므로 플레이 모드에서도 히트테스트가 돈다.
|
||||
bool haveCursor = DesktopCursor.TryGetScreenPosition(hwnd, out Vector2 cursor);
|
||||
|
||||
PollNoActivateToggle();
|
||||
// UI 가 캐릭터를 가리고 있으면 UI 가 이긴다. 채팅창 위에서 드래그가 시작되면 곤란하다.
|
||||
IsOverUi = haveCursor && IsInsideInteractiveRegion(cursor);
|
||||
|
||||
IsOverCharacter = HitTest(out var hit);
|
||||
SetClickThrough(!IsOverCharacter && !ForceInteractive);
|
||||
RaycastHit hit = default;
|
||||
IsOverCharacter = haveCursor && !IsOverUi && HitTest(cursor, out hit);
|
||||
|
||||
// 드래그 중에는 커서가 캐릭터를 벗어나도 호버 표시를 유지한다.
|
||||
// 화면 끝까지 끌면 캐릭터는 제한에 걸려 멈추고 커서만 더 나가는데,
|
||||
@@ -109,9 +148,52 @@ void Update()
|
||||
{
|
||||
clickCount++;
|
||||
lastAction = $"클릭: {hit.transform.name}";
|
||||
Debug.Log($"[ClickThroughHitTest] 캐릭터 클릭됨: {hit.transform.name}");
|
||||
}
|
||||
|
||||
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
||||
UpdateWindowStyle();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IsInsideInteractiveRegion(Vector2 screenPos)
|
||||
{
|
||||
for (int i = 0; i < interactiveRegions.Count; i++)
|
||||
{
|
||||
var region = interactiveRegions[i];
|
||||
if (region != null && region(screenPos)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HitTest(Vector2 screenPos, out RaycastHit hit)
|
||||
{
|
||||
hit = default;
|
||||
if (hitTestCamera == null) return false;
|
||||
|
||||
var ray = hitTestCamera.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
|
||||
return Physics.Raycast(ray, out hit, maxRayDistance, interactableLayers);
|
||||
}
|
||||
|
||||
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
||||
// true = 클릭이 뒤 창으로 통과하는 상태
|
||||
bool clickThrough = true;
|
||||
bool styleInitialized;
|
||||
bool prevN;
|
||||
|
||||
void UpdateWindowStyle()
|
||||
{
|
||||
if (!window.IsReady || window.Hwnd == IntPtr.Zero) return;
|
||||
|
||||
if (!styleInitialized)
|
||||
{
|
||||
ApplyBaseStyle();
|
||||
styleInitialized = true;
|
||||
}
|
||||
|
||||
PollNoActivateToggle();
|
||||
|
||||
SetClickThrough(!IsOverCharacter && !IsOverUi && !ForceInteractive);
|
||||
|
||||
currentExStyle = Win32.GetWindowLongW(window.Hwnd, Win32.GWL_EXSTYLE);
|
||||
}
|
||||
|
||||
@@ -127,9 +209,7 @@ void PollNoActivateToggle()
|
||||
bool now = Win32.IsKeyDown(Win32.VK_N);
|
||||
if (now && !prevN)
|
||||
{
|
||||
useNoActivate = !useNoActivate;
|
||||
ApplyBaseStyle();
|
||||
lastAction = $"NOACTIVATE = {useNoActivate}";
|
||||
SetNoActivate(!useNoActivate);
|
||||
Debug.Log($"[ClickThroughHitTest] NOACTIVATE = {useNoActivate}");
|
||||
}
|
||||
prevN = now;
|
||||
@@ -153,17 +233,6 @@ void ApplyBaseStyle()
|
||||
Win32.SWP_NOMOVE | Win32.SWP_NOSIZE | Win32.SWP_NOACTIVATE | Win32.SWP_FRAMECHANGED);
|
||||
}
|
||||
|
||||
bool HitTest(out RaycastHit hit)
|
||||
{
|
||||
hit = default;
|
||||
if (hitTestCamera == null) return false;
|
||||
// 좌표 변환은 DesktopCursor 에 모아뒀다. 시선 추적도 같은 변환을 쓴다.
|
||||
if (!DesktopCursor.TryGetScreenPosition(window.Hwnd, out Vector2 sp)) return false;
|
||||
|
||||
var ray = hitTestCamera.ScreenPointToRay(new Vector3(sp.x, sp.y, 0f));
|
||||
return Physics.Raycast(ray, out hit, maxRayDistance, interactableLayers);
|
||||
}
|
||||
|
||||
void SetClickThrough(bool enable)
|
||||
{
|
||||
if (enable == clickThrough) return;
|
||||
@@ -206,7 +275,7 @@ void OnGUI()
|
||||
string text =
|
||||
$"NOACTIVATE: {useNoActivate} [Ctrl+Alt+N 전환]\n" +
|
||||
$"ExStyle: 0x{currentExStyle:X8} LAYERED={layered} TRANSPARENT={transparent}\n" +
|
||||
$"OverCharacter: {IsOverCharacter} 클릭 횟수: {clickCount}\n" +
|
||||
$"OverCharacter: {IsOverCharacter} OverUI: {IsOverUi} 클릭 횟수: {clickCount}\n" +
|
||||
$"마지막: {lastAction}\n" +
|
||||
$"[Ctrl+Alt+Q 종료]";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user