Unity发布WIN版窗口等比缩放
注意的几个问题:
- PlayerSetting 中勾选Allow Fullscreen Switch,会开启默认全屏快捷键:alt + enter(KeyCode.LeftAlt + KeyCode.Return)。开启默认快捷键后,Update中收不到相关的事件。如果想自己处理alt + enter事件,请不要沟通选。
- 在Update中,点击快捷键切换全屏时,当前帧设置分辨率会不生效,需要延迟到下一帧设置分辨率。
- 快捷切换频率要设置时间间隔,一般是0.6-0.8秒较为舒服(framerate=30的情况下,大概20帧左右)
以下为参考代码(主要修改网上代码):
using UnityEngine;
using System;
using System.Collections;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine.Events;
public class WindowOper : MonoBehaviour
{
/// <summary>
/// 每当窗口分辨率改变或用户切换全屏时,都会触发此事件
/// 参数是新的宽度、高度和全屏状态(true表示全屏)
/// </summary>
public ResolutionChangedEvent resolutionChangedEvent;
[Serializable]
public class ResolutionChangedEvent : UnityEvent<int, int, bool> { }
// 如果为false,则阻止切换到全屏
[SerializeField]
private bool allowFullscreen = true;
// 长宽比的宽度和高度
[SerializeField]
private float aspectRatioWidth = 16;
[SerializeField]
private float aspectRatioHeight = 9;
// 最小值和最大值的窗口宽度/高度像素
[SerializeField]
private int minWidthPixel = 512;
private int minHeightPixel = 512 * 9/16;
[SerializeField]
private int maxWidthPixel = 2048;
[SerializeField]
private int maxHeightPixel = 2048;
private int maxResolutionWith = 1920;
private int maxResolutionHeight = 1080;
// 当前锁定长宽比。
private float aspect;
// 窗口的宽度和高度。不包括边框和窗口标题栏
// 当调整窗口大小时,就会设置这些值
private int setWidth = -1;
private int setHeight = -1;
// 最后一帧全屏状态。
private bool wasFullscreenLastFrame;
private bool switchedFullscreen = false;
// 是否初始化了AspectRatioController
// 一旦注册了WindowProc回调函数,就将其设置为true
private bool started;
// 显示器的宽度和高度。这是窗口当前打开的监视器
private int pixelHeightOfCurrentScreen;
private int pixelWidthOfCurrentScreen;
//一旦用户请求终止applaction,则将其设置为true
private bool quitStarted;
// WinAPI相关定义
#region WINAPI
// 当窗口调整时,WM_SIZING消息通过WindowProc回调发送到窗口
private const int WM_SIZING = 0x214;
// WM大小调整消息的参数
private const int WMSZ_LEFT = 1;
private const int WMSZ_RIGHT = 2;
private const int WMSZ_TOP = 3;
private const int WMSZ_BOTTOM = 6;
// 获取指向WindowProc函数的指针
private const int GWLP_WNDPROC = -4;
// 委托设置为新的WindowProc回调函数
private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
private WndProcDelegate wndProcDelegate;
// 检索调用线程的线程标识符
[DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId();
// 检索指定窗口所属类的名称
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
// 通过将句柄传递给每个窗口,依次传递给应用程序定义的回调函数,枚举与线程关联的所有非子窗口
[DllImport("user32.dll")]
private static extern bool EnumThreadWindows(uint dwThreadId, EnumWindowsProc lpEnumFunc, IntPtr lParam);
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
// 将消息信息传递给指定的窗口过程
[DllImport("user32.dll")]
private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
// 检索指定窗口的边框的尺寸
// 尺寸是在屏幕坐标中给出的,它是相对于屏幕左上角的
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetWindowRect(IntPtr hwnd, ref RECT lpRect);
//检索窗口客户区域的坐标。客户端坐标指定左上角
//以及客户区的右下角。因为客户机坐标是相对于左上角的
//在窗口的客户区域的角落,左上角的坐标是(0,0)
[DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, ref RECT lpRect);
// 更改指定窗口的属性。该函数还将指定偏移量的32位(长)值设置到额外的窗口内存中
[DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto)]
private static extern IntPtr SetWindowLong32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
//更改指定窗口的属性。该函数还在额外的窗口内存中指定的偏移量处设置一个值
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Auto)]
private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
//用于查找窗口句柄的Unity窗口类的名称
private const string UNITY_WND_CLASSNAME = "UnityWndClass";
// Unity窗口的窗口句柄
private IntPtr unityHWnd;
// 指向旧WindowProc回调函数的指针
private IntPtr oldWndProcPtr;
// 指向我们自己的窗口回调函数的指针
private IntPtr newWndProcPtr;
private int resolutionBeforeFullWith = 0;
private int resolutionBeforceFullHeight = 0;
private int resetResolutionFramerate = 0;
private int switchFullscreenFramerate = 0;
/// <summary>
/// WinAPI矩形定义。
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
#endregion
void Start()
{
Resolution[] resolutions = Screen.resolutions;
if(resolutions.Length > 0)
{
var resol = resolutions[resolutions.Length - 1];
/*maxResolutionWith = resol.width;
maxResolutionWith = resol.height;*/
}
// 不要在Unity编辑器中注册WindowProc回调函数,它会指向Unity编辑器窗口,而不是Game视图
minHeightPixel = minWidthPixel * 9 / 16;
#if !UNITY_EDITOR
//注册回调,然后应用程序想要退出
Application.wantsToQuit += ApplicationWantsToQuit;
// 找到主Unity窗口的窗口句柄
EnumThreadWindows(GetCurrentThreadId(), (hWnd, lParam) =>
{
var classText = new StringBuilder(UNITY_WND_CLASSNAME.Length + 1);
GetClassName(hWnd, classText, classText.Capacity);
if (classText.ToString() == UNITY_WND_CLASSNAME)
{
unityHWnd = hWnd;
return false;
}
return true;
}, IntPtr.Zero);
// 将长宽比应用于当前分辨率
SetAspectRatio(aspectRatioWidth, aspectRatioHeight, true);
// 保存当前的全屏状态
wasFullscreenLastFrame = Screen.fullScreen;
// Register (replace) WindowProc callback。每当一个窗口事件被触发时,这个函数都会被调用
//例如调整大小或移动窗口
//保存旧的WindowProc回调函数,因为必须从新回调函数中调用它
wndProcDelegate = wndProc;
newWndProcPtr = Marshal.GetFunctionPointerForDelegate(wndProcDelegate);
oldWndProcPtr = SetWindowLong(unityHWnd, GWLP_WNDPROC, newWndProcPtr);
// 初始化完成
started = true;
#endif
}
/// <summary>
///将目标长宽比设置为给定的长宽比。
/// </summary>
/// <param name="newAspectWidth">宽高比的新宽度</param>
/// <param name="newAspectHeight">纵横比的新高度</param>
/// <param name="apply">true,当前窗口分辨率将立即调整以匹配新的纵横比 false,则只在下次手动调整窗口大小时执行此操作</param>
public void SetAspectRatio(float newAspectWidth, float newAspectHeight, bool apply)
{
//计算新的纵横比
aspectRatioWidth = newAspectWidth;
aspectRatioHeight = newAspectHeight;
aspect = aspectRatioWidth / aspectRatioHeight;
// 调整分辨率以匹配长宽比(触发WindowProc回调)
if (apply)
{
Screen.SetResolution(Screen.width, Mathf.RoundToInt(Screen.width / aspect), Screen.fullScreen);
}
}
/// <summary>
/// WindowProc回调。应用程序定义的函数,用来处理发送到窗口的消息
/// </summary>
/// <param name="msg">用于标识事件的消息</param>
/// <param name="wParam">额外的信息信息。该参数的内容取决于uMsg参数的值 </param>
/// <param name="lParam">其他消息的信息。该参数的内容取决于uMsg参数的值 </param>
/// <returns></returns>
IntPtr wndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
// 检查消息类型
// resize事件
if (msg == WM_SIZING)
{
// 获取窗口大小结构体
RECT rc = (RECT)Marshal.PtrToStructure(lParam, typeof(RECT));
// 计算窗口边框的宽度和高度
RECT windowRect = new RECT();
GetWindowRect(unityHWnd, ref windowRect);
RECT clientRect = new RECT();
GetClientRect(unityHWnd, ref clientRect);
int borderWidth = windowRect.Right - windowRect.Left - (clientRect.Right - clientRect.Left);
int borderHeight = windowRect.Bottom - windowRect.Top - (clientRect.Bottom - clientRect.Top);
// 在应用宽高比之前删除边框(包括窗口标题栏)
rc.Right -= borderWidth;
rc.Bottom -= borderHeight;
// 限制窗口大小
int newWidth = Mathf.Clamp(rc.Right - rc.Left, minWidthPixel, maxWidthPixel);
int newHeight = Mathf.Clamp(rc.Bottom - rc.Top, minHeightPixel, maxHeightPixel);
// 根据纵横比和方向调整大小
switch (wParam.ToInt32())
{
case WMSZ_LEFT:
rc.Left = rc.Right - newWidth;
rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_RIGHT:
rc.Right = rc.Left + newWidth;
rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_TOP:
rc.Top = rc.Bottom - newHeight;
rc.Right = rc.Left + Mathf.RoundToInt(newHeight * aspect);
break;
case WMSZ_BOTTOM:
rc.Bottom = rc.Top + newHeight;
rc.Right = rc.Left + Mathf.RoundToInt(newHeight * aspect);
break;
case WMSZ_RIGHT + WMSZ_BOTTOM:
rc.Right = rc.Left + newWidth;
rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_RIGHT + WMSZ_TOP:
rc.Right = rc.Left + newWidth;
rc.Top = rc.Bottom - Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_LEFT + WMSZ_BOTTOM:
rc.Left = rc.Right - newWidth;
rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
break;
case WMSZ_LEFT + WMSZ_TOP:
rc.Left = rc.Right - newWidth;
rc.Top = rc.Bottom - Mathf.RoundToInt(newWidth / aspect);
break;
}
// 保存实际分辨率,不包括边界
setWidth = rc.Right - rc.Left;
setHeight = rc.Bottom - rc.Top;
Debug.LogError($"wndProc resize:{setWidth},{setHeight}");
// 添加边界
rc.Right += borderWidth;
rc.Bottom += borderHeight;
// 回写更改的窗口参数
Marshal.StructureToPtr(rc, lParam, true);
// 触发分辨率更改事件
resolutionChangedEvent?.Invoke(setWidth, setHeight, Screen.fullScreen);
}
// 调用原始的WindowProc函数
return CallWindowProc(oldWndProcPtr, hWnd, msg, wParam, lParam);
}
void Update()
{
if(switchedFullscreen)
{
if (Screen.fullScreen)
{
wasFullscreenLastFrame = true;
Screen.SetResolution(maxResolutionWith, maxResolutionHeight, true);
}
else
{
if(resolutionBeforceFullHeight > 0 && resolutionBeforeFullWith >0)
{
Screen.SetResolution(resolutionBeforeFullWith, resolutionBeforceFullHeight, false);
resolutionBeforeFullWith = 0;
resolutionBeforceFullHeight = 0;
}
else
{
Screen.SetResolution(Screen.width , Screen.height, false);
}
}
switchedFullscreen = false;
//resolutionChangedEvent?.Invoke(Screen.width, Screen.height, Screen.fullScreen);
}
else if ((switchFullscreenFramerate ++ ) > 20 && Input.GetKey(KeyCode.LeftAlt) && Input.GetKey(KeyCode.Return))
{
if (!Screen.fullScreen)
{
resolutionBeforeFullWith = Screen.width;
resolutionBeforceFullHeight = Screen.height;
}
Screen.fullScreen = !Screen.fullScreen;
switchFullscreenFramerate = 0;
switchedFullscreen = true;
resolutionChangedEvent?.Invoke(setWidth, setHeight, Screen.fullScreen);
}
else if((resetResolutionFramerate ++) > 33 && !Screen.fullScreen &&
setWidth != -1 && setHeight != -1 && (Screen.width != setWidth || Screen.height != setHeight))
{
Screen.SetResolution(setWidth, setHeight, Screen.fullScreen);
resolutionChangedEvent?.Invoke(setWidth, setHeight, Screen.fullScreen);
resetResolutionFramerate = 0;
}
}
/// <summary>
/// 调用SetWindowLong32或SetWindowLongPtr64,取决于可执行文件是32位还是64位。
/// 这样,我们就可以同时构建32位和64位的可执行文件而不会遇到问题。
/// </summary>
/// <param name="hWnd">The window handle.</param>
/// <param name="nIndex">要设置的值的从零开始的偏移量</param>
/// <param name="dwNewLong">The replacement value.</param>
/// <returns>返回值是指定偏移量的前一个值。否则零.</returns>
private static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
{
//32位系统
if (IntPtr.Size == 4)
{
return SetWindowLong32(hWnd, nIndex, dwNewLong);
}
return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
}
/// <summary>
/// 退出时调用。 返回false将中止并使应用程序保持活动。True会让它退出。
/// </summary>
/// <returns></returns>
private bool ApplicationWantsToQuit()
{
//仅允许在应用程序初始化后退出。
if (!started)
{
return false;
}
//延迟退出,clear up
if (!quitStarted)
{
StartCoroutine("DelayedQuit");
return false;
}
return true;
}
/// <summary>
/// 恢复旧的WindowProc回调,然后退出。
/// </summary>
IEnumerator DelayedQuit()
{
// 重新设置旧的WindowProc回调,如果检测到WM_CLOSE,这将在新的回调本身中完成, 64位没问题,32位可能会造成闪退
SetWindowLong(unityHWnd, GWLP_WNDPROC, oldWndProcPtr);
yield return new WaitForEndOfFrame();
quitStarted = true;
Application.Quit();
}
}