做了几年Unity项目,我一直觉得垂直同步这个选项是典型的“看起来最简单、实际水最深”的设置之一。很多新手在Project Settings的Quality面板里找到一个VSync下拉框,随手选个Every VBlank就完事了,结果换到不同平台之后,一会儿觉得操作变肉,一会儿发现帧率显示30波动,一会儿又看到画面撕裂照旧。最近又在一个跨平台项目上专门花了两天时间梳理垂直同步相关的表现,决定把关于Unity垂直同步选项的注意点完整整理一遍。
这篇文章从底层原理讲到各平台的配置差异,再到我实际踩过的坑和排查思路,适合刚接触Unity不久、正在调帧率策略的同学,也适合那些已经用Unity好几年但始终没把Vsync和targetFrameRate关系搞清楚的人。看完之后你应该能根据自己项目类型直接选出一套相对靠谱的组合,而不是继续在几个选项之间来回试。
1. 先把垂直同步的原理搞清楚:VBlank、撕裂和帧率节奏
1.1 屏幕刷新这件事,跟GPU不是一个节奏
要理解Unity里的垂直同步,得先认识VBlank这个概念。现在的LCD显示器虽然不像老式CRT那样靠电子束一行行扫描,但依然保留了固定刷新周期的逻辑:显示面板以固定的频率刷新画面,比如60Hz就是每秒60次,每次刷新之间有一小段没有实际传输画面数据的时间窗口,这个窗口就是垂直消隐区间,也就是VBlank。
GPU和显示器是两条独立的流水线。GPU负责把一帧画面渲染到后备缓冲区里,显示器则按照自己的固定节奏从缓冲区读取画面并显示。问题来了,如果GPU渲染完一帧的时间恰好落在显示器刷新周期的中间,那么在这个瞬间,显示器可能上半部分读的是新一帧的数据,下半部分还是上一帧的数据,画面就会被横向撕开,这就是我们常说的画面撕裂。
你可以把显示器想象成投影幕布,GPU是坐在后面换幻灯片的人。如果放映机正在一帧一帧往下扫的时候,换幻灯片的人突然把新画面塞了进去,那观众看到的就是上半截新片、下半截旧片的拼贴。垂直同步的本质就是给换幻灯片的人上了个规矩:只能在放映机扫完一帧、准备下一帧的那个间隙里换。
1.2 帧率减半为什么那么常见
理解了上面的机制,就不难解释成年老问题了:Unity里开了垂直同步之后,为什么帧率经常会从60直接掉到30,而不是稳定在45或者50?
因为垂直同步采用的是“错过一班车,就等下一班”的机制。显示器在60Hz下每16.67毫秒释放一个VBlank信号,GPU如果在一帧渲染用了18毫秒,没赶上当前的VBlank,那它只能等下一个周期,也就是再等16.67毫秒。这一个本来只要18毫秒就能出的帧,实际会占用两个显示周期,也就是大约33毫秒,换算下来帧率就是30。
这个机制的结果就是帧率并不是按渲染耗时的比例平滑下降的,而是以刷新率整数分之一的阶梯来掉档。60Hz屏幕上,你得到的是60、30、20、15这样的档位。很多人看到帧率卡在30不动,第一反应是性能有问题,其实可能只是差那么两三毫秒没赶上VBlank导致整体掉了一档。
1.3 三个选项背后的取舍逻辑
Unity的Quality设置里,VSync Count这个下拉框通常有三个选项:Don't Sync、Every VBlank、Every Second VBlank。直译过来就是不开启、每帧同步、每两帧同步一次。
Every VBlank是最常规的垂直同步,GPU每渲染完一帧就在下一个VBlank把它送显,显示器和画面保持同步,没有撕裂,但帧率上限是屏幕刷新率。Every Second VBlank则是故意让GPU每次要等两个VBlank再送显,相当于强制把帧率控制在刷新率的一半,60Hz屏幕下就是30帧,这个选项在一些有意锁半速画面的场景里会用到,但更多时候是某些设备在特殊模式下的默认选择。
Don't Sync并不是让GPU无限火力全开,而是让GPU不再等待显示器的节奏,画面渲染完就尝试送显,此时帧率上限取决于GPU性能和场景复杂度。撕裂风险由显示器技术和管理策略去处理。这三者没有绝对的好与坏,取舍核心在于你愿意用输入延迟换画面完整性,还是用画面撕裂换更低的响应时间。
2. Unity里的垂直同步选项到底分布在哪
2.1 Quality Settings面板里的坑:你改的可能不是你当前用的质量等级
Unity的垂直同步入口很隐蔽,它在Edit -> Project Settings -> Quality里,选中左侧的质量等级后,右侧Rendering区域有一个VSync Count属性。但这里最大的坑在于,Unity的质量设置是按等级生效的,如果你项目里有Ultra、High、Medium、Low多个质量等级,而你只在Ultra上改了VSync Count,运行时当前使用的却是Low等级,那你的修改完全不会生效。
我见过不少人在论坛里问“为什么我开了垂直同步还是撕裂”,最后排查发现他改的是Ultra等级,手机上跑的却是Medium。这种错位的概率比想象中高得多,因为很多团队的项目从一开始就保留了多个默认质量等级,并没有做清理。
建议做法是如果项目不需要多档质量,直接删掉多余的等级,只保留一个,或者用代码统一接管。如果确实需要多档质量,那么你要确认运行时的QualitySettings.GetQualityLevel()返回的是哪个索引,能不能在设置界面里看到实时的质量等级切换,别靠猜。
2.2 用代码控制:QualitySettings.vSyncCount与Application.targetFrameRate
运行时垂直同步的核心API是QualitySettings.vSyncCount,支持三个值:0表示关闭垂直同步,1表示开启垂直同步,2表示每两帧同步一次。这个API跟面板里的下拉框是一一对应的,它修改的是运行时的质量设置,不会写回编辑器里保存的配置,所以不用担心运行时代码改动会污染项目设置。
Application.targetFrameRate则是另一个关键API,它设置的是一个软性的最大帧率目标,单位是帧每秒。两者的关系我下面会单独讲,这里先说一个容易混淆的点:vSyncCount和targetFrameRate并不是两个独立的调速器,在Unity内部,当vSyncCount大于0的时候,部分平台上targetFrameRate会被忽略或者被钳制到与垂直同步一致的档位,而不是你设置的任意数值。
2.3 targetFrameRate和vSyncCount的“谁听谁的”问题
这是这个主题里最核心的注意点,值得反复确认。按照Unity的官方说明和实际平台行为,大致可以归纳成几种情况:
当vSyncCount为0时,targetFrameRate决定帧率上限,Unity会尽量接近这个值,但不会保证完全等于,性能好的时候可能略高,性能差的时候会低于这个值。当vSyncCount为1时,画面的送显节奏完全由VBlank控制,帧率基本等于屏幕刷新率,此时targetFrameRate设置的值在很多桌面平台上不生效或者意义不大。当vSyncCount为2时,帧率被钳制到刷新率的一半,同样会忽略targetFrameRate的任意值。
移动端的行为略有不同。iOS上Unity对Application.targetFrameRate的支持比较直接,通过它就能把帧率限制到指定值,比如30或60。Android上则更依赖vSyncCount和平台的垂直同步策略,不同厂商的驱动行为也有差异,所以我很少在移动端同时依赖这两个值的默认叠加行为,而是主动明确设置。
2.4 不同渲染管线下的差异:内置、URP、HDRP都绕不开同一个API
前几年Unity推荐大家使用URP和HDRP之后,有个很常见的问题:我在URP项目里是不是要去Universal Render Pipeline Asset里找设置?这里明确解答一下,垂直同步的核心开关依然在QualitySettings和Quality面板里,渲染管线Asset本身不直接提供Vsync开关。
URP和HDRP确实提供了很多渲染性能相关的选项,比如HDRP里的帧率控制、动态分辨率开关,但它们更多是配合垂直同步策略去解决“帧时间不稳定”这个问题,而不是替代它。URP里通常还会建议开启动态分辨率来应对掉帧,但如果你的GPU渲染时间超过Vsync窗口,动态分辨率能自动降低渲染分辨率以保证稳定地在VBlank窗口内完成,这是两个层面上的配合。
内置渲染管线虽然没有那么多花哨的功能,但垂直同步行为反而是最清晰的一个:面板怎么设置就怎么跑,不引入额外变量。所以我的建议是,不管用什么渲染管线,先把vSyncCount和targetFrameRate的意图在代码里明确写清楚,再谈URPAsset里的其他优化,这样排查问题时才不会觉得到处都是“开关”。
3. 不同平台上的垂直同步:各家脾气大不一样
3.1 PC端:撕裂、输入延迟和高刷新率显示器的恩怨
PC端是垂直同步讨论最热烈的阵地,核心矛盾一直在“画面完整性”和“输入响应”之间。传统垂直同步开启后,GPU渲染完成的一帧会进入等待队列,显示器按VBlank周期拿数据,这样画面不会撕裂,但代价是输入到画面的延迟会增加。在60Hz下这个延迟大约是16毫秒到50毫秒不等,在竞技射击或MOBA这类对跟手性要求极高的游戏里,这点延迟会被感知得很明显。
所以很多PC竞技类游戏默认关闭垂直同步,让显卡尽量跑高帧率,画面撕裂交给G-Sync或FreeSync这类自适应刷新率显示器去解决。这类显示器可以让刷新率跟随GPU的实际输出帧率动态变化,这样即使帧率在100到144之间波动,也不会撕裂且延迟很低。
但这里有个很多人的误解:用了G-Sync或FreeSync显示器,Unity里的垂直同步是不是就可以随便关了?答案是通常建议开启垂直同步,同时把帧率上限设置在显示器最大刷新率略低一点。因为自适应刷新率技术只在范围内有效,超过上限后如果帧率比刷新率还高,很可能重新出现撕裂。很多显示器的官方指引也是这个逻辑:G-Sync配合Vsync开启,并把帧率限制到略低于最大刷新率。
3.2 移动端:锁30帧的正确姿势不是vSyncCount=2
移动端的情况是很多人栽跟头的地方。iOS和Android主流屏幕刷新率已经从60Hz逐渐迈向90Hz、120Hz,甚至部分设备还有自适应刷新率,但Unity垂直同步设置并不会因为你屏幕是120Hz就自动变成适合游戏的策略。
先讲最常见的需求:我想把帧率锁在30帧来省电、控制发热。很多人的第一反应是设置vSyncCount=2,因为前面不是说在60Hz下Every Second VBlank就是30帧嘛,这逻辑没错,但在不同设备上表现不一致。部分Android设备的垂直同步周期和屏幕刷新率绑定,vSyncCount=2确实会锁到30,但也有些设备对vSyncCount的处理是直接钳制帧率而不严格等两个VBlank,最终效果就变成一会30一会60的抖动,体验更差。
在移动端我更推荐这套组合:vSyncCount设为0,不给垂直同步强行介入,然后用Application.targetFrameRate显式设置目标帧率。iOS上这个API非常可靠,Android上虽然兼容性没那么完美,但大多数现代设备也会遵守这个上限。很多AR或休闲类项目就是靠这一套实现锁30和锁60的切换。
3.3 主机平台:固定刷新率下必须遵守规则
主机平台的情况比较特殊,因为主机通常连接电视,而电视的刷新率基本是固定的60Hz或120Hz,不存在PC上那种显示器自适应刷新率的多样性。所以主机游戏几乎毫无悬念地应该开启垂直同步,否则撕裂会在电视上非常明显。
但主机项目的难点在于如何保证始终稳定在目标帧率。比如一个想跑60帧的动作游戏,如果某些复杂场景渲染时间波动超过了16.67毫秒,开了垂直同步就必然掉到30帧,那种突然从60掉到30的卡顿感比全程稳定30帧还难受。所以主机团队通常会把动态分辨率、画面细节分级和垂直同步组合起来:场景负载高时先降分辨率,再降阴影质量,确保渲染时间始终在VBlank窗口内,尽量避免触发垂直同步的掉档。
我做主机项目时的默认前提是:垂直同步固定开,把精力放在帧时间的稳定性控制上,而不是纠结要不要关Vsync来提帧率。
3.4 编辑器里的垂直同步:测试结果不能太当真
还有一个很容易被忽略的注意点:Unity编辑器里运行时的垂直同步行为和打包后的真机并不完全一致。编辑器窗口本身由操作系统的窗口系统管理,Unity编辑器内Game视图的显示帧率还受编辑器设置中的Game View VSync影响,同时编辑器里的性能开销会叠加到你的渲染时间上。
所以如果你在编辑器里看到一个40多帧的数值,先别急着怀疑垂直同步设置出了问题,因为编辑器里的时间测量本身就包含了很多非游戏逻辑的开销。我测试垂直同步相关功能时,通常只看Build到本机或真机上跑出来的数据,编辑器里的帧率只是做一个非常粗略的参考。
4. 实操配置组合:从轻量休闲到硬核竞技的常用方案
4.1 四套我常用的配置组合
根据项目类型不同,我习惯把这套配置写成一个简单的初始化函数放到启动流程里,避免设置散落在各个界面里。这里分享四套比较常见的组合。
轻量休闲游戏或超休闲游戏,目标是省电和控制发热,推荐vSyncCount=0配合targetFrameRate=30。画面本身不追求极致的动态表现,锁30帧能明显降低发热,且30帧在静态或轻度交互场景下可以接受。
卡牌、解谜、模拟经营这类低动态游戏,或者剧情演出比较多的游戏,推荐vSyncCount=1配合targetFrameRate=60。开Vsync保证画面不撕裂,60Hz下画面完整且观感平滑,哪怕偶发掉帧也不至于撕裂感。
竞技类游戏,比如对战、射击、赛车,推荐vSyncCount=0配合targetFrameRate=0或144这类高目标。帧率优先,目标是把延迟压到最低,撕裂问题尽量交给显示器的G-Sync/FreeSync处理。
大型动作冒险或开放世界游戏,推荐vSyncCount=1配合动态分辨率系统。优先保证渲染时间稳定在VBlank窗口内,再考虑要不要调整targetFrameRate,因为这类游戏对画面完整性的要求高于对极致响应速度的要求。
4.2 那个常见的困惑:targetFrameRate到底填多少
很多人在同一个工程里看到别人写targetFrameRate=60,后来又改成targetFrameRate=-1,于是产生了疑惑。简单说明一下,targetFrameRate=-1表示不限制目标帧率,让游戏以尽可能高的帧率运行,同时Unity会在有垂直同步时把它当作“不限制”。如果你的设备屏幕是120Hz且开了Vsync,那么帧率可能直接就是120,而不是你要的60。
所以在设置targetFrameRate时,先想清楚你到底想要的是“帧率上限”还是“固定帧率”。如果只是想防止游戏在菜单界面疯狂烧电,设一个稍高的值比如60就足够了;如果你是希望稳定锁30帧,不能只设targetFrameRate=30就完事,还要确认vSyncCount=0并且设备平台没有自己搞事的垂直同步策略。
我自己的习惯是:锁帧需求一律写清楚目标数值,并且在日志里打印当前帧率和屏幕刷新率,万一设备没有按预期工作,至少能看出来是谁没听话。
4.3 动态调整帧率目标:温度、负载、分辨率三位一体
到了项目后期,静态的帧率策略往往不够用,因为一台手机玩十分钟之后发热降频,和刚打开时的性能表现完全不是一个水平。动态调整的核心思路是实时监控帧时间,一旦发现连续若干帧超过期望的VBlank窗口,就按优先级逐步降低负载。
优先级通常是:降低动态分辨率目标,降低阴影质量和粒子数量,最后才是降低帧率目标。这是一个降级阶梯,而不是一上来就把60帧砍到30帧。反过来,如果帧时间非常充裕,再一步步把分辨率拉回来。
在这个过程中垂直同步的设置保持不变,因为我们要的是画面完整性,动态分辨率就是为了保证渲染时间能压进VBlank窗口而存在的。很多新手把垂直同步和动态分辨率当成两个互相独立的优化手段,其实它们是配合关系。
5. 常见问题排查与避坑实录
5.1 开了垂直同步还是看见画面撕裂
这个现象绝大多数情况下是踩了质量等级错位的坑,要么你改的不是运行时使用的质量等级,要么运行时某个脚本又主动把QualitySettings.vSyncCount改回了0。排查思路很简单:运行时日志打印一次QualitySettings.vSyncCount的值,再看目标设备实际刷新率是否跟你预期一致,如果是高刷屏还开着Every VBlank,那帧率是120而不是60,肉眼上的撕裂感可能来自其他插帧或动态分辨率逻辑。
还有一种情况是开了Vsync但Windows系统桌面本身没有开启全屏优化,或者无边框窗口模式下系统合成器又做了一层处理,导致帧节奏出现微小的错位。这种情况在编辑器窗口里最容易复现,Build成独占全屏后通常会消失。
5.2 我想锁30帧,但Unity给我跑60
iOS上如果只设置了targetFrameRate=30但没管vSyncCount,而vSyncCount恰好是1,就会出现这种尴尬现象。因为iOS的垂直同步会把送显节奏锁定到屏幕刷新率上,60Hz屏幕下自然就是60帧,你的30帧目标根本没有机会被执行。正确的顺序是先设vSyncCount=0,再设targetFrameRate=30。
Android上则复杂一些,某些驱动会把vSyncCount=0的帧率上限默认当成刷新率处理,你再设置targetFrameRate=30时它会最低取一个刷新率的公约数,比如30或20,所以你会发现帧率是30但画面流畅度很奇怪。遇到这种设备不要死磕,可以把targetFrameRate先设成-1,观察设备默认行为,再决定是30还是60。
5.3 帧率测试软件显示的值和真实体验不一致
这是个经常被误判的注意点。帧率测试软件或者性能HUD上的平均帧数,是统计时间窗口内的平均值,在开启垂直同步后帧率会呈现方块状跳变,比如交替的60和30,平均帧率可能是45。但实际体验是你看到的画面是大部分时间60、卡顿瞬间突然30,完全不是45的顺滑度,所以用平均帧率衡量Vsync下的流畅度是失真且有害的。
更有效的方法是查看帧时间曲线,也就是Frame Time的计算单位是毫秒而不是FPS。60帧对应的帧时间是16.7毫秒,30帧对应的是33.3毫秒。通过帧时间曲线你能直观看到每次掉档是从16.7跳到33.3,这比在FPS数字上看波动更容易定位问题。
5.4 移动端莫名耗电发热,怀疑是垂直同步的锅
移动端许多项目默认继承了PC时代的习惯,把vSyncCount开到1,然后在120Hz高刷屏上整个界面都跑在120帧,这比60帧的耗电多不少。这种情况你误以为是渲染负载太高,去调阴影和粒子,结果发现收益甚微,回头看一眼帧率才发现是120,问题根源就是垂直同步在高刷新率屏幕上的“默认完全同步”策略。
处理方案依旧是回到核心原则:如果你不追求高刷体验,vSyncCount设0,再用targetFrameRate明确锁60或者30。如果你确实希望游戏在高刷屏上跑满120帧,那就别锁,但要做好对应的发热监控和性能预算分配。
5.5 Daily Driver:别忘了真机与模拟器的差异
很多人在Android模拟器里测试垂直同步设置,发现无论怎么改都没有预期的效果,就开始怀疑代码错了。这里有个背景:Android模拟器跑在PC上,它读取的屏幕刷新率、Vsync行为都跟真实手机相差甚远,甚至模拟器的窗口本身就被操作系统的合成器影响。垂直同步相关的调试,一定要在真机上验证,模拟器只能用来确认功能流程是否正确,不能用来确认帧率表现。
iOS模拟器也类似,它并不真正渲染到iOS设备的显示硬件上,GPU调用路径和真机完全不同,哪怕日志里看到了预期的vSyncCount,实际帧率也可能并不可信。我的团队现在规定:凡是涉及帧率、垂直同步、电量的性能任务,一律用真机数据验收,模拟器只用来跑业务逻辑。
6. 一点点个人体会
垂直同步选项之所以值得单独拉出来写,是因为它牵涉到画面撕裂、帧率稳定性、输入延迟、功耗发热、平台行为差异这好几条线,任何一条线单独拿出来都能写一篇分析。我在实际项目里吃过最大的亏,就是太把编辑器里的表现当回事,导致上线后才发现移动端高刷屏上的发热问题。
后面我把所有垂直同步相关配置统一到一个初始化和动态调整的脚本里,固定套路是“先关Vsync,再明确设targetFrameRate,最后按设备刷新率验证结果”,遇到问题就把这两个值打印出来,第一轮排查基本能解决九成的情况。最后分享一个小技巧:如果你不确定某个平台对垂直同步支持到什么程度,写一个简单的测试场景,背景用纯色铺满,控制台实时显示当前vSyncCount、屏幕刷新率、平均帧时间和最近一帧的帧时间,然后以不同设置组合跑真机,数据一对比,哪个组合靠谱一目了然。这个方法帮我在三种不同的Android设备上快速锁定了推荐配置,希望也能给你省下几个晚上的排查时间。