做网站接单渠道,网站推广计划书具体包含哪些基本内容?,站长之家站长工具综合查询,中国建设网建设通官方网站大家好#xff0c;我是阿赵。
一、滚动复用的介绍 在制作游戏的过程中#xff0c;经常会遇到一些需要显示数量比较大的数据的情况。比如说#xff0c;一个排行榜#xff0c;需要展示当前服务器前一千个玩家的排名。或者游戏的背包容量特别大#xff0c;可以有几千个格子。… 大家好我是阿赵。
一、滚动复用的介绍 在制作游戏的过程中经常会遇到一些需要显示数量比较大的数据的情况。比如说一个排行榜需要展示当前服务器前一千个玩家的排名。或者游戏的背包容量特别大可以有几千个格子。 在早期的游戏里面这种情况一般会采取分页的做法比如一页只显示几十条信息通过翻页和跳转页逐页浏览信息。但现在的游戏很少采取分页的做法而是采用滚动列表的方式玩家可以上下滑动在同一页里面看完所有的信息。 以Unity引擎为例通过Scroll View组件就能轻松的实现上下滑动的效果。 ScrollView的基本用法很简单把需要展示的Item都放在Content节点下然后在Content上挂自动布局的组件和自动适配大小的组件列表就可以滚动起来了。 回到最开始的例子假如需要展示的内容非常多比如有1万个那我们是不是就要生成1万个item放在Content下面呢这样做显然是不行的。如果我们同时生成1万个item而且item很复杂那么生成的时候会很卡也会很占内存。虽然实际显示的数量可能只有几个但剩下的九千多个item还是需要通过Mask计算裁剪范围。 实际的情况是我们需要同时看到的item可能只有几个所以我们没有必要生成这么大数量的item而只是生成观察范围内的有限几个item就可以了通过上下滑动把移除出范围的item移动到新进入范围的位置并且把数据刷新成刚进入范围的数据显示就可以了。我习惯上把这种做法叫做滚动复用。 Unity引擎UI滚动复用1 从上面的视频可以看到我这里真的有1万个数据而且可以任意的滑动甚至任意的跳动到某个为止从视觉上是完全看不出来有什么破绽。然后从Unity的Hierarchy窗口可以看出实际上在Content节点下面的item数量就只有11个而已。 为了方便观察我把同时显示的数量减少一些并且把Mask去掉 Unity引擎UI滚动复用6 从视频的Scene视图可以看到消失的Item会立刻移动到进入的位置并且刷新显示。
二、 滚动复用的实现分析
1、计算可以滚动的范围 滚动复用还是可以使用Unity自带的Scroll View组件。只是在Content节点上面不需要再挂自动布局和自动大小的组件了。 首先需要明白的一点是Scroll View组件可以滚动的范围是通过Content节点的大小来决定的 在不使用滚动复用技术的时候我们需要加自动布局和自动大小的组件就是为了自动的计算出这个Content里面的内容总共需要多大的范围来显示从而决定滚动的实际范围。 既然我们现在不需要自动大小了所以我们可以通过计算得出Content的大小。举个例子 假如我现在是一个竖向滚动的列表每个item的宽度是200高度是60需要同时显示1000个itemitem中间没有空隙。那么要刚好放下这1000个itemContent需要的大小就是宽度200高度是60x100060000。 假如在item中间需要有5个像素的间隙那么Content的大小宽度还是200高度会变成了60x10005x(1000-1) 64995。 是不是很简单只要这样算一下把Content的大小算出来并设置。这时候列表已经可以滚动起来了。虽然上面一个item也没有但实际上它滚动的范围就是1000个item的高度。
2、 规定数据来源 对于一个滚动显示的列表来说它必须是有一个输入的数据源并且是以一维数组的形式表现的。 我这个例子里面只是简单的输入了一个字符串的数组而数组的内容就是“内容{序号}”。在实际的使用中数据的来源会更复杂比如是某个数据结构体的数组结构体里面可以包含很多数据需要在item上面显示很多内容。 不过数据是否复杂其实并不影响滚动复用的实现我这里只是为了说明原理。所以我准备了一个字符串数组每一个数据对应数组里面的一个Index。在接下来的实现里面每一个数据其实就是对应一个显示的item了。item的UI预设是预先做好的对应需要显示的数据内容。
3、 计算每个item的范围 在计算item范围之前先要定一个对齐方式。比如我们都以左上角作为对齐 然后item也是左上角对齐并且Pivot的x是0y是1。 这样设置的好处是当posX和posY都是0的时候item刚好对齐了Content的左上角。然后随着posX增大item会一直往右移动随着posY的一直减少(因为画布的Y轴是向上的所以负数才是向下)item会一直往下移动。 假如现在所有item已经按照正确的位置排列好了那么每个item应该有一个相对于Content的坐标。这里需要记录item的左上角和右下角的坐标 如果用Vector4来表示这个item的最大最小值坐标x和y就是左上角开始点的xy坐标。而z和w就是右下角结束点的xy坐标。这种记录方法还是看个人习惯的有些人喜欢记录开始点的xy坐标还有记录item的宽高也都是可以的毕竟结束点的坐标其实就是开始点坐标加上宽度和高度而已。 不过我为了下面的步骤能快速的得到起点和终点的实际坐标来计算item是否在范围内所以记录了结束点坐标而没有记录宽高。 这里需要注意一点虽然Unity的Y轴是朝向上的也就是说数据越往下就越小。但我们计算的时候其实不需要硬要这样算的我们就正常的算越往后的item坐标越大就行了包括下面的计算范围也是把计算的Y轴朝下计算起来的思维就方便很多。只要在最后给item的坐标赋值的时候把Y坐标取个负数就行。
4、 计算每个item是否在可以显示的范围内 假设下图的黑框就是现在Content的实际范围然后红框就是现在ScrollView的遮罩显示范围 由于Content的y坐标为0的时候是刚好和遮罩的左上角重合的所以可以认为现在黑色的框往上移动的距离也就是红框的顶部坐标其实就是Content的posY坐标。然后红框的底部坐标其实就是顶部坐标加上遮罩的高度。 得到了当前需要显示范围也就是红框的顶部和底部坐标之后通过之前初始化的时候已经计算好的每个item的开始坐标和结束坐标就能很简单的对比出item是否在红框的显示范围呢了。
5、 刷新显示 当知道了哪些index对应的item是在显示范围内的接下来就很好办了我们需要记录一个当前正在显示的index列表然后和新计算出来的正要显示的index列表做对比就可以知道有哪些index对应的item是需要隐藏哪些index对应的item是新显示出来的。对于没有变化的index我们不需要处理只需要先把需要隐藏的item隐藏掉再把新增的item显示出来然后通过index得到他们的坐标和数据把item摆在正确的位置并且根据数据显示item的内容就行了。 这里我的做法是维护一个对象池。当item不需要显示的时候把它们存放到对象池里面并且隐藏。当item需要新增的时候从对象池里面取出来并且显示。
6、 根据滚动事件触发刷新显示 知道了怎样刷新显示但在什么时候需要刷新呢在拖动列表的过程中按道理我们就需要不停的去检查item是否在显示范围内。 为了达到拖动的过程中触发刷新所以需要在Scroll View的OnValueChange回调里面注册一个方法当值变化的时候我们就重新计算并刷新显示。不过由于OnValueChange触发得很频繁所以我们需要降低一下调用的频率
OnValueChange回调会传入一个Vector2参数代表当前滑动的方向。因为我们做的例子是上下滑动所以参数的y坐标为0时就说明没有滑动所以不需要刷新。给一个调用的频率间隔当回调的频率过快时也不需要立刻刷新。
7、 跳转到某个item 在使用滚动列表的需求里面很多时候会有需要定位的情况。比如在显示1000个玩家信息的排行榜里面需要定位到自己所在的排名。 所以在做这个滚动复用的时候也需要加上一个定位的功能。 Unity引擎UI滚动复用2 这个功能实现的思路很简单因为每个item的坐标之前都已经记录了所以需要跳转到某个item其实直接去它的坐标然后加上item的一半高度就可以了。 不过这里有一种情况假如index对应的item在最上面或者最下面的一段而列表是会自动回弹的比如第一个item不能低于显示范围左上角如果把第一个item居中列表就会回弹到左上角那么就要计算一下当出现会回弹的情况直接把y坐标变成0或者在最下面的就要用Content高度减去mask的高度。
三、 源码 根据上面的思路简单写了一个例子是对应竖向滚动的。各位有兴趣可以自己思考一下怎样改为横向滚动或者可以用参数切换横竖向滚动。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class ScrollViewLoopCtrlBase : MonoBehaviour
{public ScrollRect scrollRect;public RectTransform content;protected float viewWidth;protected float viewHeight;private float itemWidth 200;private float itemHeight 60;protected float spaceTime 0.02f;protected float lastTime 0;protected Liststring dataList;protected float spacing 0;protected Listint currentShowIndexList;protected float contentHeight 0;protected Dictionaryint, TestItem showList;protected ListVector4 itemPosList;protected Dictionaryint, ListTestItem poolDict;// Start is called before the first frame updatevoid Start(){}// Update is called once per framevoid Update(){}#region 公共方法/// summary/// 设置滚动复用的数据/// /summary/// param namedataList数据源/param/// param namespacingitem之间的间隔/parampublic void SetData(Liststring dataList, float spacing 0){this.dataList dataList;this.spacing spacing;currentShowIndexList new Listint();RectTransform selfRect GetComponentRectTransform();viewWidth selfRect.rect.width;viewHeight selfRect.rect.height;itemPosList new ListVector4();InitData();UpdateView();}public void OnPosChange(Vector2 vec){if(vec.y 0){return;}if (Time.time - lastTime spaceTime){return;}lastTime Time.time;UpdateView();}/// summary/// 停止滚动/// /summarypublic virtual void StopMove(){if (scrollRect){scrollRect.StopMovement();}}/// summary/// 设置列表停留在某个index的item居中/// /summary/// param nameind/parampublic void SetIndexMiddle(int ind){StopMove();SetMiddleFun(ind);UpdateView();}#endregion/// summary/// 根据传入的数据初始化滚动列表/// /summaryprotected virtual void InitData(){int count dataList.Count;for (int i 0; i count; i){float startPos i * itemHeight;if (i 0){startPos spacing * i;}itemPosList.Add(new Vector4(0, startPos, 0, startPos itemHeight));}contentHeight itemHeight * count;if (count 1){contentHeight spacing * (count - 1);}content.sizeDelta new Vector2(viewWidth, contentHeight);}/// summary/// 设置index居中的具体实现/// /summary/// param nameind/paramprotected virtual void SetMiddleFun(int ind){if (dataList null || dataList.Count 0){return;}if (ind 0){ind 0;}else if (ind dataList.Count){ind dataList.Count - 1;}float halfScreenHeight viewHeight / 2;float posY itemPosList[ind].y - halfScreenHeight (itemPosList[ind].w - itemPosList[ind].y) / 2;if (posY 0){posY 0;}else if (contentHeight - posY viewHeight){posY contentHeight - viewHeight;}content.anchoredPosition new Vector2(0, posY);}/// summary/// 刷新列表显示/// /summaryprotected virtual void UpdateView(){Listint newShowList new Listint();float contentStartY content.anchoredPosition.y;float contentEndY contentStartY viewHeight;for (int i 0; i dataList.Count; i){if (CheckItemIsInArea(contentStartY, contentEndY,i)){newShowList.Add(i);}}if (currentShowIndexList null){AddItemsToShow(newShowList);currentShowIndexList newShowList;return;}Listint removeList new Listint();for (int i 0; i currentShowIndexList.Count; i){if (newShowList.IndexOf(currentShowIndexList[i]) 0){removeList.Add(currentShowIndexList[i]);}}if (removeList.Count 0){RemoveItemsFromShowList(removeList);}Listint newList new Listint();for (int i 0; i newShowList.Count; i){if (currentShowIndexList.IndexOf(newShowList[i]) 0){newList.Add(newShowList[i]);}}if (newList.Count 0){AddItemsToShow(newList);}currentShowIndexList newShowList;}/// summary/// 把对应index的item移除显示/// /summary/// param nameindList/paramprotected void RemoveItemsFromShowList(Listint indList){if (showList null || showList.Count 0){return;}for (int i 0; i indList.Count; i){if (showList.ContainsKey(indList[i])){TestItem item showList[indList[i]];showList.Remove(indList[i]);ReturnToPool(item);}}}/// summary/// 把对应index的item添加到显示/// /summary/// param nameindList/paramprotected void AddItemsToShow(Listint indList){if (showList null){showList new Dictionaryint, TestItem();}for (int i 0; i indList.Count; i){int id indList[i];if (showList.ContainsKey(id) false){TestItem item GetFromPoolById(id);showList.Add(id, item);item.SetData(id, dataList[id]);item.rect.anchoredPosition new Vector2(itemPosList[id].x, -itemPosList[id].y);}}}/// summary/// 检查某个序号的item是否在显示范围内/// /summary/// param namecontentStartY/param/// param namecontentEndY/param/// param nameitemIndex/param/// returns/returnsprotected bool CheckItemIsInArea(float contentStartY, float contentEndY, int itemIndex){Vector4 itemPos itemPosList[itemIndex];if (itemPos.y contentStartY itemPos.w contentStartY){return true;}if (itemPos.y contentStartY itemPos.w contentEndY){return true;}if (itemPos.y contentEndY itemPos.w contentEndY){return true;}return false;}#region 对象池/// summary/// 通过item类型从对象池获取对象/// /summary/// param nameitemType/param/// returns/returnsprotected TestItem GetFromPool(int itemType){if (poolDict null || poolDict.ContainsKey(itemType) false || poolDict[itemType].Count 0){string itemName GetItemNameByItemType(itemType);Object obj Resources.Load(itemName);GameObject go (GameObject)GameObject.Instantiate(obj, content.transform);TestItem item go.GetComponentTestItem();item.type itemType;return item;}else{ListTestItem poolList poolDict[itemType];TestItem item poolList[0];poolList.RemoveAt(0);item.gameObject.SetActive(true);return item;}}/// summary/// 这里是临时测试资源写死了几个item类型对应的item名字用于加载/// /summary/// param nameitemType/param/// returns/returnsprivate string GetItemNameByItemType(int itemType){string itemName ;switch (itemType){case 1:itemName testItem;break;case 2:itemName testItem2;break;case 3:itemName testItem3;break;}return itemName;}/// summary/// 把Item回收到对象池/// /summary/// param nameitem/paramprotected void ReturnToPool(TestItem item){item.gameObject.SetActive(false);if (poolDict null){poolDict new Dictionaryint, ListTestItem();}if (poolDict.ContainsKey(item.type) false){poolDict.Add(item.type, new ListTestItem());}ListTestItem poolList poolDict[item.type];if (poolList.IndexOf(item) 0){poolList.Add(item);}}/// summary/// 正常的滚动复用可能会使用到不同的Item这里定义一个通过id获取item的方法用于在不同需求下重写/// /summary/// param nameid/param/// returns/returnsprotected virtual TestItem GetFromPoolById(int id){return GetFromPool(1);}#endregion}