
简介这份资源面向Unity开发者与游戏视觉设计学习者聚焦在引擎中实现照片墙效果这一具体场景适合具备一定编辑器操作基础、希望提升场景表现力的初中级人员。包内共310个文件以png图片素材、meta资源索引、asset场景与配置资产、cs脚本、dll插件库及xml、json等配置说明为主另有少量unity场景与sample示例压缩包约1.54MB整体结构便于按素材、脚本、配置分类查阅。已有525人学习下载说明该效果在实战项目中具备一定参考价值。资源围绕平面几何体搭建展示面、自定义材质与纹理贴图、C#脚本控制图片切换与淡入淡出过渡、灯光营造立体感以及点击缩放拖动等交互展开并涉及Asset Store插件与项目文件组织思路可帮助读者快速理解照片墙从素材准备到动态交互的完整实现路径并迁移到展厅、相册或游戏UI等场景中。1. 照片墙不是把图铺满Unity 里真正要解决的是布局、性能与交互很多人第一次在 Unity 里做照片墙直觉就是把一堆贴图拖进 Canvas用 Grid Layout Group 一铺觉得完事。真跑起来才发现几十张图就开始掉帧滚动时图片乱序点击某张照片放大后返回位置全错。照片墙效果在 Unity 里本质是三个问题的叠加——不规则布局怎么排、大量图片怎么省内存、点击缩放怎么不破坏原布局。它适合做相册展示、展厅大屏、数字孪生里的资产面板也适合作为 UGUI 进阶练手项目。这篇笔记按「先能跑通最小版本再谈性能与交互」的顺序拆代码可以直接抄参数我会标清楚为什么这么设。照片墙这个词听着简单但它是 UGUI 布局、对象池、异步加载、动画状态管理的一次综合演练值得认真做一遍。2. 用 UGUI 搭出可滚动的照片墙骨架2.1 为什么选 ScrollRect 自定义布局而不是 Grid Layout GroupGrid Layout Group 的问题是它强制等宽等高照片墙往往需要错落感而且它每帧重算所有子物体位置图一多就是性能黑洞。常见做法是用 ScrollRect 做滚动容器内容节点挂一个自己写的布局脚本按列高或瀑布流算法算位置。这样你能控制每张图的宽高比也能只对可视区域内的图片做加载。结构上分三层最外层 ScrollRect中间 Content挂布局脚本和 Content Size Fitter最里面是每个照片项 PhotoItem 预制体。PhotoItem 里放一个 Image 做缩略图、一个 Button 做点击、一个 Outline 做选中态。Content 的锚点要设成顶部拉伸pivot 设 (0.5, 1)否则滚动方向会反。2.2 瀑布流布局脚本与关键参数下面这个脚本按固定列数、每列累加高度的方式排布适合宽度固定、高度随图片比例变化的场景。using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(RectTransform))] public class WaterfallLayout : MonoBehaviour { public int columnCount 3; // 列数手机竖屏一般 2-3 public float spacing 12f; // 项间距和 UI 缩放一起调 public float sidePadding 16f; // 左右留白 private RectTransform _rect; private float[] _columnHeights; void Awake() _rect GetComponentRectTransform(); // 每次增删照片后调用一次不要放 Update public void Rebuild() { int childCount transform.childCount; if (childCount 0) return; float totalWidth _rect.rect.width - sidePadding * 2f; float itemWidth (totalWidth - spacing * (columnCount - 1)) / columnCount; _columnHeights new float[columnCount]; for (int i 0; i childCount; i) { var item transform.GetChild(i) as RectTransform; // 找到当前最矮的列把这张图放进去 int col FindShortestColumn(); float x sidePadding col * (itemWidth spacing); float y -_columnHeights[col]; item.anchorMin new Vector2(0, 1); item.anchorMax new Vector2(0, 1); item.pivot new Vector2(0, 1); item.sizeDelta new Vector2(itemWidth, itemWidth); // 高度按比例改 item.anchoredPosition new Vector2(x, y); _columnHeights[col] item.rect.height spacing; } float maxHeight 0f; foreach (var h in _columnHeights) maxHeight Mathf.Max(maxHeight, h); _rect.sizeDelta new Vector2(_rect.sizeDelta.x, maxHeight sidePadding); } int FindShortestColumn() { int idx 0; for (int i 1; i _columnHeights.Length; i) if (_columnHeights[i] _columnHeights[idx]) idx i; return idx; } }逻辑说明Rebuild只在照片数量变化时调用避免每帧计算。FindShortestColumn是瀑布流的核心每次把新图塞进当前最矮的列保证整体高度均衡。参数上columnCount建议按屏幕宽度动态算比如Mathf.Max(2, Mathf.FloorToInt(Screen.width / 400f))这样平板和手机都能用。spacing和sidePadding要跟 Canvas Scaler 的参考分辨率匹配否则不同分辨率下间距会失真。提示Content 上不要同时挂 Content Size Fitter 的 Vertical Fit 为 Preferred Size会和手动设置的 sizeDelta 打架二选一。2.3 图片按比例撑满格子且不变形照片宽高比五花八门直接塞进正方形格子会拉伸。做法是给 Image 外面套一层 Mask 或 RectMask2DImage 用preserveAspect保持比例超出部分裁掉。如果想让格子高度跟着图片比例走就在加载完 Sprite 后把item.sizeDelta.y改成itemWidth * (sprite.rect.height / sprite.rect.width)再调一次 Rebuild。这一步是照片墙好不好看的分水岭别偷懒。3. 大量照片的加载与内存控制3.1 异步加载与缩略图策略照片墙最容易翻车的地方是内存。原图动辄几 MB一百张就是几百 MB移动端直接崩。正确做法是加载时生成缩略图只保留可视区域附近的原图。Unity 里可以用UnityWebRequestTexture异步加载本地或远程图片加载完用Texture2D缩放成缩略图。using UnityEngine; using UnityEngine.Networking; using System.Collections; public class ThumbnailLoader : MonoBehaviour { public int thumbSize 256; // 缩略图边长列表用 256 足够 public IEnumerator LoadThumb(string path, System.ActionTexture2D onDone) { using (var req UnityWebRequestTexture.GetTexture(file:// path)) { yield return req.SendWebRequest(); if (req.result ! UnityWebRequest.Result.Success) { Debug.LogWarning($加载失败 {path}: {req.error}); onDone?.Invoke(null); yield break; } var full DownloadHandlerTexture.GetContent(req); var thumb ScaleTexture(full, thumbSize, thumbSize); Destroy(full); // 原图立刻释放别留着 onDone?.Invoke(thumb); } } Texture2D ScaleTexture(Texture2D src, int w, int h) { var rt RenderTexture.GetTemporary(w, h); Graphics.Blit(src, rt); var prev RenderTexture.active; RenderTexture.active rt; var dst new Texture2D(w, h, TextureFormat.RGB24, false); dst.ReadPixels(new Rect(0, 0, w, h), 0, 0); dst.Apply(); RenderTexture.active prev; RenderTexture.ReleaseTemporary(rt); return dst; } }逻辑说明UnityWebRequestTexture在协程里跑不阻塞主线程。加载完立刻把原图Destroy只留缩略图这是内存能压住的关键。thumbSize设 256 是因为列表里单张图显示尺寸通常不超过 300 像素再大就是浪费显存。如果图片在 StreamingAssets 或 persistentDataPath路径前缀要对应改。3.2 对象池复用 PhotoItem滚动时不断 Instantiate 和 Destroy 会产生 GC 峰值表现就是滚动卡顿。用对象池预先创建一屏能显示的数量的 PhotoItem滚动时把移出屏幕的项回收重新绑定数据。判断是否移出屏幕用RectTransformUtility.RectangleContainsScreenPoint或直接比较 anchoredPosition 和 ScrollRect 的 viewport 范围。public class PhotoPool : MonoBehaviour { public GameObject itemPrefab; public RectTransform content; private readonly System.Collections.Generic.QueueGameObject _pool new(); public GameObject Get() { if (_pool.Count 0) { var go _pool.Dequeue(); go.SetActive(true); return go; } return Instantiate(itemPrefab, content); } public void Release(GameObject go) { go.SetActive(false); _pool.Enqueue(go); } }参数上池的初始容量按「一屏可见数量 缓冲 4 个」来定缓冲太少会频繁创建太多浪费内存。回收时记得把 Image 的 sprite 置空并Resources.UnloadUnusedAssets的时机控制好别每帧调。3.3 用 Addressables 还是 ResourcesResources 文件夹简单但会把所有资源打进包体且加载是同步的图一多启动就卡。常见做法是用 Addressables 做按需加载和远程更新缩略图打成一个 group原图打另一个 group点开大图时才加载原图。如果项目小、图不多Resources 也能用但要有心理准备后期迁移成本。选型上只要照片墙的图会超过 50 张我一般直接上 Addressables。4. 点击放大、返回与动画衔接4.1 从缩略图到大图的过渡动画点击某张照片后放大最忌讳的是瞬间切换用户会失去空间感。做法是记录被点击 PhotoItem 的世界坐标和尺寸在大图层用一个 Image 从该位置和尺寸插值到全屏用Mathf.SmoothStep做缓动。IEnumerator ZoomIn(RectTransform from, RectTransform to, float duration 0.25f) { float t 0f; Vector2 startPos from.position; Vector2 endPos to.position; Vector2 startSize from.rect.size; Vector2 endSize to.rect.size; while (t 1f) { t Time.deltaTime / duration; float e Mathf.SmoothStep(0f, 1f, t); to.position Vector2.Lerp(startPos, endPos, e); to.sizeDelta Vector2.Lerp(startSize, endSize, e); yield return null; } }逻辑说明SmoothStep比线性 Lerp 更有「加速再减速」的手感duration 设 0.2 到 0.3 秒最舒服太慢显得拖沓。返回时反向跑一遍同样的插值用户就能感知到「回到原来那张图」。注意动画期间要禁用 ScrollRect 的滚动否则会跟手势冲突。4.2 返回后布局错位的修复血泪经验放大再返回后照片墙位置全乱多半是因为动画改了 PhotoItem 的 anchoredPosition 却没还原。解决方法是动画只操作大图层绝不碰列表里的原始项或者动画前缓存原始 anchoredPosition结束后强制还原并调一次 Rebuild。另外 ScrollRect 的velocity在动画期间要清零不然松手惯性会把内容带偏。4.3 手势与点击的冲突处理ScrollRect 本身会拦截拖拽导致 Button 的点击偶尔失灵。常见做法是给 PhotoItem 挂一个实现IBeginDragHandler的脚本拖拽距离超过阈值就标记为滑动OnPointerClick里判断这个标记是滑动就不触发放大。阈值一般设 10 到 20 像素太小会误判太大点击迟钝。5. 照片墙避坑与排查清单5.1 图片显示成紫红色现象所有照片变成紫红色方块。原因Shader 丢失或材质引用的 shader 在当前渲染管线里不存在比如 URP 项目里用了 Built-in 的 UI/Default。解决把 Canvas 的材质换成 URP 对应的 UI shader或检查 Addressables 打包时 shader 有没有被剥离在 Graphics Settings 里把用到的 shader 加进 Always Included Shaders。5.2 滚动时图片闪烁或错位现象快速滚动时缩略图跳到别的格子。原因异步加载回调回来时PhotoItem 已经被对象池回收并绑定了新数据旧回调把图贴到了错误的项上。解决给每个加载请求带一个版本号或 token回调时比对当前项的 token不一致就丢弃结果。这是异步列表的经典坑别省这个判断。5.3 移动端内存暴涨后闪退现象浏览几十张图后 App 崩溃。原因原图 Texture 没释放或者缩略图用了TextureFormat.RGBA32且没压缩。解决加载完立刻 Destroy 原图缩略图用TextureFormat.RGB24在 Profiler 里看Texture Memory曲线正常应该稳定在一个平台期而不是持续爬升。另外注意Resources.UnloadUnusedAssets不要频繁调它本身很重。5.4 高分辨率屏上照片模糊现象同样的图在 2K 屏上糊。原因缩略图尺寸按逻辑像素算没乘 Canvas Scaler 的 scaleFactor。解决thumbSize要乘以canvas.scaleFactor或者干脆按屏幕物理分辨率的一屏尺寸来定缩略图边长。这个参数设错用户第一眼就觉得「不清晰」。5.5 点击放大后返回滚动位置跳回顶部现象返回列表时内容滚到了最上面。原因动画或重建布局时改了 Content 的 anchoredPosition。解决进入大图前缓存content.anchoredPosition返回后还原再调 Rebuild。顺序不能反先 Rebuild 再还原位置会被布局覆盖。6. 进阶让照片墙在低端机上也能跑满帧前面跑通之后真正拉开差距的是性能。我一般会做三件事。第一把缩略图按可视区域分页加载屏幕外两屏的项直接回收贴图只留占位图。第二用Canvas分层把滚动内容和固定 UI 拆到不同 Canvas减少重建范围因为 UGUI 任何一个元素变脏都会触发整个 Canvas 的 batch 重建。第三如果图量真的很大考虑用UIElements或者干脆用Mesh自己拼UGUI 在几千个元素时顶点数会爆。验证方法很直接打开 Profiler看Canvas.BuildBatch和Canvas.SendWillRenderCanvases的耗时正常应该低于 2ms再看Texture Memory是否稳定。如果 BuildBatch 超过 5ms就是 Canvas 拆分没做好。下面这个表是我常用的参数起点按项目实际情况微调。参数手机竖屏平板/大屏说明列数2-34-6按屏宽除以 400 取整缩略图边长256384乘 scaleFactor 后取整对象池容量可见数4可见数6缓冲太少会频繁创建动画时长0.25s0.3s大屏可稍慢拖拽阈值15px20px防误触还有一个容易被忽略的点照片墙的滚动惯性。ScrollRect 的decelerationRate默认 0.135手机上感觉偏「滑」改成 0.1 到 0.2 之间按手感调。movementType设 Elastic 还是 Clamped 看需求相册一般用 Clamped避免拉出空白。我自己踩过最深的坑是早期用同步Resources.Load加载缩略图图一多启动直接卡死三秒用户以为崩了。后来全部改成异步加对象池启动时间降到 200ms 以内。做照片墙先把加载和回收这两件事做扎实再谈动画和视觉顺序反了就是给自己挖坑。希望帮到你。本文还有配套的精品资源点击获取