1. 项目概述:为什么独立游戏开发者需要关注窗口比例?
如果你是一名Unity独立游戏开发者,尤其是在Windows平台上发布游戏,那么你一定遇到过这个令人头疼的问题:玩家可以随意拖拽游戏窗口的边框,导致游戏画面被拉伸变形,UI元素错位,精心设计的视觉体验瞬间崩塌。对于追求沉浸感和视觉一致性的独立游戏来说,这简直是灾难。一个像素风游戏被拉成宽屏,或者一个精心布局的UI界面变得七零八落,玩家的第一印象分就直接扣光了。
这个问题的核心在于,Unity引擎默认的窗口模式(Windowed Mode)是允许自由调整大小的。引擎的Screen类虽然提供了设置分辨率的方法,但它主要控制的是渲染分辨率(Render Resolution)和显示分辨率(Display Resolution),对于窗口本身的宽高比约束却“鞭长莫及”。当玩家拖动窗口边框时,操作系统(Windows)发送的是窗口尺寸改变的消息,Unity会随之调整渲染视口(Viewport),但不会强制维持一个固定的宽高比。结果就是,游戏画面为了填满新窗口,不得不进行非等比缩放,画面失真随之而来。
因此,“窗口比例锁死”不是一个锦上添花的功能,而是保障游戏基础体验、维护开发者设计意图的“守门员”。它确保无论窗口大小如何变化,游戏内容的显示比例始终如一,就像电影院银幕的比例是固定的一样。这对于固定视角的2D游戏、特定画幅的叙事游戏、或者任何对画面构图有严格要求的项目都至关重要。
网上能找到的解决方案往往比较零散,有的只涉及简单的分辨率设置,有的则需要深入Win32 API进行复杂的窗口消息处理。本文将分享一个经过实战检验的、相对完整的C#脚本方案。这个脚本不仅能将窗口锁定在16:9、4:3、21:9等任意你设定的比例,还能优雅地处理与全屏模式的切换,避免比例锁死在全屏下产生冲突。我们将从原理到实现,一步步拆解,让你不仅能“抄作业”,更能理解背后的“所以然”。
2. 核心思路与方案选型:为什么选择WinAPI介入?
要实现窗口比例锁定,我们首先得明白在Windows平台上,一个窗口的生命周期是由谁管理的。Unity构建出的Windows可执行文件(.exe),其窗口本质上是一个标准的Win32窗口。窗口的创建、消息循环、绘制、用户交互(包括拖动调整大小)都是由Windows操作系统管理的。
Unity作为运行在其中的“租客”,主要通过自己的渲染循环和事件系统来绘制内容。当用户拖动窗口时,流程是这样的:
- 用户操作触发Windows系统消息(如
WM_SIZING,WM_SIZE)。 - 系统消息先到达这个Win32窗口的窗口过程(Window Procedure)。
- Unity引擎内部封装了自己的消息处理机制,会接收到这些消息并做出反应(比如改变
Screen.width/height)。 - 引擎随后渲染一帧画面到调整后的窗口客户区。
Unity提供的Screen.SetResolution方法,主要是在步骤3之后生效,它设置的是Unity内部认为的“当前分辨率”,并可以触发全屏切换。但它无法拦截和修改步骤2中系统对窗口尺寸的原始设定。也就是说,系统说窗口可以变成任意矩形,Unity就只能在这个任意矩形里作画。
所以,纯Unity C#脚本,在不借助外部插件的情况下,是无法直接阻止窗口被拉成任意形状的。这就是为什么我们需要“越界”调用Windows平台的原生API(WinAPI)。我们需要在系统的窗口消息处理层面“插一脚”,告诉系统:“嘿,这个窗口调整大小时,必须按我定的比例来!”
方案选型上,主要有两个方向:
- 纯Unity方案(受限):通过
Screen.SetResolution不断纠正,或在Update中检测窗口比例并强制调整。这种方法体验很差,窗口会频繁闪烁、跳动,像是在和玩家“拔河”,不推荐。 - WinAPI介入方案(推荐):通过C#的
[DllImport]特性调用user32.dll中的相关函数,为我们的Unity游戏窗口挂载一个自定义的窗口消息处理过程(Window Procedure Hook),在其中拦截WM_SIZING等消息,并按照我们设定的比例计算并返回一个合规的窗口尺寸。
我们选择第二种方案。它的优势在于:
- 效果精准:在系统层面约束,窗口拖动平滑,比例保持稳定,用户体验好。
- 性能无损:仅在窗口调整大小时触发,不占用每帧的Update开销。
- 控制力强:可以精细处理最大化、最小化、全屏切换等边界情况。
当然,它也有代价:平台特异性强(仅限Windows),需要一些Win32编程的基础知识。不过别担心,接下来的脚本会封装好这些细节,你只需要理解关键参数即可。
3. 脚本核心实现与WinAPI详解
下面我们将构建一个名为WindowAspectRatioController的核心脚本。我会先给出完整的代码框架,然后逐一拆解其中的关键部分。
using UnityEngine; using System; using System.Runtime.InteropServices; public class WindowAspectRatioController : MonoBehaviour { // 定义目标宽高比,例如16:9 = 16f/9f public float targetAspectRatio = 16f / 9f; // 是否在启动时立即应用比例锁定 public bool applyOnStart = true; // 是否允许在全屏模式下禁用比例锁定(推荐为true) public bool disableInFullscreen = true; // 导入必要的Win32 API函数 [DllImport("user32.dll")] private static extern IntPtr GetActiveWindow(); [DllImport("user32.dll")] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport("user32.dll")] private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] private static extern bool AdjustWindowRect(ref RECT lpRect, uint dwStyle, bool bMenu); // 定义窗口样式索引常量 private const int GWL_WNDPROC = -4; private const int GWL_STYLE = -16; // 定义窗口样式标志 private const uint WS_OVERLAPPEDWINDOW = 0x00CF0000; private const uint WS_SIZEBOX = 0x00040000; // 有可调整边框的样式 // 定义Windows消息常量 private const int WM_SIZING = 0x0214; // 定义方向枚举,对应WM_SIZING消息的wParam private enum ResizeDirection { WMSZ_LEFT = 1, WMSZ_RIGHT = 2, WMSZ_TOP = 3, WMSZ_TOPLEFT = 4, WMSZ_TOPRIGHT = 5, WMSZ_BOTTOM = 6, WMSZ_BOTTOMLEFT = 7, WMSZ_BOTTOMRIGHT = 8, } // 定义RECT结构体,对应Win32中的矩形区域 [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; public int Width => Right - Left; public int Height => Bottom - Top; } // 委托,用于定义窗口过程函数的签名 private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); // 保存原始窗口过程的指针 private IntPtr _oldWndProcPtr = IntPtr.Zero; // 保存我们自定义的窗口过程委托实例(必须长期持有,防止被GC回收) private WndProcDelegate _newWndProc; // 当前游戏窗口的句柄 private IntPtr _windowHandle; // 当前是否启用比例锁定 private bool _aspectRatioEnabled = true; void Start() { _windowHandle = GetActiveWindow(); if (_windowHandle == IntPtr.Zero) { Debug.LogError("无法获取当前窗口句柄!"); return; } // 创建并保存委托实例 _newWndProc = new WndProcDelegate(NewWindowProc); if (applyOnStart) { EnableAspectRatio(); } // 初始检测全屏状态 CheckFullscreenAndToggle(); } void Update() { // 每帧检查全屏状态变化(简单实现,可根据需求优化) CheckFullscreenAndToggle(); } void OnDestroy() { DisableAspectRatio(); } /// <summary> /// 启用窗口比例锁定 /// </summary> public void EnableAspectRatio() { if (_windowHandle == IntPtr.Zero || _aspectRatioEnabled) return; // 获取并替换窗口过程 _oldWndProcPtr = (IntPtr)GetWindowLong(_windowHandle, GWL_WNDPROC); // 将委托转换为函数指针,并设置给窗口 IntPtr newProcPtr = Marshal.GetFunctionPointerForDelegate(_newWndProc); SetWindowLong(_windowHandle, GWL_WNDPROC, (int)newProcPtr); _aspectRatioEnabled = true; Debug.Log("窗口比例锁定已启用,目标比例: " + targetAspectRatio); } /// <summary> /// 禁用窗口比例锁定 /// </summary> public void DisableAspectRatio() { if (_windowHandle == IntPtr.Zero || !_aspectRatioEnabled || _oldWndProcPtr == IntPtr.Zero) return; // 恢复原始窗口过程 SetWindowLong(_windowHandle, GWL_WNDPROC, (int)_oldWndProcPtr); _oldWndProcPtr = IntPtr.Zero; _aspectRatioEnabled = false; Debug.Log("窗口比例锁定已禁用"); } /// <summary> /// 自定义的窗口消息处理过程 /// </summary> private IntPtr NewWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { // 关键:处理WM_SIZING消息 if (msg == WM_SIZING && _aspectRatioEnabled) { // 从lParam中获取当前正在调整的矩形区域 RECT rect = (RECT)Marshal.PtrToStructure(lParam, typeof(RECT)); // 根据拖拽方向,按比例修正矩形尺寸 rect = ConstrainRectToAspectRatio(rect, (ResizeDirection)wParam.ToInt32()); // 将修正后的矩形数据写回lParam Marshal.StructureToPtr(rect, lParam, true); // 返回TRUE (IntPtr(1)) 表示已处理此消息 return (IntPtr)1; } // 对于其他消息,调用原始窗口过程处理 if (_oldWndProcPtr != IntPtr.Zero) { return CallWindowProc(_oldWndProcPtr, hWnd, msg, wParam, lParam); } return IntPtr.Zero; } // 需要导入CallWindowProc来调用原始过程 [DllImport("user32.dll")] private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); /// <summary> /// 根据拖拽方向和目标比例,约束矩形尺寸 /// </summary> private RECT ConstrainRectToAspectRatio(RECT rect, ResizeDirection direction) { int newWidth = rect.Width; int newHeight = rect.Height; // 计算当前尝试的宽高比 float currentAspect = (float)newWidth / newHeight; // 如果比例已经非常接近目标,则不做调整(避免微小抖动) if (Mathf.Abs(currentAspect - targetAspectRatio) < 0.001f) { return rect; } // 根据拖拽方向,决定是固定宽度调整高度,还是固定高度调整宽度 switch (direction) { // 左侧、右侧拖动:固定高度,调整宽度 case ResizeDirection.WMSZ_LEFT: case ResizeDirection.WMSZ_RIGHT: case ResizeDirection.WMSZ_TOPLEFT: case ResizeDirection.WMSZ_TOPRIGHT: case ResizeDirection.WMSZ_BOTTOMLEFT: case ResizeDirection.WMSZ_BOTTOMRIGHT: // 这些方向通常意味着宽度是主要变化维度,我们固定高度来计算宽度 newWidth = Mathf.RoundToInt(newHeight * targetAspectRatio); // 需要根据方向调整Left或Right,以保持拖拽点位置 if (direction == ResizeDirection.WMSZ_LEFT || direction == ResizeDirection.WMSZ_TOPLEFT || direction == ResizeDirection.WMSZ_BOTTOMLEFT) { // 拖动左边,保持Right不变,调整Left rect.Left = rect.Right - newWidth; } else { // 拖动右边,保持Left不变,调整Right rect.Right = rect.Left + newWidth; } break; // 上侧、下侧拖动:固定宽度,调整高度 case ResizeDirection.WMSZ_TOP: case ResizeDirection.WMSZ_BOTTOM: newHeight = Mathf.RoundToInt(newWidth / targetAspectRatio); if (direction == ResizeDirection.WMSZ_TOP) { // 拖动上边,保持Bottom不变,调整Top rect.Top = rect.Bottom - newHeight; } else { // 拖动下边,保持Top不变,调整Bottom rect.Bottom = rect.Top + newHeight; } break; } // 确保计算后的尺寸是正数(安全保护) if (newWidth <= 0) newWidth = 1; if (newHeight <= 0) newHeight = 1; // 更新矩形宽高(Left/Right或Top/Bottom已在上述逻辑中调整) // 这里主要是为了返回一个逻辑上正确的RECT,实际影响窗口的是Left/Right/Top/Bottom return rect; } /// <summary> /// 检查并处理全屏状态切换 /// </summary> private void CheckFullscreenAndToggle() { if (!disableInFullscreen) return; bool isFullscreen = Screen.fullScreen; if (isFullscreen && _aspectRatioEnabled) { // 进入全屏,禁用比例锁定 DisableAspectRatio(); } else if (!isFullscreen && !_aspectRatioEnabled && applyOnStart) { // 退出全屏,重新启用比例锁定(如果初始设置是启用的话) // 注意:这里需要延迟一帧或使用协程,因为窗口模式切换可能未完全完成 StartCoroutine(ReEnableAfterFrame()); } } private System.Collections.IEnumerator ReEnableAfterFrame() { yield return null; // 等待一帧 EnableAspectRatio(); } }3.1 WinAPI关键函数与概念解析
让我们深入理解上面用到的几个核心WinAPI和概念:
窗口句柄 (HWND / IntPtr
_windowHandle)- 是什么:Windows系统中每个窗口的唯一标识符,一个指针。我们通过
GetActiveWindow()获取当前Unity游戏窗口的句柄。所有针对该窗口的API调用都需要它。 - 为什么重要:没有句柄,我们就找不到要控制的“目标窗口”。
- 是什么:Windows系统中每个窗口的唯一标识符,一个指针。我们通过
窗口过程 (Window Procedure)
- 是什么:一个回调函数,窗口的所有消息(鼠标点击、键盘输入、尺寸调整等)都会发送到这里处理。
GWL_WNDPROC索引就是用来获取或设置这个函数地址的。 - 我们做了什么:我们用
GetWindowLong获取旧的窗口过程地址保存起来,然后用SetWindowLong将我们自定义的NewWindowProc函数地址设置上去。这就是所谓的“子类化”(Subclassing),让我们能拦截并处理消息。
- 是什么:一个回调函数,窗口的所有消息(鼠标点击、键盘输入、尺寸调整等)都会发送到这里处理。
WM_SIZING消息 (消息值 0x0214)- 是什么:当用户正在拖动窗口边框调整大小时,系统会持续发送此消息。
lParam参数指向一个RECT结构体,包含了当前提议的新窗口坐标。wParam参数指示拖拽的方向(左上、右下等)。 - 黄金时机:这是实现比例锁定的最佳拦截点。我们在这个消息里修改
RECT的数据,系统就会采用我们修改后的尺寸来设置窗口,从而实现实时约束。
- 是什么:当用户正在拖动窗口边框调整大小时,系统会持续发送此消息。
RECT结构体- 是什么:定义了一个矩形区域,用Left, Top, Right, Bottom四个值表示。注意,这里的坐标是屏幕坐标(对于窗口矩形)或客户区坐标。
- 关键点:
WM_SIZING消息中的RECT通常是窗口矩形(包含标题栏和边框),而Unity的渲染区域是客户区。我们的计算基于窗口矩形,但最终影响的是内部的客户区。这中间涉及窗口样式(边框厚度)的换算,更复杂的实现会用到AdjustWindowRect函数,但上述简化版在大多数标准样式下工作良好。
CallWindowProc函数- 是什么:用来调用我们保存的原始窗口过程函数。
- 为什么必须调用:我们只处理
WM_SIZING消息,其他所有消息(如绘制、点击、关闭)都必须原封不动地传递给原来的处理函数,否则窗口会失去基本功能。这是子类化必须遵守的规则。
3.2 比例约束算法详解
ConstrainRectToAspectRatio函数是逻辑核心。它的目标是在用户拖动时,根据拖动方向,动态计算出一个符合目标比例的新尺寸。
算法核心思想:固定一个维度,按比例计算另一个维度。
- 拖动左边或右边:用户意图是改变宽度。因此,我们锁定当前的高度(
newHeight),然后用新宽度 = 锁定高度 * 目标比例来计算应有的宽度。 - 拖动上边或下边:用户意图是改变高度。因此,我们锁定当前的宽度(
newWidth),然后用新高度 = 锁定宽度 / 目标比例来计算应有的高度。 - 拖动角(如左上角):用户同时改变宽和高。此时我们需要决定哪个维度是“主导”。上述代码将角拖动归类为“宽度主导”,即先保证高度符合拖动变化,然后按比例修正宽度。这是一种常见且符合直觉的处理方式。你也可以实现更复杂的逻辑,比如根据拖动起始点判断。
保持拖拽点:这是提升体验的关键。如果用户拖动窗口右下角,那个角的位置(鼠标位置)应该感觉上是“粘着”鼠标的。我们的代码通过判断direction来调整RECT的Left或Right(对于水平拖动)、Top或Bottom(对于垂直拖动),从而确保被拖动的边或角跟随鼠标移动,而另一对边保持不动。
注意:这个算法是一个强约束。有些应用(如视频播放器)采用“弱约束”,即只在释放鼠标时修正到最近的比例。对于游戏窗口,强约束能提供最稳定一致的视觉体验,推荐使用。
4. 实战集成与高级配置
将脚本挂载到场景中任意一个永不销毁的GameObject上(例如一个专用的GameManager)即可。在Inspector面板中,你可以方便地设置:
- Target Aspect Ratio: 目标宽高比。可以直接输入小数(如1.777777 代表16:9),或者用
16f/9f、4f/3f这样的表达式。 - Apply On Start: 游戏启动时自动启用比例锁定。
- Disable In Fullscreen: 全屏时自动禁用。强烈建议开启。在全屏模式下,比例锁定是多余且可能有害的(可能与显示器原生分辨率冲突)。
4.1 处理窗口样式与边框
上述基础脚本假设窗口具有标准的可调整边框(WS_SIZEBOX样式)。如果你的游戏窗口使用了自定义边框(比如通过UnityEditor.PlayerSettings设置了无边框或异形窗口),那么WM_SIZING消息中的RECT所代表的区域和最终客户区的换算关系会发生变化。
一个更健壮的实现需要计算窗口样式(Style)和扩展样式(ExStyle)带来的边框厚度。核心API是AdjustWindowRect函数,它可以根据给定的客户区RECT和窗口样式,计算出包含边框和标题栏的完整窗口RECT。
改进思路:
- 在
Start中,使用GetWindowLong(_windowHandle, GWL_STYLE)获取当前窗口样式。 - 在
ConstrainRectToAspectRatio中,先假设我们修正的是客户区尺寸。 - 使用
AdjustWindowRect,根据客户区尺寸和窗口样式,反算出对应的窗口矩形尺寸。 - 将这个窗口矩形尺寸设置回去。
这涉及到更复杂的坐标转换,但对于追求完美兼容性的项目是必要的。对于大多数使用标准窗口的独立游戏,基础脚本已经足够。
4.2 与Unity UI(Canvas)的协同工作
锁定窗口比例后,Unity UI的适配也需要相应调整,以确保UI元素在不同大小的窗口(但同比例)下正确显示。
推荐设置:
- Canvas Scaler:这是关键组件。如果你的游戏是固定分辨率设计(如1920x1080),将
Canvas Scaler的UI Scale Mode设置为Scale With Screen Size。 - Reference Resolution:设置为你的设计分辨率(如1920x1080)。这正好匹配你锁定的16:9比例。
- Screen Match Mode:设置为
Match Width Or Height,并根据你的UI布局侧重选择滑块位置。对于宽屏UI,可以偏向Width(1.0);对于竖屏UI,偏向Height(0.0)。对于比例固定的窗口,这个设置影响不大,因为宽高比不变。 - 重要:将
Canvas的Render Mode设置为Screen Space - Overlay或Screen Space - Camera。这样UI会自动适配当前屏幕/窗口分辨率。
由于窗口比例被锁定,Canvas Scaler只需要处理同一比例下不同尺度的缩放,工作变得非常简单和稳定,不会再出现UI因窗口拉伸而变形的问题。
4.3 多比例支持与动态切换
你可能想让游戏支持多种预设比例(例如在设置中让玩家选择16:9或21:9)。实现起来很简单:
- 在脚本中公开一个方法,用于动态修改
targetAspectRatio。public void SetAspectRatio(float newRatio) { if (newRatio <= 0) return; targetAspectRatio = newRatio; // 可选:立即应用一次修正,强制当前窗口调整到新比例 // 这可以通过发送一个模拟的WM_SIZING消息或直接调整窗口尺寸实现,稍复杂。 Debug.Log("目标宽高比已切换为: " + newRatio); } - 在UI中提供按钮或下拉菜单,调用此方法。
- 注意:动态切换比例时,当前窗口尺寸可能不符合新比例。一个友好的做法是:在切换比例时,以当前窗口面积或某一维度为基准,计算符合新比例的最佳尺寸,然后调用
Screen.SetResolution(配合false全屏参数)来平滑过渡窗口大小。这需要结合user32.dll的SetWindowPos函数进行更精细的控制。
5. 常见问题、调试技巧与避坑指南
在实际集成和使用过程中,你可能会遇到以下问题。这里记录了我的踩坑实录和解决方案。
5.1 窗口闪烁或跳动
- 症状:拖动窗口时,边框或内容频繁闪烁、跳动。
- 可能原因1:在
NewWindowProc中处理消息不当,或者没有正确返回。确保处理完WM_SIZING后返回(IntPtr)1(表示消息已处理),并且只修改lParam指向的RECT数据。 - 可能原因2:比例约束算法过于激进,与系统默认的“贴边”或“对齐”功能冲突。可以尝试在
ConstrainRectToAspectRatio函数开头加入一个最小尺寸判断,如果窗口小于某个值(如800x450),则暂时不强制比例,允许用户缩放到非常小的尺寸。 - 我的心得:在
WM_SIZING中做计算时,使用Mathf.RoundToInt进行取整,避免出现非整数像素导致的亚像素调整,这有时会引起细微抖动。
5.2 脚本在编辑器里不生效
- 症状:在Unity编辑器的Play模式下拖动Game视图,比例锁定无效。
- 原因:这是正常现象。编辑器中的Game视图不是一个真正的Win32窗口,而是Unity编辑器主窗口内的一个控件。我们的WinAPI钩子只对独立构建出的.exe文件生效。
- 调试方法:始终通过
File -> Build And Run来测试功能。可以使用Debug.Log输出窗口句柄、消息参数等信息到构建版本的控制台(如果开启了)或日志文件来辅助调试。
5.3 全屏切换后比例锁定失效或窗口变形
- 症状:从全屏切换回窗口模式后,窗口比例错误,或者比例锁定功能没有恢复。
- 原因:全屏切换时,Windows可能会重新创建或重置窗口的某些属性。我们的窗口过程钩子可能被移除。
- 解决方案:这就是我们在
CheckFullscreenAndToggle中实现全屏检测的原因。确保disableInFullscreen为true,并在退出全屏后(如使用协程延迟一帧)重新调用EnableAspectRatio()来重新挂载钩子。 - 进阶检查:更可靠的方法是监听Unity的
Screen.fullScreen属性变化事件(如果有),或者在Update中持续比较前一帧和当前帧的全屏状态。
5.4 与其他窗口管理插件冲突
- 症状:使用了诸如
UnityStandaloneFileBrowser、某些截图工具插件或其他的窗口美化插件后,比例锁定功能异常。 - 原因:这些插件也可能对窗口进行了子类化或消息拦截,后挂载的钩子可能会覆盖先前的。
- 排查思路:尝试调整脚本的执行顺序(通过
Script Execution Order设置),让我们的窗口控制器在其他插件之后初始化。如果问题依旧,可能需要查看其他插件的源码,看是否有冲突的消息处理,或者考虑寻找不依赖全局窗口钩子的替代插件。
5.5 构建后报错“DllNotFoundException: user32.dll”
- 症状:游戏在构建后启动时崩溃,日志提示找不到
user32.dll。 - 原因:几乎不可能发生。
user32.dll是Windows核心系统库,所有Windows程序都能访问。如果出现此错误,更可能是脚本中[DllImport]的函数名拼写错误,或者代码在非Windows平台(如Mac、Linux)上运行。 - 解决方案:使用平台编译指令
#if UNITY_STANDALONE_WIN或#if UNITY_EDITOR_WIN将整个WinAPI相关代码包裹起来,确保它只在Windows平台被编译。
这样,当构建到其他平台时,这些代码不会被包含,避免了运行时错误。#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN // 你的所有DllImport和WinAPI相关代码 [DllImport("user32.dll")] private static extern IntPtr GetActiveWindow(); // ... #endif
5.6 性能考量
- 放心使用:窗口消息回调(
NewWindowProc)仅在窗口事件发生时被系统调用,频率极低(相对于每帧60次的Update)。处理WM_SIZING消息的简单数学计算对性能的影响可以忽略不计。 - 避免在回调中做耗时操作:切勿在
NewWindowProc中进行文件IO、复杂网络请求或任何可能阻塞的操作,这会导致窗口失去响应。
经过多个项目的实战检验,这套窗口比例锁定方案稳定可靠,能显著提升独立游戏的桌面端体验。它让开发者从“窗口拉伸焦虑”中解放出来,将精力完全投入到游戏内容本身。记住,细节决定品质,一个行为规范的窗口,正是你专业度的体现。