201 lines
6.9 KiB
C#
201 lines
6.9 KiB
C#
using System.Collections;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using UnityEngine.Rendering.Universal;
|
|
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
|
using System;
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// 창을 테두리 없는 최상단 투명 창으로 만든다.
|
|
/// 작업표시줄과 Alt+Tab 에서도 숨긴다.
|
|
/// 에디터에서는 아무것도 하지 않는다 (에디터 창이 사라지는 사고 방지).
|
|
/// </summary>
|
|
[RequireComponent(typeof(Camera))]
|
|
public class TransparentWindow : MonoBehaviour
|
|
{
|
|
public enum CoverageMode
|
|
{
|
|
/// <summary>플레이어 설정의 기본 해상도를 그대로 쓴다. 디버깅용.</summary>
|
|
FixedSize,
|
|
|
|
/// <summary>주 모니터를 꽉 채운다. 해상도가 달라도 자동으로 맞춘다.</summary>
|
|
PrimaryMonitor,
|
|
|
|
/// <summary>모든 모니터를 합친 가상 데스크톱 전체를 덮는다.</summary>
|
|
VirtualDesktop,
|
|
}
|
|
|
|
[Header("창 동작")]
|
|
[Tooltip("창이 덮을 범위. 해상도가 바뀌면 자동으로 다시 맞춘다")]
|
|
[SerializeField] CoverageMode coverage = CoverageMode.PrimaryMonitor;
|
|
|
|
[Tooltip("해상도/모니터 변경을 확인하는 주기(초). 0 이면 확인하지 않는다")]
|
|
[SerializeField] float boundsCheckInterval = 2f;
|
|
|
|
[Tooltip("작업표시줄 / Alt+Tab 에서 숨김 (WS_EX_TOOLWINDOW)")]
|
|
[SerializeField] bool hideFromTaskbar = true;
|
|
|
|
[Header("렌더링")]
|
|
[Tooltip("URP 포스트프로세싱 비활성화")]
|
|
[SerializeField] bool forceDisablePostProcessing = true;
|
|
|
|
[Tooltip("포커스가 있을 때 Esc 로 종료. NOACTIVATE 를 켜면 동작하지 않으므로 GlobalExitHotkey 를 함께 쓸 것")]
|
|
[SerializeField] bool quitOnEscape = true;
|
|
|
|
Camera cam;
|
|
|
|
/// <summary>플레이어 창 핸들. 준비되기 전에는 Zero.</summary>
|
|
public System.IntPtr Hwnd { get; private set; } = System.IntPtr.Zero;
|
|
|
|
/// <summary>Win32 스타일 적용이 끝났는지.</summary>
|
|
public bool IsReady { get; private set; }
|
|
|
|
void Awake()
|
|
{
|
|
cam = GetComponent<Camera>();
|
|
|
|
// 알파 0 클리어 — 투명의 출발점
|
|
cam.clearFlags = CameraClearFlags.SolidColor;
|
|
cam.backgroundColor = new Color(0f, 0f, 0f, 0f);
|
|
|
|
if (forceDisablePostProcessing)
|
|
{
|
|
var urp = cam.GetUniversalAdditionalCameraData();
|
|
if (urp != null) urp.renderPostProcessing = false;
|
|
}
|
|
|
|
Application.runInBackground = true;
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
|
StartCoroutine(ApplyWindowStyle());
|
|
#else
|
|
Debug.Log("[TransparentWindow] 에디터에서는 Win32 적용을 건너뜁니다. 빌드해서 테스트하세요.");
|
|
#endif
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
// 포커스가 있을 때만 동작하는 보조 탈출구
|
|
if (quitOnEscape)
|
|
{
|
|
var kb = Keyboard.current;
|
|
if (kb != null && kb.escapeKey.wasPressedThisFrame) Application.Quit();
|
|
}
|
|
|
|
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
|
CheckBoundsChanged();
|
|
#endif
|
|
}
|
|
|
|
#if !UNITY_EDITOR && UNITY_STANDALONE_WIN
|
|
/// <summary>설정한 범위에 해당하는 화면 사각형을 구한다.</summary>
|
|
void GetTargetBounds(out int x, out int y, out int w, out int h)
|
|
{
|
|
switch (coverage)
|
|
{
|
|
case CoverageMode.PrimaryMonitor:
|
|
x = 0;
|
|
y = 0;
|
|
w = Win32.GetSystemMetrics(Win32.SM_CXSCREEN);
|
|
h = Win32.GetSystemMetrics(Win32.SM_CYSCREEN);
|
|
break;
|
|
|
|
case CoverageMode.VirtualDesktop:
|
|
x = Win32.GetSystemMetrics(Win32.SM_XVIRTUALSCREEN);
|
|
y = Win32.GetSystemMetrics(Win32.SM_YVIRTUALSCREEN);
|
|
w = Win32.GetSystemMetrics(Win32.SM_CXVIRTUALSCREEN);
|
|
h = Win32.GetSystemMetrics(Win32.SM_CYVIRTUALSCREEN);
|
|
break;
|
|
|
|
default:
|
|
x = 0;
|
|
y = 0;
|
|
w = Screen.width;
|
|
h = Screen.height;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool applying;
|
|
Vector4 appliedBounds = new Vector4(-1f, -1f, -1f, -1f);
|
|
float nextBoundsCheck;
|
|
|
|
/// <summary>
|
|
/// 해상도 변경, 모니터 연결/해제, 배율 변경에 대응한다.
|
|
/// 이걸 안 하면 모니터를 바꿔 꽂았을 때 창이 예전 크기로 남아 화면 일부만 덮는다.
|
|
/// </summary>
|
|
void CheckBoundsChanged()
|
|
{
|
|
if (!IsReady || applying) return;
|
|
if (boundsCheckInterval <= 0f) return;
|
|
if (Time.unscaledTime < nextBoundsCheck) return;
|
|
|
|
nextBoundsCheck = Time.unscaledTime + boundsCheckInterval;
|
|
|
|
GetTargetBounds(out int x, out int y, out int w, out int h);
|
|
var now = new Vector4(x, y, w, h);
|
|
if (now == appliedBounds) return;
|
|
|
|
Debug.Log($"[TransparentWindow] 화면 구성 변경 감지: {appliedBounds} -> {now}. 다시 적용합니다.");
|
|
StartCoroutine(ApplyWindowStyle());
|
|
}
|
|
|
|
IEnumerator ApplyWindowStyle()
|
|
{
|
|
applying = true;
|
|
|
|
GetTargetBounds(out int x, out int y, out int w, out int h);
|
|
appliedBounds = new Vector4(x, y, w, h);
|
|
|
|
if (coverage != CoverageMode.FixedSize && (w != Screen.width || h != Screen.height))
|
|
{
|
|
Screen.SetResolution(w, h, FullScreenMode.Windowed);
|
|
}
|
|
|
|
// SetResolution 은 프레임 끝에 반영되므로 스타일 적용 전에 기다린다
|
|
yield return null;
|
|
yield return new WaitForEndOfFrame();
|
|
yield return null;
|
|
|
|
IntPtr hwnd = Win32.GetActiveWindow();
|
|
if (hwnd == IntPtr.Zero) hwnd = Win32.FindWindowW(null, Application.productName);
|
|
if (hwnd == IntPtr.Zero)
|
|
{
|
|
Debug.LogError("[TransparentWindow] HWND 를 찾지 못했습니다.");
|
|
yield break;
|
|
}
|
|
|
|
// 타이틀바 / 테두리 제거
|
|
Win32.SetWindowLongW(hwnd, Win32.GWL_STYLE, Win32.WS_POPUP | Win32.WS_VISIBLE);
|
|
|
|
if (hideFromTaskbar)
|
|
{
|
|
uint ex = Win32.GetWindowLongW(hwnd, Win32.GWL_EXSTYLE);
|
|
ex = (ex | Win32.WS_EX_TOOLWINDOW) & ~Win32.WS_EX_APPWINDOW;
|
|
Win32.SetWindowLongW(hwnd, Win32.GWL_EXSTYLE, ex);
|
|
|
|
// TOOLWINDOW 변경은 hide/show 해야 작업표시줄에 반영된다
|
|
Win32.ShowWindow(hwnd, Win32.SW_HIDE);
|
|
Win32.ShowWindow(hwnd, Win32.SW_SHOW);
|
|
}
|
|
|
|
// 픽셀 단위 알파 투명
|
|
var margins = new Win32.MARGINS { cxLeftWidth = -1, cxRightWidth = -1, cyTopHeight = -1, cyBottomHeight = -1 };
|
|
int hr = Win32.DwmExtendFrameIntoClientArea(hwnd, ref margins);
|
|
Debug.Log($"[TransparentWindow] DwmExtendFrameIntoClientArea hr=0x{hr:X8} (0 이면 성공)");
|
|
|
|
Win32.SetWindowPos(hwnd, Win32.HWND_TOPMOST, x, y, w, h,
|
|
Win32.SWP_FRAMECHANGED | Win32.SWP_SHOWWINDOW);
|
|
|
|
Hwnd = hwnd;
|
|
IsReady = true;
|
|
applying = false;
|
|
Debug.Log($"[TransparentWindow] 적용 완료: {coverage} {x},{y} {w}x{h}");
|
|
}
|
|
#endif
|
|
}
|