当前位置:   article > 正文

Unity win平台 调整窗口大小强制固定比例_unity unity游戏窗口的长宽比

unity unity游戏窗口的长宽比

我写了一个脚本,对独立的Windows版本(仅Windows,32/64位)强制执行任意宽高比。
它通过拦截窗口调整大小事件(特别是WinProc回调函数)并对其进行修改以保持所需的宽高比来工作。

AspectRatioController.cs添加到场景中的任何GameObject。然后在Inspector中设置所需的纵横比和最小分辨率值。

  1. using UnityEngine;
  2. using System;
  3. using System.Collections;
  4. using System.Runtime.InteropServices;
  5. using System.Text;
  6. using UnityEngine.Events;
  7. /// <summary>
  8. ///强制设置Unity游戏窗口的长宽比。你可以调整窗口的大小,他会强制保持一定比例
  9. ///通过拦截窗口大小调整事件(WindowProc回调)并相应地修改它们来实现的
  10. ///也可以用像素为窗口设置最小/最大宽度和高度
  11. ///长宽比和最小/最大分辨率都与窗口区域有关,标题栏和边框不包括在内
  12. ///该脚本还将在应用程序处于全屏状态时强制设置长宽比。当你切换到全屏,
  13. ///应用程序将自动设置为当前显示器上可能的最大分辨率,而仍然保持固定比。如果显示器没有相同的宽高比,则会在左/右或上/下添加黑条
  14. ///确保你在PlayerSetting中设置了“Resizable Window”,否则无法调整大小
  15. ///如果取消不支持的长宽比在PlayerSetting中设置“Supported Aspect Rations”
  16. ///注意:因为使用了WinAPI,所以只能在Windows上工作。在Windows 10上测试过
  17. /// </summary>
  18. public class AspectRatioController : MonoBehaviour
  19. {
  20. /// <summary>
  21. /// 每当窗口分辨率改变或用户切换全屏时,都会触发此事件
  22. /// 参数是新的宽度、高度和全屏状态(true表示全屏)
  23. /// </summary>
  24. public ResolutionChangedEvent resolutionChangedEvent;
  25. [Serializable]
  26. public class ResolutionChangedEvent : UnityEvent<int, int, bool> { }
  27. // 如果为false,则阻止切换到全屏
  28. [SerializeField]
  29. private bool allowFullscreen = true;
  30. // 长宽比的宽度和高度
  31. [SerializeField]
  32. private float aspectRatioWidth = 16;
  33. [SerializeField]
  34. private float aspectRatioHeight = 9;
  35. // 最小值和最大值的窗口宽度/高度像素
  36. [SerializeField]
  37. private int minWidthPixel = 512;
  38. [SerializeField]
  39. private int minHeightPixel = 512;
  40. [SerializeField]
  41. private int maxWidthPixel = 2048;
  42. [SerializeField]
  43. private int maxHeightPixel = 2048;
  44. // 当前锁定长宽比。
  45. private float aspect;
  46. // 窗口的宽度和高度。不包括边框和窗口标题栏
  47. // 当调整窗口大小时,就会设置这些值
  48. private int setWidth = -1;
  49. private int setHeight = -1;
  50. // 最后一帧全屏状态。
  51. private bool wasFullscreenLastFrame;
  52. // 是否初始化了AspectRatioController
  53. // 一旦注册了WindowProc回调函数,就将其设置为true
  54. private bool started;
  55. // 显示器的宽度和高度。这是窗口当前打开的监视器
  56. private int pixelHeightOfCurrentScreen;
  57. private int pixelWidthOfCurrentScreen;
  58. //一旦用户请求终止applaction,则将其设置为true
  59. private bool quitStarted;
  60. // WinAPI相关定义
  61. #region WINAPI
  62. // 当窗口调整时,WM_SIZING消息通过WindowProc回调发送到窗口
  63. private const int WM_SIZING = 0x214;
  64. // WM大小调整消息的参数
  65. private const int WMSZ_LEFT = 1;
  66. private const int WMSZ_RIGHT = 2;
  67. private const int WMSZ_TOP = 3;
  68. private const int WMSZ_BOTTOM = 6;
  69. // 获取指向WindowProc函数的指针
  70. private const int GWLP_WNDPROC = -4;
  71. // 委托设置为新的WindowProc回调函数
  72. private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
  73. private WndProcDelegate wndProcDelegate;
  74. // 检索调用线程的线程标识符
  75. [DllImport("kernel32.dll")]
  76. private static extern uint GetCurrentThreadId();
  77. // 检索指定窗口所属类的名称
  78. [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  79. private static extern int GetClassName(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
  80. // 通过将句柄传递给每个窗口,依次传递给应用程序定义的回调函数,枚举与线程关联的所有非子窗口
  81. [DllImport("user32.dll")]
  82. private static extern bool EnumThreadWindows(uint dwThreadId, EnumWindowsProc lpEnumFunc, IntPtr lParam);
  83. private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
  84. // 将消息信息传递给指定的窗口过程
  85. [DllImport("user32.dll")]
  86. private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
  87. // 检索指定窗口的边框的尺寸
  88. // 尺寸是在屏幕坐标中给出的,它是相对于屏幕左上角的
  89. [DllImport("user32.dll", SetLastError = true)]
  90. private static extern bool GetWindowRect(IntPtr hwnd, ref RECT lpRect);
  91. //检索窗口客户区域的坐标。客户端坐标指定左上角
  92. //以及客户区的右下角。因为客户机坐标是相对于左上角的
  93. //在窗口的客户区域的角落,左上角的坐标是(0,0)
  94. [DllImport("user32.dll")]
  95. private static extern bool GetClientRect(IntPtr hWnd, ref RECT lpRect);
  96. // 更改指定窗口的属性。该函数还将指定偏移量的32位(长)值设置到额外的窗口内存中
  97. [DllImport("user32.dll", EntryPoint = "SetWindowLong", CharSet = CharSet.Auto)]
  98. private static extern IntPtr SetWindowLong32(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
  99. //更改指定窗口的属性。该函数还在额外的窗口内存中指定的偏移量处设置一个值
  100. [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", CharSet = CharSet.Auto)]
  101. private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
  102. //用于查找窗口句柄的Unity窗口类的名称
  103. private const string UNITY_WND_CLASSNAME = "UnityWndClass";
  104. // Unity窗口的窗口句柄
  105. private IntPtr unityHWnd;
  106. // 指向旧WindowProc回调函数的指针
  107. private IntPtr oldWndProcPtr;
  108. // 指向我们自己的窗口回调函数的指针
  109. private IntPtr newWndProcPtr;
  110. /// <summary>
  111. /// WinAPI矩形定义。
  112. /// </summary>
  113. [StructLayout(LayoutKind.Sequential)]
  114. public struct RECT
  115. {
  116. public int Left;
  117. public int Top;
  118. public int Right;
  119. public int Bottom;
  120. }
  121. #endregion
  122. void Start()
  123. {
  124. // 不要在Unity编辑器中注册WindowProc回调函数,它会指向Unity编辑器窗口,而不是Game视图
  125. #if !UNITY_EDITOR
  126. //注册回调,然后应用程序想要退出
  127. Application.wantsToQuit += ApplicationWantsToQuit;
  128. // 找到主Unity窗口的窗口句柄
  129. EnumThreadWindows(GetCurrentThreadId(), (hWnd, lParam) =>
  130. {
  131. var classText = new StringBuilder(UNITY_WND_CLASSNAME.Length + 1);
  132. GetClassName(hWnd, classText, classText.Capacity);
  133. if (classText.ToString() == UNITY_WND_CLASSNAME)
  134. {
  135. unityHWnd = hWnd;
  136. return false;
  137. }
  138. return true;
  139. }, IntPtr.Zero);
  140. // 将长宽比应用于当前分辨率
  141. SetAspectRatio(aspectRatioWidth, aspectRatioHeight, true);
  142. // 保存当前的全屏状态
  143. wasFullscreenLastFrame = Screen.fullScreen;
  144. // Register (replace) WindowProc callback。每当一个窗口事件被触发时,这个函数都会被调用
  145. //例如调整大小或移动窗口
  146. //保存旧的WindowProc回调函数,因为必须从新回调函数中调用它
  147. wndProcDelegate = wndProc;
  148. newWndProcPtr = Marshal.GetFunctionPointerForDelegate(wndProcDelegate);
  149. oldWndProcPtr = SetWindowLong(unityHWnd, GWLP_WNDPROC, newWndProcPtr);
  150. // 初始化完成
  151. started = true;
  152. #endif
  153. }
  154. /// <summary>
  155. ///将目标长宽比设置为给定的长宽比。
  156. /// </summary>
  157. /// <param name="newAspectWidth">宽高比的新宽度</param>
  158. /// <param name="newAspectHeight">纵横比的新高度</param>
  159. /// <param name="apply">true,当前窗口分辨率将立即调整以匹配新的纵横比 false,则只在下次手动调整窗口大小时执行此操作</param>
  160. public void SetAspectRatio(float newAspectWidth, float newAspectHeight, bool apply)
  161. {
  162. //计算新的纵横比
  163. aspectRatioWidth = newAspectWidth;
  164. aspectRatioHeight = newAspectHeight;
  165. aspect = aspectRatioWidth / aspectRatioHeight;
  166. // 调整分辨率以匹配长宽比(触发WindowProc回调)
  167. if (apply)
  168. {
  169. Screen.SetResolution(Screen.width, Mathf.RoundToInt(Screen.width / aspect), Screen.fullScreen);
  170. }
  171. }
  172. /// <summary>
  173. /// WindowProc回调。应用程序定义的函数,用来处理发送到窗口的消息
  174. /// </summary>
  175. /// <param name="msg">用于标识事件的消息</param>
  176. /// <param name="wParam">额外的信息信息。该参数的内容取决于uMsg参数的值 </param>
  177. /// <param name="lParam">其他消息的信息。该参数的内容取决于uMsg参数的值 </param>
  178. /// <returns></returns>
  179. IntPtr wndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
  180. {
  181. // 检查消息类型
  182. // resize事件
  183. if (msg == WM_SIZING)
  184. {
  185. // 获取窗口大小结构体
  186. RECT rc = (RECT)Marshal.PtrToStructure(lParam, typeof(RECT));
  187. // 计算窗口边框的宽度和高度
  188. RECT windowRect = new RECT();
  189. GetWindowRect(unityHWnd, ref windowRect);
  190. RECT clientRect = new RECT();
  191. GetClientRect(unityHWnd, ref clientRect);
  192. int borderWidth = windowRect.Right - windowRect.Left - (clientRect.Right - clientRect.Left);
  193. int borderHeight = windowRect.Bottom - windowRect.Top - (clientRect.Bottom - clientRect.Top);
  194. // 在应用宽高比之前删除边框(包括窗口标题栏)
  195. rc.Right -= borderWidth;
  196. rc.Bottom -= borderHeight;
  197. // 限制窗口大小
  198. int newWidth = Mathf.Clamp(rc.Right - rc.Left, minWidthPixel, maxWidthPixel);
  199. int newHeight = Mathf.Clamp(rc.Bottom - rc.Top, minHeightPixel, maxHeightPixel);
  200. // 根据纵横比和方向调整大小
  201. switch (wParam.ToInt32())
  202. {
  203. case WMSZ_LEFT:
  204. rc.Left = rc.Right - newWidth;
  205. rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
  206. break;
  207. case WMSZ_RIGHT:
  208. rc.Right = rc.Left + newWidth;
  209. rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
  210. break;
  211. case WMSZ_TOP:
  212. rc.Top = rc.Bottom - newHeight;
  213. rc.Right = rc.Left + Mathf.RoundToInt(newHeight * aspect);
  214. break;
  215. case WMSZ_BOTTOM:
  216. rc.Bottom = rc.Top + newHeight;
  217. rc.Right = rc.Left + Mathf.RoundToInt(newHeight * aspect);
  218. break;
  219. case WMSZ_RIGHT + WMSZ_BOTTOM:
  220. rc.Right = rc.Left + newWidth;
  221. rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
  222. break;
  223. case WMSZ_RIGHT + WMSZ_TOP:
  224. rc.Right = rc.Left + newWidth;
  225. rc.Top = rc.Bottom - Mathf.RoundToInt(newWidth / aspect);
  226. break;
  227. case WMSZ_LEFT + WMSZ_BOTTOM:
  228. rc.Left = rc.Right - newWidth;
  229. rc.Bottom = rc.Top + Mathf.RoundToInt(newWidth / aspect);
  230. break;
  231. case WMSZ_LEFT + WMSZ_TOP:
  232. rc.Left = rc.Right - newWidth;
  233. rc.Top = rc.Bottom - Mathf.RoundToInt(newWidth / aspect);
  234. break;
  235. }
  236. // 保存实际分辨率,不包括边界
  237. setWidth = rc.Right - rc.Left;
  238. setHeight = rc.Bottom - rc.Top;
  239. // 添加边界
  240. rc.Right += borderWidth;
  241. rc.Bottom += borderHeight;
  242. // 触发分辨率更改事件
  243. resolutionChangedEvent.Invoke(setWidth, setHeight, Screen.fullScreen);
  244. // 回写更改的窗口参数
  245. Marshal.StructureToPtr(rc, lParam, true);
  246. }
  247. // 调用原始的WindowProc函数
  248. return CallWindowProc(oldWndProcPtr, hWnd, msg, wParam, lParam);
  249. }
  250. void Update()
  251. {
  252. // 如果不允许全屏,则阻止切换到全屏
  253. if (!allowFullscreen && Screen.fullScreen)
  254. {
  255. Screen.fullScreen = false;
  256. }
  257. if (Screen.fullScreen && !wasFullscreenLastFrame)
  258. {
  259. //切换到全屏检测,设置为最大屏幕分辨率,同时保持长宽比
  260. int height;
  261. int width;
  262. //根据当前长宽比和显示器的比例进行比较,上下或左右添加黑边
  263. bool blackBarsLeftRight = aspect < (float)pixelWidthOfCurrentScreen / pixelHeightOfCurrentScreen;
  264. if (blackBarsLeftRight)
  265. {
  266. height = pixelHeightOfCurrentScreen;
  267. width = Mathf.RoundToInt(pixelHeightOfCurrentScreen * aspect);
  268. }
  269. else
  270. {
  271. width = pixelWidthOfCurrentScreen;
  272. height = Mathf.RoundToInt(pixelWidthOfCurrentScreen / aspect);
  273. }
  274. Screen.SetResolution(width, height, true);
  275. resolutionChangedEvent.Invoke(width, height, true);
  276. }
  277. else if (!Screen.fullScreen && wasFullscreenLastFrame)
  278. {
  279. // 从全屏切换到检测到的窗口。设置上一个窗口的分辨率。
  280. Screen.SetResolution(setWidth, setHeight, false);
  281. resolutionChangedEvent.Invoke(setWidth, setHeight, false);
  282. }
  283. else if (!Screen.fullScreen && setWidth != -1 && setHeight != -1 && (Screen.width != setWidth || Screen.height != setHeight))
  284. {
  285. //根据高度设置宽度,因为Aero Snap不会触发WM_SIZING。
  286. setHeight = Screen.height;
  287. setWidth = Mathf.RoundToInt(Screen.height * aspect);
  288. Screen.SetResolution(setWidth, setHeight, Screen.fullScreen);
  289. resolutionChangedEvent.Invoke(setWidth, setHeight, Screen.fullScreen);
  290. }
  291. else if (!Screen.fullScreen)
  292. {
  293. // 保存当前屏幕的分辨率
  294. // 下次切换到全屏时,此分辨率将被设置为窗口分辨率
  295. // 只有高度,如果需要,宽度将根据高度和长宽比设置,以确保长宽比保持在全屏模式
  296. pixelHeightOfCurrentScreen = Screen.currentResolution.height;
  297. pixelWidthOfCurrentScreen = Screen.currentResolution.width;
  298. }
  299. //保存下一帧的全屏状态
  300. wasFullscreenLastFrame = Screen.fullScreen;
  301. // 当游戏窗口调整大小时,在编辑器中触发分辨率改变事件。
  302. #if UNITY_EDITOR
  303. if (Screen.width != setWidth || Screen.height != setHeight)
  304. {
  305. setWidth = Screen.width;
  306. setHeight = Screen.height;
  307. resolutionChangedEvent.Invoke(setWidth, setHeight, Screen.fullScreen);
  308. }
  309. #endif
  310. }
  311. /// <summary>
  312. /// 调用SetWindowLong32或SetWindowLongPtr64,取决于可执行文件是32位还是64位。
  313. /// 这样,我们就可以同时构建32位和64位的可执行文件而不会遇到问题。
  314. /// </summary>
  315. /// <param name="hWnd">The window handle.</param>
  316. /// <param name="nIndex">要设置的值的从零开始的偏移量</param>
  317. /// <param name="dwNewLong">The replacement value.</param>
  318. /// <returns>返回值是指定偏移量的前一个值。否则零.</returns>
  319. private static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
  320. {
  321. //32位系统
  322. if (IntPtr.Size == 4)
  323. {
  324. return SetWindowLong32(hWnd, nIndex, dwNewLong);
  325. }
  326. return SetWindowLongPtr64(hWnd, nIndex, dwNewLong);
  327. }
  328. /// <summary>
  329. /// 退出时调用。 返回false将中止并使应用程序保持活动。True会让它退出。
  330. /// </summary>
  331. /// <returns></returns>
  332. private bool ApplicationWantsToQuit()
  333. {
  334. //仅允许在应用程序初始化后退出。
  335. if (!started)
  336. return false;
  337. //延迟退出,clear up
  338. if (!quitStarted)
  339. {
  340. StartCoroutine("DelayedQuit");
  341. return false;
  342. }
  343. return true;
  344. }
  345. /// <summary>
  346. /// 恢复旧的WindowProc回调,然后退出。
  347. /// </summary>
  348. IEnumerator DelayedQuit()
  349. {
  350. // 重新设置旧的WindowProc回调,如果检测到WM_CLOSE,这将在新的回调本身中完成, 64位没问题,32位可能会造成闪退
  351. SetWindowLong(unityHWnd, GWLP_WNDPROC, oldWndProcPtr);
  352. yield return new WaitForEndOfFrame();
  353. quitStarted = true;
  354. Application.Quit();
  355. }
  356. }

项目源码:https://download.csdn.net/download/u014661152/15116978

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/100094
推荐阅读
相关标签
  

闽ICP备14008679号