尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity图片循环滚动全攻略:5种实战技巧与性能优化

Unity图片循环滚动全攻略:5种实战技巧与性能优化 1. 项目概述与核心价值最近在做一个横版跑酷游戏里面有个经典的背景层无限滚动的需求。一开始我直接用了两个SpriteRenderer来回切换感觉挺简单但真做起来发现要兼顾性能、适配不同分辨率、以及应对UI层图片滚动时坑还真不少。SpriteRenderer和RawImage一个用于场景中的精灵一个用于UI层虽然都能实现图片循环滚动但背后的逻辑、优化点和适用场景截然不同。网上很多教程只讲其一或者代码给得不清不楚参数调起来一头雾水。这篇文章我就把自己在项目中实际用过的、验证过的五种图片循环滚动技巧从原理到代码掰开揉碎了讲给你听。无论是想实现2D游戏里连绵不绝的远景山脉还是UI界面中动态流动的背景你都能在这里找到可以直接“抄作业”的方案。每种方法我都会说清楚为什么选它参数怎么算以及我踩过哪些坑。代码都是完整的、可运行的你复制到自己的项目里改改参数就能用。2. 核心思路与方案选型为什么是这五种在Unity里让图片动起来核心思路无非是每帧修改图片的位移Transform.position或者材质球的纹理偏移Material.mainTextureOffset。但“循环滚动”这个需求关键在于“无缝衔接”。你不能让图片滚着滚着突然出现一片空白或者接缝处对不上那可就穿帮了。基于这个核心我筛选出了五种实战价值最高的技巧它们覆盖了从基础到进阶从场景对象到UI组件的全场景需求。2.1 方案全景图与选型逻辑首先我们得明确SpriteRenderer和RawImage的根本区别这决定了方案的起点。SpriteRenderer属于GameObject存在于3D/2D场景空间。它的位置由Transform控制滚动通常通过修改Transform.position来实现。它更适用于游戏世界中的背景、远景层。RawImage属于UI组件存在于Canvas的屏幕空间。它的位置和UV坐标由RectTransform和Material控制。滚动通常通过修改材质球的_MainTex_ST缩放偏移或直接操作uvRect来实现。它更适用于UI界面上的动态背景、特效遮罩。基于这两大载体五种技巧的选型逻辑如下双SpriteRenderer物理位移法这是最经典、最直观的2D游戏背景滚动方案。用两个相同的精灵并排摆放一起向左移动。当第一个精灵完全移出屏幕左侧时立刻将它“瞬移”到第二个精灵的右侧如此循环。它的优势是逻辑简单符合直觉非常适合刚入门的新手理解无限滚动的本质。缺点是如果图片拼接处有细微差异比如云朵的形状可能会在“瞬移”的瞬间被眼尖的玩家察觉。单SpriteRenderer材质UV偏移法这个方法跳出了“移动物体”的思维转而操作图片本身的纹理坐标。它只使用一个SpriteRenderer但为其创建一个特殊的材质球通常是Unlit/Transparent变体。在Update中不断累加材质的mainTextureOffset.x假设横向滚动。纹理坐标是0到1循环的所以偏移量超过1后会自动回绕从而实现无缝循环。这个方法性能极佳只改一个材质参数并且天生无缝因为纹理采样本身就是循环的。但它需要一点Shader基础来理解UV并且对图片有要求图片的左右边缘必须在内容上是连续的即“可平铺”纹理比如砖墙、草地、水面波纹。双RawImage的UI滚动法当你的滚动背景需要放在UI层时比如游戏内的菜单动态背景RawImage是唯一选择。它的思路和双SpriteRenderer类似但操作的是RectTransform的anchoredPosition。由于UI系统基于锚点我们需要更精细地计算屏幕宽度和像素单位。这个方法的关键在于如何根据Canvas的渲染模式Screen Space - Overlay/Camera/World来正确换算位置。单RawImage的UV Rect动画法这是UI层滚动最优雅高效的方法。RawImage组件自带一个uvRect属性它定义了截取原始纹理的哪一个矩形区域进行显示。通过每帧修改uvRect的x值同样假设横向滚动让这个“显示窗口”在纹理上滑动当窗口滑出纹理右边界时由于纹理Wrap模式设为Repeat它会自动从左边界重新开始实现无缝滚动。这个方法只需要一个RawImage性能好且同样要求纹理本身是可无缝衔接的。Shader Graph可视化编程法这是一个面向未来、更高级的解决方案。如果你使用的是Unity的可编程渲染管线URP那么可以完全不用写C#代码通过Shader Graph节点连线来实现滚动逻辑。创建一个Unlit Shader Graph用Time节点驱动一个Tiling And Offset节点再将结果连到Base Color上赋给SpriteRenderer或RawImage的材质。这种方法将滚动逻辑从CPU转移到了GPU性能理论上更优并且调整参数非常直观可视化面板。它特别适合需要复杂动态效果如多层不同速度的视差滚动的情况但要求项目使用URP/HDRP管线并有一定Shader Graph基础。选型速查表技巧名称核心载体实现原理优点缺点适用场景双SpriteRenderer位移法SpriteRenderer移动两个物体循环重置位置逻辑简单直观无需特殊纹理需要两个对象可能有瞬时跳变2D游戏背景对无缝要求不高单SpriteRenderer UV法SpriteRenderer修改材质球的纹理偏移(UV)性能好天生无缝循环需要可平铺纹理需创建材质远景层山、云、海双RawImage UI位移法RawImage移动两个UI元素的锚点位置适用于所有UI层动态背景计算需考虑Canvas模式需两个UI元素UI菜单、界面动态背景单RawImage UV Rect法RawImage修改RawImage组件的uvRect属性UI层最佳性能实现优雅需要可平铺纹理UI动态背景、特效遮罩层Shader Graph法任意渲染器在Shader中用时间驱动UV偏移GPU驱动性能高可调性强需URP/HDRP有学习成本复杂视差效果追求高性能注意选择方案时第一个要问自己的问题就是“这个滚动的图片用在哪儿”。是游戏场景里跟着相机跑的背景还是固定在屏幕上的UI装饰这个问题决定了你是用SpriteRenderer还是RawImage。第二个问题是“我的图片资源左右边缘能无缝对接吗”。如果能优先考虑UV偏移方案单SpriteRenderer或单RawImage如果不能那就只能用双对象位移方案了。3. 核心细节解析与实操要点在动手写代码之前有几个共通的细节必须吃透否则即使代码跑起来了效果也可能惨不忍睹。3.1 纹理导入设置一切无缝滚动的基础如果你的方案涉及到UV偏移也就是方法2、4、5那么纹理的导入设置是重中之重。在Project面板选中你的图片在Inspector中需要关注这两点Wrap Mode环绕模式必须设置为Repeat。这是告诉GPU当纹理坐标UV超过1的时候不要截断而是重复平铺这张图片。这是实现无缝循环的底层保障。默认设置往往是Clamp钳制超出的部分会拉伸边缘像素导致滚动时出现难看的色条。Max Size最大尺寸确保图片尺寸是2的幂次方如25651210242048。虽然不是绝对强制但非2的幂次方纹理在GPU上可能无法使用Repeat环绕模式或者会带来性能损耗和内存浪费。Unity可能会在内部将其缩放至最近的2的幂次方。养成使用2的幂次方图片的习惯是专业开发者的基本素养。3.2 “无缝”图片的制作技巧所谓“可平铺纹理”就是图片的左边缘和右边缘在内容上能够完美衔接。对于自然景物如天空、远山在美术制作时就需要有意识地将笔触在画布两侧处理得一致。有一个取巧的办法在Photoshop等软件中使用“偏移”滤镜Filter - Other - Offset将图片平移一半然后处理中间因位移而产生的生硬接缝这样就能快速得到一张左右可无缝衔接的图片。3.3 滚动速度与帧率无关的处理在Update函数中直接使用transform.position Vector3.left * speed * Time.deltaTime来移动这是基础做法。但这里有个细节Time.deltaTime是上一帧的时间间隔。在性能波动导致帧率不稳时这种基于时间的移动能保证速度恒定而不是帧数越高滚得越快。所有涉及随时间变化的操作都必须乘以Time.deltaTime这是保证游戏体验一致性的黄金法则。对于UV偏移方案计算偏移量的公式通常是offset scrollSpeed * Time.deltaTime;。这里scrollSpeed的单位可以理解为“每秒纹理重复的次数”。比如scrollSpeed 0.5f就表示纹理每秒向左滚动半个自身宽度。3.4 屏幕尺寸与精灵尺寸的换算这在双对象位移法中至关重要。你需要知道你的精灵Sprite在游戏世界中的实际宽度。假设你的精灵图片像素是512x256而Sprite的Pixels Per UnitPPU是100那么这个精灵在世界中的宽度就是512 / 100 5.12个单位。如何判断精灵“完全移出屏幕”你需要获取相机Camera的视口范围。对于一个正交相机2D游戏常用其可见高度的一半是camera.orthographicSize可见宽度则取决于屏幕宽高比camera.orthographicSize * camera.aspect。那么精灵需要移动的“重置位置”距离就是其自身的宽度。在双精灵方案中两个精灵初始应紧密相邻间距为零。4. 五种实战技巧完整实现与代码解析下面我将为每一种技巧提供可直接运行的完整C#脚本并附上关键参数说明和挂载步骤。4.1 技巧一双SpriteRenderer物理位移法这个脚本挂载在一个空物体上然后将两个作为背景的精灵拖拽给它即可。using UnityEngine; public class DualSpriteScroller : MonoBehaviour { [Header(滚动设置)] [Tooltip(滚动速度世界单位/秒)] public float scrollSpeed 2.0f; [Tooltip(滚动的方向)] public Vector2 direction Vector2.left; // 默认向左滚 [Header(精灵引用)] [Tooltip(第一个背景精灵左)] public Transform backgroundSprite01; [Tooltip(第二个背景精灵右)] public Transform backgroundSprite02; private float _spriteWidth; // 单个精灵的世界宽度 void Start() { // 计算精灵宽度。这里假设两个精灵大小一致。 // 通过获取SpriteRenderer的bounds来得到世界空间的大小更准确。 if (backgroundSprite01.TryGetComponentSpriteRenderer(out var sr)) { _spriteWidth sr.bounds.size.x; } else { Debug.LogError(BackgroundSprite01上没有找到SpriteRenderer组件); enabled false; // 禁用脚本 return; } // 初始化第二个精灵的位置紧挨着第一个精灵的右侧 if (backgroundSprite02 ! null) { Vector3 pos backgroundSprite01.position; pos.x _spriteWidth; backgroundSprite02.position pos; } } void Update() { // 1. 同时移动两个精灵 Vector3 moveDelta (Vector3)direction * scrollSpeed * Time.deltaTime; backgroundSprite01.Translate(moveDelta); if (backgroundSprite02 ! null) { backgroundSprite02.Translate(moveDelta); } // 2. 检查第一个精灵是否完全移出屏幕左侧假设向左滚动 // 我们需要一个参考点来判断“移出屏幕”这里使用主相机 Camera mainCam Camera.main; if (mainCam null) return; // 将精灵的位置转换到视口坐标Viewport Space范围是(0,0)到(1,1) Vector3 spriteViewportPos mainCam.WorldToViewportPoint(backgroundSprite01.position); // 计算精灵右边界在视口中的X坐标近似估算精确计算需考虑中心点和宽度 // 更稳健的做法是计算精灵bounds的视口范围这里用简单估算 float spriteRightEdgeInViewport spriteViewportPos.x (_spriteWidth / 2f) / (mainCam.orthographicSize * mainCam.aspect * 2f); // 如果精灵的右边界已经移出视口左边缘viewport x 0 if (spriteRightEdgeInViewport 0) { // 3. 将第一个精灵重置到第二个精灵的右侧 Vector3 resetPosition backgroundSprite02.position; resetPosition.x _spriteWidth; backgroundSprite01.position resetPosition; // 4. 交换两个精灵的引用这样下次就检查另一个 Transform temp backgroundSprite01; backgroundSprite01 backgroundSprite02; backgroundSprite02 temp; } } }实操要点与避坑精灵宽度计算我使用了sr.bounds.size.x而不是根据PPU计算因为bounds直接给出了在世界空间中的轴对齐包围盒大小它已经考虑了Sprite的缩放Scale结果更准确可靠。移出屏幕判断代码中使用了WorldToViewportPoint将世界坐标转换到视口坐标。视口坐标中(0,0)是屏幕左下角(1,1)是右上角。判断0即移出左边界。这是一个比直接计算相机边界更通用的方法也适用于透视相机虽然2D背景一般用正交。重置与交换重置位置时是将其放到当前第二个精灵的右侧而不是固定位置。交换引用是为了让两个精灵的角色“被检查者”和“参考者”在每次重置后互换逻辑上更清晰。性能每帧进行两次Translate操作和一次视口坐标转换对性能影响微乎其微。确保你的背景精灵图集合并得当Draw Call不会因此增加。4.2 技巧二单SpriteRenderer材质UV偏移法首先你需要为这个精灵创建一个材质。在Project中右键 - Create - Material命名为“ScrollingBackground”。在Inspector中将Shader改为“Unlit/Transparent”或者URP下的“Unlit”并开启透明度。然后将你的无缝纹理拖拽给这个材质。using UnityEngine; public class SingleSpriteUVScroller : MonoBehaviour { [Header(滚动设置)] [Tooltip(纹理横向滚动速度单位纹理宽度/秒)] public float scrollSpeedX 0.5f; [Tooltip(纹理纵向滚动速度)] public float scrollSpeedY 0f; private Material _material; private Vector2 _currentOffset Vector2.zero; void Start() { SpriteRenderer sr GetComponentSpriteRenderer(); if (sr null) { Debug.LogError(此脚本需要挂载在带有SpriteRenderer的物体上); enabled false; return; } _material sr.material; // 注意这里获取的是material会创建实例 // 如果你不希望每个对象都有独立的材质实例节省内存可以使用 sr.sharedMaterial // 但修改sharedMaterial会影响所有使用该材质的对象。 _currentOffset _material.mainTextureOffset; } void Update() { // 累积偏移量 _currentOffset.x scrollSpeedX * Time.deltaTime; _currentOffset.y scrollSpeedY * Time.deltaTime; // 应用偏移量到材质 _material.mainTextureOffset _currentOffset; // 注意mainTextureOffset的x/y值可以大于1纹理会根据Wrap Mode自动重复。 } void OnDestroy() { // 如果是通过.material创建的实例材质在对象销毁时最好手动销毁材质防止内存泄漏 // 但谨慎操作确保没有其他地方引用。简单的做法是如果材质名包含“(Instance)”则销毁。 if (_material ! null _material.name.Contains((Instance))) { Destroy(_material); } } }实操要点与避坑材质实例化sr.material会在运行时创建一个该材质的独立实例Clone对这个实例的修改不会影响其他使用同一材质的对象。这通常是我们想要的。但如果你有成千上万个相同的滚动背景这会产生大量材质实例增加内存和Draw Call。此时可以考虑使用MaterialPropertyBlock来动态修改属性而不创建实例但这稍微复杂一些。纹理环绕模式再次强调材质所使用的纹理Texture的Wrap Mode必须设置为Repeat否则当mainTextureOffset超过1时不会循环而是被钳制。性能这是性能最好的方案之一每帧只更新一个Vector2材质属性开销极小。重置偏移量如果需要从某个特定偏移量开始可以在Start中设置_material.mainTextureOffset new Vector2(startX, startY);。4.3 技巧三双RawImage的UI滚动法这个脚本需要挂载在Canvas下的一个空物体上并拖入两个作为背景的RawImage子物体。确保Canvas的渲染模式适合你的需求通常Overlay或Screen Space - Camera。using UnityEngine; using UnityEngine.UI; // 需要引入UI命名空间 public class DualRawImageScroller : MonoBehaviour { [Header(滚动设置)] [Tooltip(滚动速度像素/秒)] public float scrollSpeed 100f; [Tooltip(滚动方向通常为左或右)] public Vector2 direction Vector2.left; [Header(UI引用)] public RawImage backgroundImage01; public RawImage backgroundImage02; private RectTransform _rt01, _rt02; private float _imageWidth; // 图片的像素宽度 void Start() { if (backgroundImage01 null || backgroundImage02 null) { Debug.LogError(请将两个RawImage组件拖拽到脚本的对应字段); enabled false; return; } _rt01 backgroundImage01.rectTransform; _rt02 backgroundImage02.rectTransform; // 获取RawImage纹理的原始像素宽度。注意这里获取的是原始纹理宽度。 // 实际显示宽度受rectTransform.sizeDelta和锚点影响这里用纹理宽度作为移动单位基准。 _imageWidth backgroundImage01.texture.width; // 初始化第二个图片的位置紧挨着第一个的右边 // 注意这里操作的是anchoredPosition它是相对于锚点的像素位置。 Vector2 anchoredPos _rt01.anchoredPosition; anchoredPos.x _imageWidth; _rt02.anchoredPosition anchoredPos; } void Update() { // 计算本帧移动的像素距离 Vector2 moveDelta direction * scrollSpeed * Time.deltaTime; // 移动两个UI元素 _rt01.anchoredPosition moveDelta; _rt02.anchoredPosition moveDelta; // 判断第一个图片是否完全移出屏幕 // 将anchoredPosition转换为屏幕空间的位置来判断更直观但受锚点影响复杂。 // 更简单的方法判断其世界空间的边界是否超出相机视口。 Camera uiCamera GetComponentInParentCanvas()?.worldCamera; // 获取Canvas使用的相机Overlay模式为null if (uiCamera null) uiCamera Camera.main; // Overlay模式使用主相机计算 // 获取第一个图片在屏幕空间中的边界 Vector3[] worldCorners new Vector3[4]; _rt01.GetWorldCorners(worldCorners); // 四个角的世界坐标顺序左下、左上、右上、右下 // 将右下角索引2转换到视口坐标 Vector3 rightBottomViewport uiCamera.WorldToViewportPoint(worldCorners[2]); // 如果右下角已经移出屏幕左边缘视口X 0 if (rightBottomViewport.x 0) { // 重置第一个图片到第二个图片的右侧 Vector2 resetPos _rt02.anchoredPosition; resetPos.x _imageWidth; _rt01.anchoredPosition resetPos; // 交换引用 RectTransform tempRT _rt01; RawImage tempImg backgroundImage01; _rt01 _rt02; backgroundImage01 backgroundImage02; _rt02 tempRT; backgroundImage02 tempImg; } } }实操要点与避坑Canvas渲染模式这是最大的坑。对于Screen Space - Overlay模式UI直接绘制在屏幕上没有关联的相机。此时GetWorldCorners返回的坐标是屏幕像素坐标原点在左下角。我们的判断逻辑依然有效但uiCamera为null我们回退到Camera.main来进行WorldToViewportPoint转换在Overlay模式下这可能会有些偏差。更严谨的做法是针对Overlay模式直接使用屏幕像素坐标判断if (worldCorners[2].x 0)。但worldCorners在Overlay下本就是屏幕坐标所以可以简化。锚点Anchors与轴心PivotanchoredPosition严重依赖于RectTransform的锚点设置。如果锚点不是集中在中心计算会变得复杂。强烈建议将两个RawImage的锚点Anchors和轴心Pivot都设置为(0.5, 0.5)即中心对齐这样anchoredPosition就直接代表了UI元素的中心位置计算最直观。图片宽度代码中使用texture.width作为移动和重置的单位。这假设图片以原始像素大小显示。如果你的RawImage通过rectTransform.sizeDelta或锚点拉伸进行了缩放这个值就不准确了。此时应该使用_rt01.rect.width来获取UI元素实际的矩形宽度受缩放影响后的值。性能GetWorldCorners会产生少量的GC Alloc因为返回new Vector3[4]在Update中调用需注意。对于移动设备如果滚动UI很多可以考虑优化比如每两帧检查一次或者用其他估算方法。4.4 技巧四单RawImage的UV Rect动画法这是我最推荐的UI滚动方案。确保你的纹理Wrap Mode为Repeat。using UnityEngine; using UnityEngine.UI; public class SingleRawImageUVRectScroller : MonoBehaviour { [Header(滚动设置)] [Tooltip(横向滚动速度单位纹理宽度/秒)] public float scrollSpeedX 0.5f; [Tooltip(纵向滚动速度)] public float scrollSpeedY 0f; private RawImage _rawImage; private Rect _uvRect; void Start() { _rawImage GetComponentRawImage(); if (_rawImage null) { Debug.LogError(此脚本需要挂载在带有RawImage组件的物体上); enabled false; return; } _uvRect _rawImage.uvRect; // 获取初始UV矩形 } void Update() { // 修改UV矩形的x和y偏移量 _uvRect.x scrollSpeedX * Time.deltaTime; _uvRect.y scrollSpeedY * Time.deltaTime; // 将新的UV矩形赋值回去 _rawImage.uvRect _uvRect; // uvRect的x和y可以无限累加纹理会自动重复。 } }实操要点与避坑极致简单代码只有短短几行逻辑清晰。性能开销就是每帧更新一个Rect结构体几乎可以忽略不计。理解uvRectuvRect是一个Rect结构定义了从原始纹理上截取哪个矩形区域来显示。x和y是起始点的UV坐标0-1范围width和height是截取区域的宽高通常就是1表示显示整个纹理。我们通过不断改变起始点(x, y)让这个“显示窗口”滑动就实现了滚动效果。纹理设置同样纹理的Wrap Mode必须是Repeat。RawImage vs Image为什么用RawImage而不用更常见的Image因为Image组件使用Sprite而Sprite的UV Rect操作不如RawImage直接和灵活。RawImage直接使用Texture更适合这种动态UV操作。4.5 技巧五Shader Graph可视化编程法这个方法不需要写C#代码但需要在Unity编辑器中操作。假设你使用的是URPUniversal Render Pipeline。创建Shader Graph在Project窗口右键 - Create - Shader - Universal Render Pipeline - Shader Graph。编辑Shader Graph打开创建好的Shader Graph。在Blackboard参数列表中创建两个Vector1类型的属性分别命名为_ScrollSpeedX和_ScrollSpeedY设置合适的默认值如0.5。在主图中添加一个Time节点获取Time输出端口代表自游戏开始的总时间。添加一个Multiply节点将Time与_ScrollSpeedX相乘得到横向的偏移量。同理处理_ScrollSpeedY。添加一个Vector2节点将上一步得到的两个偏移量作为X和Y输入。添加一个Tiling And Offset节点。将UV连接到UV输入口将上一步的Vector2连接到Offset输入口。Tiling可以连接一个Vector2属性来控制平铺次数默认(1,1)。添加一个Sample Texture 2D节点。将Tiling And Offset节点的输出连接到它的UV输入口。将你的纹理拖拽到它的Texture 2D输入口或者通过一个Texture 2D属性来引用。将Sample Texture 2D节点的RGBA输出连接到主节点的Base Color。将Alpha输出连接到Alpha如果你的纹理带透明度。保存Shader Graph命名为“ScrollingBackgroundGraph”。创建材质并使用在Project中右键用刚才的Shader Graph创建材质。将材质赋给你的SpriteRenderer或RawImage。在运行时调整速度如果你需要在游戏运行时例如通过脚本修改滚动速度可以通过代码访问材质属性Material mat GetComponentRenderer().material; // 或 RawImage.material mat.SetFloat(_ScrollSpeedX, newSpeed);实操要点与避坑管线兼容性Shader Graph只适用于SRP可编程渲染管线即URP或HDRP。如果你在使用内置渲染管线Built-in这个方法行不通。性能优势所有计算时间累加、UV偏移都在GPU的Shader中完成CPU零开销。对于大量需要滚动的对象性能优势明显。灵活性你可以轻松地添加更多效果比如用正弦波节点让滚动速度波动或者添加多个纹理层进行混合实现非常复杂的动态背景。参数暴露在Shader Graph中定义的属性Properties会自动出现在材质面板上美术人员可以直接调整无需修改代码。5. 常见问题、性能优化与进阶技巧在实际项目中把基础功能跑通只是第一步。接下来你会遇到各种奇怪的问题并开始考虑优化。下面是我总结的“避坑指南”和“进阶心法”。5.1 常见问题排查速查表问题现象可能原因解决方案滚动时图片出现断裂或空白1. 双对象法重置位置计算错误两个精灵间有间隙。2. UV法纹理Wrap Mode不是Repeat。1. 检查_spriteWidth或_imageWidth计算是否准确使用Debug.DrawLine绘制边界辅助调试。2. 在Project面板确认纹理的Wrap Mode为Repeat。滚动速度忽快忽慢移动代码中没有乘以Time.deltaTime。确保所有速度相关的累加操作都乘以Time.deltaTime。UI滚动背景位置错乱或闪烁1. Canvas渲染模式导致坐标计算错误。2. 锚点Anchor和轴心Pivot设置非中心。1. 针对Overlay模式使用屏幕坐标直接判断worldCorners[2].x 0。2. 将RawImage的锚点和轴心都设为(0.5, 0.5)。使用UV偏移法图片边缘有接缝图片本身不是可无缝平铺纹理。使用图像处理软件的“偏移”滤镜处理图片手动修补贴图接缝。移动设备上滚动卡顿1. 图片尺寸过大。2. 每帧操作太多对象或进行了昂贵计算如GetWorldCorners。1. 压缩图片使用合适的Max Size。2. 对于静态速度的滚动可以考虑在FixedUpdate中更新或降低检查频率如每3帧检查一次重置逻辑。3. 优先使用单对象UV偏移法或Shader Graph法。多个背景层滚动不同步每层使用了独立的脚本和速度但帧率波动导致累积误差。让所有层共享一个“主计时器”例如从一个公共的float totalTime变量获取偏移量而不是各自累加Time.deltaTime。RawImage的uvRect滚动无效可能RawImage的材质是默认材质不支持uvRect动画。确保RawImage使用的材质Shader支持UV操作。通常使用UI/Default或自定义的Unlit Shader。5.2 性能优化实战心得Draw Call合并是关键无论用哪种方法如果屏幕上同时有多个滚动背景要确保它们使用的纹理在同一张图集Atlas中。SpriteRenderer使用同一图集的精灵RawImage使用同一图集的纹理可以合并Draw Call极大提升渲染效率。Unity的Sprite Atlas和UI的图集功能要善用。对象池思想对于双对象位移法虽然只有两个对象但在大型项目中可以将其抽象为对象池。需要更多层如前景、中景、背景或更多段比如非常长的背景时可以动态生成和回收精灵对象而不是在场景里预先放置一大堆。慎用.material属性如技巧二所述GetComponentRenderer().material会创建材质实例。如果你的场景中有100个相同的滚动云朵这就会创建100个材质实例增加内存和Draw Call。对于大量重复的、参数相同的滚动物体有两种优化方案使用MaterialPropertyBlock可以修改渲染器的属性而不创建材质实例。代码稍复杂但性能最佳。MaterialPropertyBlock _propBlock; void Start() { _propBlock new MaterialPropertyBlock(); GetComponentRenderer().GetPropertyBlock(_propBlock); } void Update() { _currentOffset.x scrollSpeedX * Time.deltaTime; _propBlock.SetTextureOffset(_MainTex, _currentOffset); // 属性名可能是_BaseMap (URP) GetComponentRenderer().SetPropertyBlock(_propBlock); }共享材质并手动管理偏移所有对象共享一个材质实例但这样它们的滚动就会完全同步。如果你需要不同的滚动速度此方法不适用。对于UI禁用Raycast Target如果你的滚动RawImage只是背景不需要交互务必在Inspector中取消勾选Raycast Target。这能避免不必要的UI事件检测提升UI系统的运行效率。5.3 进阶技巧视差滚动与动态速度掌握了基础滚动就可以玩出更多花样。多层视差滚动这是2D游戏增加景深感的标配。原理很简单让不同层的背景以不同的速度滚动。离相机越远的层如远处的山、云滚动速度越慢越近的层如近处的灌木滚动速度越快。你只需要为每一层单独配置一个脚本并设置不同的scrollSpeed即可。使用单SpriteRenderer UV法或Shader Graph法来实现视差效果尤其方便。根据角色速度动态调整在跑酷或赛车游戏中背景滚动速度应该和角色速度成正比。你可以在控制角色移动的脚本中暴露一个public float currentSpeed然后在背景滚动脚本的Update中用这个速度来动态计算本帧的滚动偏移量。// 在背景滚动脚本中 public PlayerController player; // 拖拽引用 public float parallaxFactor 0.5f; // 视差系数0-1之间 void Update() { float effectiveSpeed player.currentSpeed * parallaxFactor; _currentOffset.x effectiveSpeed * Time.deltaTime; // ... 应用偏移 }使用Animation Curve控制速度曲线如果你希望滚动速度不是恒定的而是有快慢变化比如模拟加速、减速可以在脚本中定义一个AnimationCurve变量用游戏时间作为横坐标X轴从曲线中读取当前的速度系数Y轴。public AnimationCurve speedCurve; private float _gameTime; void Update() { _gameTime Time.deltaTime; float speedMultiplier speedCurve.Evaluate(_gameTime); _currentOffset.x baseScrollSpeed * speedMultiplier * Time.deltaTime; // ... 应用偏移 }最后选择哪种技巧没有绝对的好坏只有合不合适。对于简单的2D背景双SpriteRenderer足矣对于需要精细控制的UI动态背景单RawImage UV Rect法是首选如果你的项目已经使用URP并且追求极致的性能和表现力那么投入一点时间学习Shader Graph绝对是值得的。希望这五种实战技巧和背后的细节剖析能让你在下次遇到图片滚动需求时游刃有余。
返回列表