本文主要是介绍Unity下的List,Grid,Page,Banner优化方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
问题抛出
Unity的UI从NGUI到UGUI一直都在进化中,但是一直都是运行效率低。一个简洁的游戏界面会令游戏运行很舒服,但是有时候游戏就是需要大量的UI。
今天我们不谈UI的优化效率方法,网络上一搜一大把各种优化,今天我来分享下关于我们的游戏优化ScrollView的替代办法。
我们当前项目是一个轻量游戏。很大的比重都是UI,特别是各种List,Grid,Page翻页,滚动的Banner。
在早期Demo发布的时候,用的系统的ScrollView,大家知道这个显示一些数量不多的还能接受,如果数据量超大,那么就要用一些缓存逻辑来显示。而且那个拖动手感弱到不能接受。
解决办法
- 早期方案:
再一开始使用了store商店里的SupperScrollView组件(如下图),能够很好的缓存数据,优化显示,有几种显示方案,如下图:
他是基于UGUI系统ScrollView,有池管理,用最少的实例化对象来显示数据,算的上很容易上手的一个组件。基本上两个函数就可以显示数据出来。
因为早期的我们游戏UI也相对简单,只有一个主列表和固定的Banner,这个方案总的来说过得去。
随着UI界面的复杂,Banner需要能横向滚动,Grid列表不但可以上下滑动还要可以左右翻动,要有Page的概念。还有比较麻烦的顶部Banner和底部Grid拖拉关系的变化,这就需要改动Drag相关底层函数,SupperScrollView已经很难去扩展了。加上之前效率测试中发现ScrollView组件本身占用CPU耗费过大,如果再多几个ScrollView,那么不可想象,所以又有了下面的解决方案。
- 最终优化方案
经过大量翻找,要排除用系统自己的ScrollView,最好是自己重写ScrollView,终于找到了这个(OSA)Optimized ScrollView Adapter,貌似用过的人不多,看到好评不错,尝试了下感觉可以使用。
OSA完全抛弃了系统ScrollView,完全重写了列表,表格,并加了各种扩充,尽量表现的接近原生手感和效率,还有拖动特效等。
经过几天的测试和使用,在摸透了大部分功能后应用到项目里,发现效率更加的好,而且手感逼近原生,开心:)。
But,他并不适合Unity新手使用,他的模板类能创建简单的脚本模板,如果要加自己的东西,要改大量的代码。如果你有一颗挑战的心可以尝试下阅读和修改他的源码,进行学习和扩充。
https://docs.google.com/document/d/1OXxId2McwZ162Mm2VSd8BTvyjYjsgskT1BWkRF00wXU/edit
这个链接是我把OSA的文档的部分机翻和手动改正。内容主要是一步一步教你怎么能快速上手,后面的QA模块可能能遇到你想问的问题。这个建议还是要看一下的。或者你可以在他的网站上直接看最新的英文版手册。
学习和使用她
一些Unity初学者拿到OSA,看了帮助文档,照着做了,发现要改动下无从下手。那么本教程就是带你大致了解下他的框架,方便修改脚本。
首先看他的帮助文档,就是上文的地址。他的指引教程其实也只是创建了脚本,但是脚本的内容修改,还是要靠自己的理解。
所以这里我对他的源码进行加入一些我的理解分析,帮助大家能快速知道在什么地方改什么东西。也是写本文的另一个目的。
首先,你想要什么?
一个Grid还是一个List?区别就是Grid是表格的,支持一行多个,List只有一列。
所以在官方文档引导创建中也有选择:GridSRIA还是ListSRIA。他们创建出来的类是不同的
下面我们先从Grid来说,相比List稍微简单些。因为复杂的Grid脚本已经被他封装到基类了。
源码部分片段来自我自己的测试用例:
- GridAdapter
// You should modify the namespace to your own or - if you're sure there won't ever be conflicts - remove it altogether
namespace Thinbug
{// There is 1 important callback you need to implement, apart from Start(): UpdateCellViewsHolder()// See explanations belowpublic class MainGridAdapter : GridAdapter<GridParams, MainGridItemViewsHolder>{// Helper that stores data and notifies the adapter when items count changes// Can be iterated and can also have its elements accessed by the [] operatorpublic SimpleDataHelper<MainGridItemModel> Data { get; private set; }#region GridAdapter implementationpublic void InitGridAdpter(){Data = new SimpleDataHelper<MainGridItemModel>(this);// Calling this initializes internal data and prepares the adapter to handle item count changesbase.Start();}// This is called anytime a previously invisible item become visible, or after it's created, // or when anything that requires a refresh happens// Here you bind the data from the model to the item's views// *For the method's full description check the base implementationprotected override void UpdateCellViewsHolder(MainGridItemViewsHolder newOrRecycled){// In this callback, "newOrRecycled.ItemIndex" is guaranteed to always reflect the// index of item that should be represented by this views holder. You'll use this index// to retrieve the model from your data setMainGridItemModel model = Data[newOrRecycled.ItemIndex];newOrRecycled.listone.InitListOne(gameRoot.inst.HeroPlayer, XMLRoot.inst.levelXMLDict[model.idx], 1 , model.listid);LoadRoot.inst.ListOneLoad(newOrRecycled.listone.xml.fullpath, "png", newOrRecycled.listone);}// This is the best place to clear an item's views in order to prepare it from being recycled, but this is not always needed, // especially if the views' values are being overwritten anyway. Instead, this can be used to, for example, cancel an image // download request, if it's still in progress when the item goes out of the viewport.// <newItemIndex> will be non-negative if this item will be recycled as opposed to just being disabled// *For the method's full description check the base implementationprotected override void OnBeforeRecycleOrDisableCellViewsHolder(MainGridItemViewsHolder inRecycleBinOrVisible, int newItemIndex){if (inRecycleBinOrVisible.listone != null){inRecycleBinOrVisible.listone.Clear();}base.OnBeforeRecycleOrDisableCellViewsHolder(inRecycleBinOrVisible, newItemIndex);}#endregion// These are common data manipulation methods// The list containing the models is managed by you. The adapter only manages the items' sizes and the count// The adapter needs to be notified of any change that occurs in the data list. // For GridAdapters, only Refresh and ResetItems work for now#region data manipulationpublic void AddItemsAt(int index, IList<MainGridItemModel> items){//Commented: this only works with Lists. ATM, Insert for Grids only works by manually changing the list and calling NotifyListChangedExternally() after//Data.InsertItems(index, items);Data.List.InsertRange(index, items);Data.NotifyListChangedExternally();}public void RemoveItemsFrom(int index, int count){//Commented: this only works with Lists. ATM, Remove for Grids only works by manually changing the list and calling NotifyListChangedExternally() after//Data.RemoveRange(index, count);Data.List.RemoveRange(index, count);Data.NotifyListChangedExternally();}public void SetItems(IList<MainGridItemModel> items){Data.ResetItems(items);}#endregionpublic void OnDataRetrieved(MainGridItemModel[] newItems){//Commented: this only works with Lists. ATM, Insert for Grids only works by manually changing the list and calling NotifyListChangedExternally() after// Data.InsertItemsAtEnd(newItems);Data.List.Clear();Data.List.AddRange(newItems);Data.NotifyListChangedExternally();//刷新位置SetNormalizedPosition(_lastpos);}}// Class containing the data associated with an itempublic class MainGridItemModel : FrameBaseModel{public int idx;public int listid;}// This class keeps references to an item's views.// Your views holder should extend BaseItemViewsHolder for ListViews and CellViewsHolder for GridViews// The cell views holder should have a single child (usually named "Views"), which contains the actual // UI elements. A cell's root is never disabled - when a cell is removed, only its "views" GameObject will be disabledpublic class MainGridItemViewsHolder : CellViewsHolder{public SvgOne listone;//public Text titleText;// Retrieving the views from the item's root GameObjectpublic override void CollectViews(){base.CollectViews();// GetComponentAtPath is a handy extension method from frame8.Logic.Misc.Other.Extensions// which infers the variable's component from its type, so you won't need to specify it yourselfviews.GetComponentAtPath("listone", out listone);//views.GetComponentAtPath("TitleText", out titleText);}}
}
- 头部结构说明
public class MainGridAdapter : GridAdapter<GridParams, MainGridItemViewsHolder>
MainGridAdapter ,是我的类的名字,Adapter表示他一种显示数据的方法。继承自GridAdapter(OSA内置)
GridParmams(OSA内置),这个你可以理解是对应的Prefab
MainGridItemViewsHolder ,是一个View,也就是对应的一个Grid的显示组件集合的布局、
public SimpleDataHelper Data 这个是所有数据的集合。
所以你就明白了OSA他是把Data,View,Prefab配合Adapter来处理的。列表模式也是如此。
初始化函数InitGridAdpter() ,我更喜欢在系统开始是我自己调用初始化,而不是Start函数,所以这里和模板里不一样。
调用过Init后,那么我的Adapter就准备好了,随时数据可以进入了。
这里注意下,再初始化数据的时候,组件本身一定是selfActive = true 才行。
- 数据初始化
//初始化数据
......List<MainGridItemModel> listitem = new List<MainGridItemModel>();for (int i = 0; i < list.Length; ++i){var model = new MainGridItemModel(){idx = list[i],listid = pageid};listitem.Add(model);}_gridAdapter.OnDataRetrieved(listitem.ToArray());if (iScrollTo != -1){_gridAdapter.ScrollTo(iScrollTo);}
数据初始化调用OnDataRetrieved后,整个Adapter就开始运作起来了。
GridAdapter的核心只有UpdateCellViewsHolde,所以在这里你就可以刷新(update)数据就可以了。
注意你的组件大小,他会自动适配是几行几列,方便适配不同设备。开心吧,适配省心了。
这里不打算说Adapter里的其他函数,下面的List 里 我们再细讲。
只需要知道,只要在UpdateCellViewsHolder里刷新数据就可以了。
newOrRecycled.listone.InitListOne 就是用来初始化这个Grid数据 。
这个listone就是我的另外一个脚本,每个Grid有一个,能快速访问UGUI组件,写一些交互方法等,如下图:
下面来说一下稍微复杂点的List类的框架
- ListAdapter
// You should modify the namespace to your own or - if you're sure there won't ever be conflicts - remove it altogether
namespace Thinbug
{// There are 2 important callbacks you need to implement, apart from Start(): CreateViewsHolder() and UpdateViewsHolder()// See explanations belowpublic class MainListAdapter : OSA<MainParamsWithPrefab, FrameBaseView>{// Helper that stores data and notifies the adapter when items count changes// Can be iterated and can also have its elements accessed by the [] operatorpublic SimpleDataHelper<FrameBaseModel> Data { get; private set; }#region OSA implementationpublic void InitMainListAdapter(RectTransform freeze = null, float _freezeMoveHeight = 0f){Data = new SimpleDataHelper<FrameBaseModel>(this);// Calling this initializes internal data and prepares the adapter to handle item count changesbase.Start();}// This is called initially, as many times as needed to fill the viewport, // and anytime the viewport's size grows, thus allowing more items to be displayed// Here you create the "ViewsHolder" instance whose views will be re-used// *For the method's full description check the base implementationprotected override FrameBaseView CreateViewsHolder(int itemIndex){var modelType = Data[itemIndex].CachedType;if (modelType == typeof(FrameBannerModel)){//用来存放顶部Banner的Viewvar vh = new FrameBannerView();vh.Init(_Params.bannerFramePrefab, _Params.Content, itemIndex);if (Data[itemIndex].dataType == 1){if (BannerFreezeTrm != null){bannerFrameView = vh; //因为只有一个bannerview所以,这里锁定可以用这个}}return vh;}if (modelType == typeof(FramePageMainModel)){//用来存放Page翻页的var vh = new FramePageMainView();vh.Init(_Params.pageFramePrefab, _Params.Content, itemIndex);return vh;}// If you want to avoid ifs, you could use a dictionary with model type as key and a Func<int, SimpleBaseHV> as value// which would point to a separate method for each model type. Then here simply return _Map[modelType](itemIndex)throw new InvalidOperationException("Unrecognized model type: " + modelType.Name);}public void OnDataRetrieved(FrameBaseModel[] newItems){Data.ResetItems(newItems);}// This is called anytime a previously invisible item become visible, or after it's created, // or when anything that requires a refresh happens// Here you bind the data from the model to the item's views// *For the method's full description check the base implementationprotected override void UpdateViewsHolder(FrameBaseView newOrRecycled){// In this callback, "newOrRecycled.ItemIndex" is guaranteed to always reflect the// index of item that should be represented by this views holder. You'll use this index// to retrieve the model from your data setFrameBaseModel model = Data[newOrRecycled.ItemIndex];newOrRecycled.UpdateViews(model);// This allows items to have different sizes by calling UpdateItemSizeOnTwinPass() for each of them after the current ComputeVisibility pass.// We're always calling it just for simplicity, but usually you'd only call it if you detect the item's size has changed (in our case// this can only happen with the Ad items, whose sizes depend on their image)ScheduleComputeVisibilityTwinPass();}protected override float UpdateItemSizeOnTwinPass(FrameBaseView viewsHolder){viewsHolder.UpdateSize();FrameBaseModel model = Data[viewsHolder.ItemIndex];viewsHolder.root.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, model.modelSize);//viewsHolder.root.SetSizeFromParentEdgeWithCurrentAnchors(_Params.Content, RectTransform.Edge.Top, model.modelSize);return viewsHolder.root.rect.height;}protected override bool IsRecyclable(FrameBaseView potentiallyRecyclable, int indexOfItemThatWillBecomeVisible, double sizeOfItemThatWillBecomeVisible){FrameBaseModel model = Data[indexOfItemThatWillBecomeVisible];return potentiallyRecyclable.CanPresentModelType(model.CachedType);}#endregion}/// <summary>/// Custom params containing a single prefab. <see cref="ItemPrefabSize"/> is calculated on first accessing and invalidated each time <see cref="InitIfNeeded(IOSA)"/> is called./// </summary>[System.Serializable]public class MainParamsWithPrefab : BaseParams{public RectTransform bannerFramePrefab, pageFramePrefab, panelPrefab, myTopFramePrefab, myPageFramePrefab;public override void InitIfNeeded(IOSA iAdapter){base.InitIfNeeded(iAdapter);//AssertValidWidthHeight(bannerFramePrefab);//AssertValidWidthHeight(pageFramePrefab);}}
}
为什么List复杂呢,我们在看下结构
public class MainListAdapter : OSA<MainParamsWithPrefab, FrameBaseView>
MainListAdapter 继承自 OSA (OSA核心类)
FrameBaseView 还是View,但是和Grid在概念上不一样了。因为列表类可以显示不同的东西。
下图是OSA Demo中的一个例子截图:
第一个行是一个进度,第二行是带头像的,有的大小高一些,这样的多样性就可以通过不同的View来实现。
那么就需要这里可以建立一个FrameBaseView 基类,根据数据来显示不同的View。
我的View类是这样的 :
- FrameBaseView 基类
namespace Thinbug
{/// <summary>/// Includes common functionalities for the 3 views holders. <see cref="CanPresentModelType(Type)"/> is /// implemented in all of them to return wether the views holder can present a model of specific type (that's /// why we cache the model's type into <see cref="SimpleBaseModel.CachedType"/> inside its constructor)/// </summary>public abstract class FrameBaseView : BaseItemViewsHolder{//public Text titleText;/// <inheritdoc/>public override void CollectViews(){base.CollectViews();//root.GetComponentAtPath("TitleText", out titleText);}public abstract bool CanPresentModelType(Type modelType);/// <summary>/// Called to update the views from the specified model. /// Overriden by inheritors to update their own views after casting the model to its known type./// </summary>public virtual void UpdateViews(FrameBaseModel model){//if (titleText)// titleText.text = "#" + ItemIndex;}/// <summary>Used to manually update the RectTransform's size based on custom rules each VH type specifies</summary>public virtual void UpdateSize(){}}
}
我的最外围的ListAdapter是分为了一个Banner,一个Page,Page里嵌套了Grid。
- FramePageMainView
继承自FrameBaseView
我在构造函数CollectViews里初始化了Page数据。因为不会有变动,其实严格的来说应该放到UpdateViews中。
namespace Thinbug
{/// <summary>The views holder that can present an <see cref="GreenModel"/></summary>public class FramePageMainView : FrameBaseView{/// <inheritdoc/>public override void CollectViews(){base.CollectViews();MainPageListAdapter pageAdapter = root.GetComponentInChildren<MainPageListAdapter>();pageAdapter.InitPageAdpter();MainListScrollView.inst.InitPageData(pageAdapter);//root.GetComponentAtPath("ContentText", out contentText);}/// <inheritdoc/>public override bool CanPresentModelType(Type modelType){ return modelType == typeof(FramePageMainModel); }/// <inheritdoc/>public override void UpdateViews(FrameBaseModel model){base.UpdateViews(model);}}
}
下面是顶部的Banner类
- FrameBannerView
namespace Thinbug
{/// <summary>The views holder that can present an <see cref="GreenModel"/></summary>public class FrameBannerView : FrameBaseView{public RectTransform child;public RectTransform rawHide; //隐藏用的遮盖板public Image imgLine; //阴影线/// <inheritdoc/>public override void CollectViews(){base.CollectViews();//如果bannle初始化了,那么显示bannle数据MainTableAdapter tableAdapter;root.GetComponentAtPath("Banner/TableParent/OSATable", out tableAdapter);tableAdapter.InitTableAdapter();MainListScrollView.inst.InitTableData(tableAdapter);MainBannerAdapter bannerAdapter;root.GetComponentAtPath("Banner/OSABanner", out bannerAdapter);bannerAdapter.InitBannerAdapter();MainListScrollView.inst.InitBannerData(bannerAdapter);root.GetComponentAtPath("Banner", out child);root.GetComponentAtPath("Banner/OSABanner", out rawHide);root.GetComponentAtPath("Banner/ImageLine", out imgLine);}/// <inheritdoc/>public override bool CanPresentModelType(Type modelType){ return modelType == typeof(FrameBannerModel); }/// <inheritdoc/>public override void UpdateViews(FrameBaseModel model){base.UpdateViews(model);//var greenModel = model as BannerFrameModel;//contentText.text = greenModel.textContent;}//头部移入隐藏public void Hide(){child.transform.SetParent(MainListScrollView.inst.bannerFreeze, false);rawHide.gameObject.SetActive(false);imgLine.gameObject.SetActive(true);}//头部移除public void Show() {child.transform.SetParent(root, false);rawHide.gameObject.SetActive(true);imgLine.gameObject.SetActive(false);}}
}
同样的在构造函数里初始化了一些要用的组件数据等。
包含在Banner里横向的TableAdpter等数据。
- MainParamsWithPrefab
接下来看下预设Prefab类, MainParamsWithPrefab 是继承自 BaseParams,他也有了变化,对应不同的Prefab,和View对应起来。
/// <summary>/// Custom params containing a single prefab. <see cref="ItemPrefabSize"/> is calculated on first accessing and invalidated each time <see cref="InitIfNeeded(IOSA)"/> is called./// </summary>[System.Serializable]public class MainParamsWithPrefab : BaseParams{public RectTransform bannerFramePrefab, pageFramePrefab, panelPrefab, myTopFramePrefab, myPageFramePrefab;public override void InitIfNeeded(IOSA iAdapter){base.InitIfNeeded(iAdapter);//AssertValidWidthHeight(bannerFramePrefab);//AssertValidWidthHeight(pageFramePrefab);}}
最后还是数据
public SimpleDataHelper Data
所以我们看到List和Grid的区别,就是List因为可能内容不同,需要能灵活的扩充View和Prefab。
- 数据的刷新
//初始化主框架var newModels = new FrameBaseModel[2];newModels[0] = new FrameBannerModel();newModels[0].dataType = 1;newModels[0].modelSize = mainAdapter.Parameters.bannerFramePrefab.rect.height;newModels[1] = new FramePageMainModel();newModels[1].dataType = 1;newModels[1].modelSize = mainAdapter.Viewport.rect.height - (newModels[0].modelSize - freezeMoveHeight);mainAdapter.OnDataRetrieved(newModels);
这里我们是两个数据,一个Banner,一个Page的数据。
(TableAdapter是包含在BannerAdapter的预设里的,所以是在Banner的构造函数里初始化的)
下面我们说下ListAdapter里的其他函数。
- View创建
protected override FrameBaseView CreateViewsHolder(int itemIndex)
数据创建后,OSA会进入CreateViewsHolder函数,Grid里并不需要我们处理这个,毕竟不同的数据对应的View是不同的。
这里我根据数据类型创建不同的View就可以了。
根据不同的数据创建不同的View类。(就是上面的FrameBannerView,FramePageMainModel)
var modelType = Data[itemIndex].CachedType;if (modelType == typeof(FrameBannerModel)){//用来存放顶部Banner的Viewvar vh = new FrameBannerView();vh.Init(_Params.bannerFramePrefab, _Params.Content, itemIndex);if (Data[itemIndex].dataType == 1){if (BannerFreezeTrm != null){bannerFrameView = vh; //因为只有一个bannerview所以,这里锁定可以用这个}}return vh;}if (modelType == typeof(FramePageMainModel)){//用来存放Page翻页的var vh = new FramePageMainView();vh.Init(_Params.pageFramePrefab, _Params.Content, itemIndex);return vh;}。。。
-
View刷新
protected override void UpdateViewsHolder(FrameBaseView newOrRecycled)
在需要显示的时候,会调用到这里,刷新某个View -
View大小
protected override float UpdateItemSizeOnTwinPass(FrameBaseView viewsHolder)
因为多Prefab,大小不同,所以显示时候要处理不同的大小 -
回收
protected override bool IsRecyclable
因为缓存的问题,对于相同的有缓存,这里要根据类型判断是否需要回收。
总结:
经过Grid和List 两个结构的简单介绍,希望大家能迅速的熟悉OSA的一个基础框架。
就只有这样吗?
是的,简单的了解下核心代码的结构就够了,结合自己的需求去做,才能深刻的理解他。再就是大量看官方例子中的源码更加深刻的理解相应部分。
经过预设的嵌套多层OSA,可以实现比较复杂的列表显示。
自己做的一个项目里
主界面Adapter嵌套了BannerAdapter(可以左右滚动的),TitleAdapter标题栏,PageAdapter(就是下面的分页的Grid,List的不同列表体)这样的一个多嵌套结构。
Unity中运行良好,手感和效率都值得推荐。
最后,好的组件来之不易,请大家支持原版。
SuperScrollView
https://assetstore.unity.com/packages/tools/gui/ugui-super-scrollview-86572
OSA
https://assetstore.unity.com/packages/tools/gui/optimized-scrollview-adapter-68436
https://download.csdn.net/download/thinbug/12928761?spm=1001.2014.3001.5501
试用如果好用还是建议支持osa作者
这篇关于Unity下的List,Grid,Page,Banner优化方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!