
1. 项目概述与整体设计思路1.1 雪天效果要解决的核心问题做2D游戏开发的时候天气系统往往是最后才被想起的东西。角色、关卡、战斗、UI这些核心模块占掉了绝大部分开发时间等回头补天气效果时才发现它比想象中复杂得多。我在这个项目里做雪天效果一开始只打算飘几片白色的粒子结果越做越深最后牵扯到渲染顺序、场景色调、地面覆盖、性能预算甚至影响了整个关卡的光照氛围。先说说这个项目的基本情况。这是一款横版2D动作游戏整体美术风格偏暗黑系场景大多是废墟、森林、村庄遗迹这类压抑的环境。正是因为暗黑风格的底色雪这个元素反而成了视觉上的强对比——大面积冷白色落在深色场景里能给玩家非常强烈的环境冲击。暗黑类素材通常低饱和、低明度整体调子压得很沉雪天效果如果做得太干净、太白就会和场景脱节如果做得太脏、太灰玩家又会觉得这不是雪而是灰烬。这个度非常难拿捏也是我在这个DEV LOG里最想分享的东西。雪天效果要解决的核心问题可以拆成四块第一飘落的雪片本身也就是粒子的运动逻辑和视觉表现第二场景对雪的响应比如地面、屋顶、栏杆上积累的雪层第三整体氛围的统一包括环境光的色温、背景的雾效、声音的配合第四性能2D游戏别看画面简单粒子数量一旦上去低端设备照样卡成幻灯片。1.2 技术路线选型为什么不用现成插件很多朋友第一反应是去Asset Store找个天气插件一拖一拽完事。我以前也是这么干的但这次我决定自己写一套。原因很简单现成的插件大部分面向通用场景它们设计的粒子形态、颜色、运动逻辑都偏向于写实风格很难配合暗黑手绘风格的2D素材。而且插件往往附带大量你用不到的功能渲染管线和你的2D项目不一定匹配后期想调细节反而被插件框架限制住。自己做的好处是每一个参数都握在手里调出来的是和项目美术风格完全契合的效果。比如我可以在粒子上直接叠加character light的影响让雪片在靠近角色时稍微亮一点这种细节是通用插件很难做到的。当然自己做的代价是踩坑这个我在后面几节会详细讲。我的技术栈是Unity 2022 LTS URP 2D Renderer用C#写粒子管理逻辑结合Shader Graph做雪片材质。之所以不直接用Unity自带的Particle System是因为我需要非常精确地控制粒子的生成区域、速度衰减和视差偏移而Particle System在这些参数上虽然都能调但对于我这种需要频繁改动逻辑的迭代场景用代码控制反而更灵活。需要说明的是这个选择不一定适合所有项目如果你的场景比较简单Particle System完全够用没必要非要自己造轮子。2. 粒子系统的基本架构2.1 发射器布局与发射参数我用了三个发射器而不是一个。第一个负责远景雪粒子小、速度慢、透明度低模拟远处飘落的雪幕第二个负责中景雪这是玩家视线的主要焦点粒子适中运动稍快偶尔有几片大的雪花增加层次感第三个负责前景雪粒子大而模糊贴着镜头掠过产生强烈的空间感。三个发射器的视差系数分别设置在0.3、0.6和1.2左右。Unity的2D项目里我通过Canvas或自定义的ParallaxLayer脚本控制它们跟随摄像机的位移比例。不同视差会让玩家产生明显的纵深错觉——远景雪移动慢像是在很远的地方前景雪移动快像是就在眼前。这个手法在动作游戏里很常用几层背景错开速度画面立刻就有立体感了。发射器的具体参数我放在下面方便你对照参考参数远景雪中景雪前景雪粒子数量300-500500-80080-150发射区域屏幕宽x1.2屏幕宽x1.5屏幕宽x1.8初始速度0.5-1.51.5-3.03.0-6.0粒子尺寸4-8像素8-16像素20-40像素透明度0.2-0.40.5-0.80.6-0.9视差系数0.30.61.2发射区域的宽度大于屏幕宽度是为了保证角色走到屏幕边缘时不会出现雪花突然消失的断裂感。宽度系数1.2到1.8这个范围是我试出来的底线——小于1.2边缘会露馅大于1.8粒子浪费在半空没人看得到的地方性能白白损耗。2.2 粒子运动风场与重力模拟雪的飘落和雨完全不同。雨水基本是垂直下落的最多受点风力偏折但雪因为重量轻、表面积大受到空气阻力和湍流的影响非常明显。它的运动轨迹应该是一种不规则的左右摇摆叠加在稳定的下落速度上像一片纸在空中打旋。我用了一个简单的公式x方向偏移 sin(时间 * 频率 相位) * 振幅再叠加一个Noise函数产生不规则扰动。振幅和频率按粒子尺寸做随机——大雪花振幅小、频率低看起来沉稳小雪花振幅大、频率高飘得很碎。整体下落速度控制在每秒30到90像素之间这个速度对2D游戏来说是合适的太快像雨太慢像悬浮。垂直方向除了重力的匀速下落还要考虑风的强度。我做了个全局WindStrength变量玩家在关卡中走到风口区域时这个值会从0平滑升到0.7所有粒子的x方向偏移量乘以这个系数就能模拟风速突增的场景效果。风的过渡用Lerp做大概2秒内完成玩家能感觉到环境的变化但不会觉得突兀。2.3 粒子生命周期与循环每个粒子从生成到消亡的完整路径是这样的在发射区域顶部随机位置生成初始y坐标在摄像机视野上方20-50像素处保证玩家看不到粒子凭空出现的那个瞬间然后粒子以当前的风速和重力参数开始运动当y坐标低于视野底部以下30像素时回收该粒子重新在顶部生成。我用的是对象池而不是每帧直接New一个粒子对象。这个优化对2D游戏很重要因为粒子数量动不动就是上千反复new和销毁会触发大量GC游戏跑几分钟就会出现明显的卡顿。对象池的逻辑很简单预先分配一个长度为最大粒子数的数组每个粒子是一个结构体包含位置、速度、尺寸、透明度、相位这些字段。需要新粒子时从池里取粒子沉底后把它标记为“空闲”下一次发射直接复用。刚开始我用的List存储粒子每帧遍历更新粒子数到800以上后CPU占用明显上升。后来改成数组加循环下标遍历成本降了一半。原理很简单数组内存连续对CPU缓存友好List在扩容和遍历时会有间接寻址开销。这个优化虽然不起眼但做2D游戏粒子系统每帧能占掉3-4ms的CPU时间压下来对整体帧时间很有帮助。3. 雪片渲染与素材处理3.1 雪片素材的选择与制作暗黑类游戏的雪片素材和明亮卡通风格的完全不同不能直接去素材站下载一张白点带模糊边缘的贴图就用了。暗黑场景本身是低明度的如果雪片贴图太干净会像贴纸一样浮在画面上如果直接降低贴图的透明度又会显得脏。我的做法是先在一张512x512的透明背景上用喷枪画出几个大小不一的柔边圆点然后叠加少量杂色纹理最后做高斯模糊处理。关键的一步是把贴图的明度从纯白色压到200-230左右的灰度而不是纯255。这个灰度值在暗背景上依然显得亮但不会刺眼能和其他场景元素自然融合。另一个细节是雪花形状。现实中雪花是六角形的但在2D游戏里过于真实的细节在运动状态下反而看不清而且制作成本高。我最终用了三种形状的粒子圆形小点90%、轻微拉长的椭圆形8%、带一点六角星轮廓的纹理2%。圆形小点是主力椭圆形用来模拟风中的雪六角星纹理只在近景偶尔出现给玩家一种“这是雪花”的心理暗示。3.2 暗黑风格下的调色与混合模式雪片材质的渲染模式我踩过一个大坑。默认的Particle材质用Alpha Blend就能有不错的半透明效果但放在暗黑场景里雪片覆盖在深色背景上会显得发灰原因是Alpha Blend的本质是透明度混合遮盖不住背景里的暗色纹理雪片透出来的背景噪点会降低它的纯度。我测试了两种方案Additive混合和Custom混合。Additive的效果非常亮雪片和背景叠加后会变亮视觉冲击力强但代价是雪花和雪花重叠的位置会过曝成一片死白失去细节。Custom混合则允许我自定义SrcBlend和DstBlend最终我选了SrcAlpha配OneMinusSrcAlpha再叠加一层自定义的雪白颜色参数让粒子在保证柔和边缘的同时中心区域有较高的覆盖度。调色上除了贴图灰度我还要在材质里加一个TintColor参数。暗黑场景的夜晚关卡TintColor设为淡蓝色(0.7, 0.8, 1.0)黄昏关卡则偏向冷淡白色(1.0, 0.9, 0.8)浓雾区域甚至会用偏黑色0.3, 0.35, 0.4配高透明度模拟黑雪暴的感觉。这个参数不仅影响粒子我还在全局用同样的值去调整场景的光照和地面雪层颜色保证整个画面的色调统一。3.3 景深模糊与前景遮挡2D游戏缺少真实的景深效果但可以通过模糊模拟。我做了两个效果一是中景和远景的粒子本身就用缩小尺寸配合降低透明度模拟远处的雪雾二是前景粒子贴图额外做一次更强的模糊让镜头前的大雪花真的有一种“贴在镜头上、焦点之外”的感觉。前景遮挡这个细节很少有人提。2D游戏里角色是横向移动视角是侧视前景的雪花如果没有遮挡逻辑会直接穿过角色的身体看起来非常穿帮。我的解决办法是给前景雪粒子加一个Z排序把它们的渲染层级固定在角色之前、背景之后。Unity里通过Setting SortingOrder实现背景0中景雪10地面雪层20角色30前景雪50。渲染顺序一旦对了雪花在角色身前飘过空间关系立刻清晰。这个排序方案在URP 2D Renderer下是有效的但要注意如果你的项目里混用了SpriteRenderer和Canvas UI它们之间还有一套渲染优先级要处理。我的方案是把所有天气粒子都用SpriteRenderer渲染不经过UI Canvas避免UI的批次被粒子的动态变化打断。4. 地面积雪与场景响应4.1 地面覆盖方案只下雪不积雪玩家会觉得这是个贴图效果而不是一个真实的天气系统。我花了不少时间做地面积雪覆盖。最直接的方式是给地面铺一层半透明白色Sprite放在地形之上、角色之下。但这层积雪如果完全不动会显得很死板而且在玩家走过的地方雪应该被踩掉或变薄。我做了一个基于碰撞检测的“雪迹”系统地面雪层其实由多个方块组成每个方块包含一个SpriteRenderer和一个BoxCollider2D。角色或其他物体移动时如果与之发生碰撞就把对应方块的白色覆盖物Alpha降为0露出地面原色。为了让痕迹看起来更自然我用了三个层次的地块完全覆盖的雪层Alpha0.85踩过一层后的残雪Alpha0.4被完全清除的区域Alpha0。这种做法的不是之处是地块数量不能太多。一个1000x600像素的地面如果按64x64像素分成地块大约是240个左右如果处理得精细些分到32x32就是960个每个都有碰撞体和SpriteRenderer对DrawCall和物理计算都有压力。我最终用64x64的粒度配合一次简单的接触检测把碰撞检测频率控制在每0.1秒一次而不是每帧一次成本降了不少。4.2 屋顶、栏杆与轮廓积雪积雪只在地面上铺一层在俯视或平视游戏中看起来还凑合但2D横版游戏大多是侧视场景建筑屋顶、树枝、围栏、墓碑这些凸出物才最能体现雪的堆叠感。屋顶上的积雪不是地面那层白色Sprite能覆盖的它需要跟随屋顶的形状。我的方案是给每个需要积雪的地形物件手动添加一个“雪层”子物体它复制了父物体的Sprite形状但高度偏移20-30像素颜色改为白色透明度0.7-0.9。这样做的好处是无脑且效果稳定缺点是工作量大每个场景物件都要手动配置。对于程序化生成的关卡这个方案不太现实但对于手工设计的场景可控性反而更高。栏杆和树枝这类细长物件的堆积更麻烦直接叠加一个偏移的白色sprite会导致边缘穿帮。我的做法是把它们的sprite拆成两半下沿和上沿分别做一条白色的渐变边缘再用黑色半透明贴图模拟背光面的阴影。这个细节虽然简单但对于暗黑风格来说极其重要——雪会堆积在物体的上沿而下沿因为被主体阴影遮蔽不应该有太多积雪。4.3 边缘过渡与交互消融雪层和地面原色之间如果是一刀切的硬边观感会很差就像直接盖了块白布。我用了两层过渡第一雪层贴图自身的边缘是柔边的这要求在制作雪层Sprite时不要把Alpha画得太满边缘部分用30-50%的透明渐变第二在雪层和地面交接的位置额外画一条淡灰色阴影模拟雪的厚度感。交互消融这个功能我是后来才加的。原本只是踩过会留脚印后来发现角色攻击地面或投掷物落地时也应该溅起一点雪或者露出一点地面。我实现了一个简易的“冲击涡”逻辑当检测到一个冲击事件时以该位置为圆心、半径50像素内的雪地块全部降Alpha并轻微向外位移形成被气浪吹开的效果。粒子系统同时在该位置生成一小簇飞散的雪屑让人感觉真的有东西砸在雪堆上。写代码的时候最容易漏掉的是“融雪之后的复原”。我的设计是雪被踩掉后不会恢复直到玩家退出当前关卡重新进入雪层才会刷新。如果不做恢复长时间游玩后所有地面都会变得光秃秃天气系统等于失效了。如果你希望雪能下个不停、边下边恢复就需要在雪层上叠加一个缓慢的Alpha回升逻辑我暂时没做留给后续版本迭代。5. 氛围感的统一处理5.1 环境光的冷色调调整雪天不只是下雪整个环境的光照都会变色。暗黑风格本来偏阴沉冷峻雪天又会再往冷色推一步。我用Unity的2D Light系统在场景里加了一个全局的Point Light和Directional Light混合把色温调向淡蓝Color从(1, 0.9, 0.8)调成(0.8, 0.9, 1.0)强度降低10%-20%模拟阴天光线被雪云遮挡的效果。过渡方式要注意雪不是瞬间下大的。我做了全局WeatherController管理Normal、LightSnow、HeavySnow、Blizzard四种状态的切换状态切换时色调、粒子发射速率、遮蔽强度一起做Lerp。这样玩家从无雪区域走进雪山区时能明显感受到环境渐变而不是被切了一个生硬的开关。色调统一这个环节最容易翻车。如果你只改了粒子和地面雪层但场景里的光源还是原来的暖黄色那雪地会被照得发黄冷白和暖黄互相打架整个画面会变得“脏”。因此光照和粒子颜色的对应关系必须同步调先定主色调所有白色物体的TintColor统一引用同一个全局变量再微调色相偏移才不会翻车。5.2 背景视差与天空盒雪天的大背景我分成了四层最远的天空层、中层远山、近处建筑轮廓、最近的地面层。天空层是一张缓慢流动的低分辨率云胶扰动画色调整体偏暗灰蓝配合全局光线变暗远山层是一张静态图叠加了一层半透明白色渐变模拟雪雾遮蔽建筑轮廓和地面层的雪则和场景实体一一对应。背景层在雪天效果下也要调整。我加了一个SnowFog层——一张覆盖全屏的渐变透明贴图通过Layer调整放在背景和场景之间。它的作用是让远景的细节快速模糊产生风雪天的能见度下降感。这个图层用Additive混合模式配以画面边缘透明度更低、中央透明度略高的中心聚焦效果很能烘托暗黑肃杀的氛围。视差参数我在1.1里已经列了这里补充一个容易忽视的点背景移动速度和信号源的关系。摄像机移动越快视差层越要快速错开但当角色高速奔跑时背景层会跟着快速平移如果贴图本身没有足够的边缘延展就会看到明显的图像边界。我解决的方案是把背景贴图做成可平铺的四方连续再让边缘偏移与摄像机速度挂钩用Lerp做个平滑尾巴避免生硬。5.3 声音与微细节视觉上全做到位了声音还不能出戏。雪天效果的声音设计包含三块环境底噪、落雪声、风啸声。环境底噪用的是低通滤波后的白噪声循环音量很小类似安静的“嘶嘶”声代表雪花落在地面和空气中的摩擦落雪声是打断的白噪音片段频率上做随机延迟模拟雪花偶尔砸在护栏或屋檐上的“扑扑”声风啸声则是当风速参数变化时用一个带通滤波器的风噪声循环音调和音量随风速值变化。这些声音我在Audio Mixer里单独建了一个Weather的Group方便整体调音量和做回声效果。运行时WeatherController会和音效参数绑定把WindStrength变量映射到风噪的Cutoff频率和Volume渐变时长和视觉的渐变保持同步不能让视觉已经暴雪了声音还在微风。微细节方面我还做了雪粒落在角色轮廓上的一瞬间高光闪烁、雪地行走时脚底反馈的轻微白色泡沫、武器挥舞时雪花被气流推开的局部扰动。这些效果单独拿出来都很小但叠加起来就构成了一个让人信服的“正在下雪”的空间。做天气系统的体会是最终让玩家感到身临其境的往往不是最大的那个雪片而是这些埋在角落里的细节。6. 性能优化与常见问题排查6.1 粒子数量与纹理合批性能是所有天气系统的阿喀琉斯之踵。雪天效果最容易踩的坑就是粒子数飙升。粒子一多哪怕每个粒子的计算只有几百次浮点运算乘上一千个一帧的CPU时间就很可观。我给自己定的预算是中景和远景粒子总数不超过1000个前景粒子不超过150个超过就强制丢弃新生成请求而不是让系统卡死。为了榨干性能我把所有纯色圆形雪片合成一张图集共享同一材质让Unity的SRP Batcher和Dynamic Batching尽量合并DrawCall。纹理合批对于不透明材质效果好但半透明材质有很多限制况且粒子运动会让UV和顶点信息每帧改变动态合批的缓存很容易失效。实测下来使用Unity粒子的内置MaterialPropertyBlock和预分配Mesh顶点数组批量渲染1000个粒子大约能压到2-3次DrawCall如果自定义渲染管线还能进一步合并成一次。帧率方面我在普通PC和一台低端笔记本上做了对比测试。PC上1500粒子的雪天场景稳定60帧低端笔记本掉到45帧左右。我把粒子上限锁定在900并把Post-processing里Bloom关了之后低端机能稳定在55帧以上。这个取舍是值得的毕竟面部阴影、轮廓光这些视觉效果比多200片雪花重要得多。6.2 低配置设备的适配低端机适配不只是减少粒子数量。我在WeatherController里做了一个QualityLevel检测根据设备GPU跑分动态调整三个参数粒子数量上限、雪片纹理尺寸、地面雪层地块密度。低端机把纹理从512降到256地面地块粒度从64x64放宽到96x96粒子上限压到600。中端机和高端机的差异主要在视觉密度上逻辑不变。2D游戏用URP时还要小心2D Renderer下Post-processing的开销。Bloom和Color Grading非常适合雪天场景但低端机上这些全开帧率会掉很多。我的建议是至少要保留Color Grading或Lut调整的整体色调Bloom可以降级成不做屏幕模糊只通过粒子本身大小和透明度模拟光晕。我在低端机上就是这么处理的画面依然能保持暗黑冷调但帧率稳了不少。对象池还有一个细节我预先分配的是最大粒子数的数组最大是1500。如果你设置的上限比较低预分配还是会给你开1500的数组占的内存不好回收。优化方法是让对象池根据QualityLevel重新初始化低端机只分配600个结构体避免白占内存。6.3 常见Bug与修复记录开发过程中遇到几个比较典型的问题这里列出来大概率你会碰到第一个粒子在相机旋转时错乱。2D相机有时会带一点旋转来表现震动或倾斜但我用世界坐标计算粒子方向时粒子会跟着相机旋转产生滑移。解决方案是把粒子的生成和更新都放在相机的LocalSpace下再做一次空间转换。改完以后相机怎么转雪花都稳如老狗地往下飘。第二个地面雪层在角色跳跃时穿透。我一直用BoxCollider2D做碰撞检测但角色在空中时BoxCollider的底部和雪层Union会产生不自然的挤压。后来把雪层的碰撞改成EdgeCollider2D只保留一个严格的地表面接触线雪迹的生成只发生在接触点问题就解决了。第三个半透明雪片叠出黑色描边。这是半透明排序的经典问题两个半透明Sprite交叉时后绘制的会挡住先绘制的叠出很丑的黑边尤其是深色场景下特别明显。我通过强制设置每个粒子的CustomData存储深度偏移并且在材质里开启AlphaToMask把半泛白区域稍微裁掉一点视觉上就顺眼多了。第四个粒子贴在屏幕上不动了。排查了很久发现是摄像机明确设置了跟随角色但我的粒子视差脚本里同步的是摄像机的位置而摄像机有时会用LocalPosition和世界坐标混用导致偏移重复叠加。统一用世界坐标后问题消失排查花了我整整一下午。第五个雪天从远处看地面雪层白得发亮。暗黑风格场景里突然冒一块明度85%以上的雪地整个画面的暗色调立刻被破坏。原因是我在雪层材质里给了太高基础亮度又没有考虑场景的全局雾和光衰减。给雪层加了统一的TintColor变量和粒子色调联动把亮度压到65%画面整体就统一了。6.4 实测数据参考最后贴一组我在项目当前场景里的实测数据方便你有直观概念。测试机分别是台式机RTX 3060 32G内存和一台五六年前的老笔记本Intel HD 620核显。分辨率统一为1920x1080垂直同步关闭项目台式机老笔记本粒子数远景中景前景1350600DrawCall晴朗场景3232DrawCall雪天场景4738平均帧率6052峰值CPU耗时粒子更新2.1ms1.3ms峰值渲染耗时4.6ms4.2ms老笔记本上粒子数降低后画面没有台式机那么密但整体氛围依然成立。我自己的经验是天气系统启动后不能只看平均帧率还要看最低帧——那种突然卡一下的瞬间往往是最影响体验的。所以我有一个DailyDriver检查清单每改一次天气参数就在老笔记本上做30秒的“螺旋走位放技能”测试确保在地图最复杂区域也不会掉到40帧以下。后续扩展方向这个雪天系统目前已经能支撑游戏的主要关卡氛围需求后续有几个方向我很想做。一个是“雪停之后的痕迹保留”——现在雪会一直下没有设计“雪逐渐变小到停”的状态如果后续剧情需要出现雪后初晴应该把WeatherController的Blizzard和Clear之间再加一个Flurry状态并让地面雪层的Alpha随着时间缓慢降低。另外就是交互系统可以继续加深比如玩家用火属性技能时能融化附近的雪层或者动物NPC走过会留下专属的脚印轨迹这些沉浸感细节做出来非常有成就感。我个人的建议是如果你是第一次做2D天气系统不要急着把风、暴雪、落雪声、雪迹全部塞进去先把飘雪、地面覆盖、色调统一这三样做好就已经能唬住大部分玩家了。剩下的效果等核心稳定之后再慢慢加性能优化也才有地方下手。