当前位置: 首页 > news >正文

UGUI学习笔记(九)自制3D轮播图

一、效果展示

二、实现过程

2.1 准备工作

首先在Canvas下创建一个空物体,将其命名为「SlideShow」,并设置好其大小。它将作为轮播图的父容器。

在「SlideShow」身上挂载一个脚本,命名为「SlideShow3D」。声明一个Vector2成员用来设定每张图片的大小,一个Sprite数组用来存储需要展示的图片

public class SlideShow3D : MonoBehaviour
{
    // 图片大小
    public Vector2 ItemSize;
    // 图片集
    public Sprite[] ItemSprites;
}

新创建一个脚本命名为「SlideShowItem」,作为子物体身上挂载的脚本。

public class SlideShowItem : MonoBehaviour
{

}

2.2 动态创建子物体

由于子物体都具有相同的特征,因此单独写一个创建子物体模板的方法

private GameObject CreateTemplate()
{
	GameObject item = new GameObject("Template");
	item.AddComponent<Image>();
	item.AddComponent<RectTransform>().sizeDelta = ItemSize;
	item.AddComponent<SlideShowItem>();
	return item;
}

接下来通过给生成的模板添加sprite、设置parent并实例化,来真正生成子物体。

// 子物体集合
private List<SlideShowItem> _items;
void Start()
{
	_items = new List<SlideShowItem>();
	CreateItems();
}
/// <summary>
/// 创建子物体
/// </summary>
private void CreateItems()
{
	var template = CreateTemplate();
	foreach (var sprite in ItemSprites)
	{
		var slideShowItem = Instantiate(template).GetComponent<SlideShowItem>();
		slideShowItem.SetParent(transform);
		slideShowItem.SetSprite(sprite);
		_items.Add(slideShowItem);
	}
	Destroy(template);
}

这里将设置parent和sprite的方法放在了子物体的脚本中,以提高内聚性。

public class SlideShowItem : MonoBehaviour
{
    private Image _img;

    private Image Img
    {
        get
        {
            if (_img == null)
                _img = GetComponent<Image>();
            return _img;
        }
    }
    public void SetParent(Transform parentTransform)
    {
        transform.SetParent(parentTransform);
    }

    public void SetSprite(Sprite sprite)
    {
        Img.sprite = sprite;
    }
}

现在运行游戏,可以看到图片成功生成了出来,并且都堆在了(0,0)位置。

2.3 使用一维数据模仿椭圆轨迹

首先分析一下图片的移动轨迹。从俯视角来看,图片是在一个椭圆形的轨迹上移动

但摄像机是位于轮播图的正前方,也就是说在摄像机的角度来看,图片是在一条数轴上往复移动。图片的远近通过缩放来表示。

因此这里可以定义一个方法,通过图片在椭圆轨道上的位置计算出映射到数轴上的位置

/// <summary>  
/// 获取图片在x轴上的位置  
/// </summary>  
/// <param name="ratio">图片在椭圆上的位置[0,1]</param>  
/// <param name="lenght">椭圆的周长</param>  
/// <returns></returns>
private float GetX(float ratio,float lenght)
{
	if (ratio > 1 || ratio < 0)
	{
		Debug.LogError("ratio必须在[0,1]的范围内");
		return 0;
	}
	if (ratio >= 0 && ratio < 0.25f)
	{
		return lenght * ratio;
	}
	else if (ratio >= 0.25f && ratio < 0.75f)
	{
		return lenght * (0.5f - ratio);
	}
	else
	{
		return lenght * (ratio - 1);
	}
}

接下来计算图片的放大系数。这个比较简单,只需要定义出最大放大系数(图片离相机最近时)和最小放大系数(图片离相机最远时),就可以根据图片在椭圆轨道上的位置,计算出当前的缩放系数。

/// <summary>
/// 获取放大系数
/// </summary>
/// <param name="ratio">图片在椭圆上的位置[0,1]</param>
/// <param name="max">最大放大系数</param>
/// <param name="min">最小放大系数</param>
/// <returns></returns>
private float GetScaleTimes(float ratio, float max, float min)
{
	if (ratio > 1 || ratio < 0)
	{
		Debug.LogError("ratio必须在[0,1]的范围内");
		return 0;
	}
	float offset = (max - min) / 0.5f;
	if (ratio < 0.5f)
	{
		return max - offset * ratio;
	}
	else
	{
		return max - offset * (1 - ratio);
	}
}

有了这两个方法,我们就可以计算出每个图片所在的位置及其缩放。这里可以把这两个信息封装成一个结构体,便于传参

// 最大缩放系数  
public float MaxScale;  
// 最小放大系数  
public float MinScale;  
// 图片间间距  
public float Offset;  
// 子物体位置数据集合  
private List<ItemPos> _posData;

public struct ItemPos  
{  
    public float X;  
    public float Scale;  
}
/// <summary>
/// 计算子物体的位置数据
/// </summary>
private void CalculateItemData()
{
	// 椭圆轨道周长
	float length = (ItemSize.x + Offset) * _items.Count;
	// 比例系数
	float radioOffset = 1 / (float) _items.Count;
	float radio = 0;
	for (int i = 0; i < _items.Count; i++)
	{
		ItemPos data = new();
		data.X = GetX(radio, length);
		data.Scale = GetScaleTimes(radio, MaxScale, MinScale);
		radio += radioOffset;
		_posData.Add(data);
	}
	
}

Start()方法中调用上面的方法,计算出子物体的位置信息后,将信息存储在一个集合中。然后再定义一个方法对集合中的数据进行遍历,将位置信息传递给子物体类。让位置、缩放的设置工作交给子物体类。

void Start()
{
	_items = new List<SlideShowItem>();
	_posData = new List<ItemPos>();
	CreateItems();
	CalculateItemData();
	SetItemData();
}
/// <summary>
/// 设置子物体的位置信息
/// </summary>
private void SetItemData()
{
	for (int i = 0; i < _items.Count; i++)
	{
		_items[i].SetPosData(_posData[i]);
	}
}

「SlideShowItem」类中新增的代码如下

private RectTransform _rect;

private RectTransform Rect
{
	get
	{
		if (_rect == null)
			_rect = GetComponent<RectTransform>();
		return _rect;
	}
}
public void SetPosData(ItemPos itemPos)
{
	Rect.anchoredPosition = Vector2.right*itemPos.X;
	Rect.localScale = Vector3.one*itemPos.Scale;
}

运行游戏,可以看到生成出来的图片已经有了位移和缩放变化,只不过还存在一些层级问题

2.4 计算层级

下面来解决前面出现的层级问题。一种解决方案是给每个子物体添加Canvas组件,并单独设置它们的「Sort Order」属性。但这种方案会增加额外的draw call,造成性能问题,因此不推荐使用。另一种方案是动态改变子物体在Hierarchy面板上的顺序,将靠近摄像机的图片置于下方。这里采用第二种方案。

首先给「ItemPos」添加一个Order字段,用来表示该图片对应的层级。因为后面需要单独对_posData集合中的Order字段进行修改,所以要把「ItemPos」改为class类型。

public class ItemPos  
{  
    public float X;  
    public float Scale;  
    public int Order;  
}

修改CalculateItemData()方法,通过Linq给_posData集合按Scale从小到大进行排序,生成一个新的集合。根据新的集合的顺序,给Order属性赋值

private void CalculateItemData()
{
	// 椭圆轨道周长
	float length = (ItemSize.x + Offset) * _items.Count;
	// 比例系数
	float radioOffset = 1 / (float) _items.Count;
	float radio = 0;
	for (int i = 0; i < _items.Count; i++)
	{
		ItemPos data = new();
		data.X = GetX(radio, length);
		data.Scale = GetScaleTimes(radio, MaxScale, MinScale);
		radio += radioOffset;
		_posData.Add(data);
	}

	var newPosData = _posData.OrderBy(u => u.Scale).ToList();
	for (int i = 0; i < newPosData.Count; i++)
	{
		newPosData[i].Order = i;
	}
}

然后在「SlideShowItem」类中的SetPosData()方法中,将Order值设置为当前物体在Hierarchy面板上的层级即可。利用transform.SetSiblingIndex()方法可以很方便地实现这一点。

public void SetPosData(ItemPos itemPos)
{
	Rect.anchoredPosition = Vector2.right*itemPos.X;
	Rect.localScale = Vector3.one*itemPos.Scale;
	transform.SetSiblingIndex(itemPos.Order);
}

运行游戏,可以发现层级已显示正常

2.5 实现旋转效果

下面来实现鼠标拖拽旋转效果。既然涉及到拖拽,那么实现「IDragHandler」和「IEndDragHandler」两个接口就是第一选择。我们让「SlideShowItem」类实现这两个接口,并重写OnDrag()OnEndDrag()这两个方法。OnDrag()方法传入的参数中,有一个delta属性,它记录了鼠标拖拽的位移,我们将它记录在成员变量中。然后定义一个委托,这个委托可以从外部传入,然后在OnEndDrag()中调用。

// 鼠标拖动增量  
private float _moveDelta;  
// 拖动结束时的回调  
private Action<float> _moveAction;

public void OnDrag(PointerEventData eventData)
{
	_moveDelta = eventData.delta.x;
}

public void OnEndDrag(PointerEventData eventData)
{
	_moveAction(_moveDelta);
	_moveDelta = 0;
}

直接将_moveAction属性暴露出去并不安全,因此可以定义一个设置监听事件的方法

/// <summary>
/// 添加拖动监听事件
/// </summary>
/// <param name="onMove"></param>
public void AddMoveListener(Action<float> onMove)
{
	_moveAction = onMove;
}

下面来分析一下旋转效果的实现原理。其实很简单,就是重新设置每一个图片的PosData。由于我们的「ItemPos」数据存储在了一个数组中,因此相当于把图片由原本的_posData[i]替换成_posData[i+1]_posData[i-1]。所以我们需要记录一下刚开始每个图片的下标。在「SlideShowItem」类中新增一个ItemIndex属性

// 下标  
public int ItemIndex;

然后在「SlideShow3D」中的SetItemData()方法中进行赋值

private void SetItemData()
{
	for (int i = 0; i < _items.Count; i++)
	{
		_items[i].SetPosData(_posData[i]);
		_items[i].ItemIndex = i;
	}
}

接下来在「SlideShow3D」类中添加回调方法。根据传入的鼠标位移值的正负,判断向右移动还是向左移动。

private void CreateItems()
{
	var template = CreateTemplate();
	foreach (var sprite in ItemSprites)
	{
		var slideShowItem = Instantiate(template).GetComponent<SlideShowItem>();
		slideShowItem.SetParent(transform);
		slideShowItem.SetSprite(sprite);
		// 添加事件监听
		slideShowItem.AddMoveListener(MoveItem);
		_items.Add(slideShowItem);
	}
	Destroy(template);
}

private void MoveItem(float moveDelta)
{
	int symbol = moveDelta > 0 ? 1 : -1;
	for (int i = 0; i < _items.Count; i++)
	{
		_items[i].ChangeIndex(symbol,_items.Count);
		_items[i].SetPosData(_posData[_items[i].ItemIndex]);
	}
}

这里的ChangeIndex()方法定义在「SlideShowItem」类中,用来更新ItemIndex属性

/// <summary>
/// 变更下标
/// </summary>
/// <param name="symbol">下标变化量</param>
/// <param name="total">item总数</param>
public void ChangeIndex(int symbol,int total)
{
	int id = ItemIndex;
	id += symbol;
	if (id < 0)
	{
		id += total;
	}
	ItemIndex = id % total;
}

运行游戏,可以看到现在旋转功能基本实现,只是还缺少转动时的动画

2.6 添加旋转动画

给旋转效果添加动画可以直接使用DOTween插件,十分简单高效。这里之所以使用协程是为了防止动画播放过程中出现层级显示问题。

public void SetPosData(ItemPos itemPos)
{
	Rect.DOAnchorPos(Vector2.right * itemPos.X, _aniTime);
	Rect.DOScale(Vector3.one*itemPos.Scale, _aniTime);
	StartCoroutine(WaitAnime(itemPos));
}

private IEnumerator WaitAnime(ItemPos itemPos)
{
	yield return new WaitForSeconds(_aniTime * 0.5f);
	transform.SetSiblingIndex(itemPos.Order);
}

最后的效果如下

相关文章:

  • R统计-单因素ANOVA/Kruskal-Wallis置换检验
  • 动态开点线段树(C++实现)
  • pytorch保存和加载模型权重以及CUDA在pytorch中的使用
  • UDF提权(mysql)
  • linux内核漏洞(CVE-2022-0847)
  • kubekey 离线部署 kubesphere v3.3.0
  • Git史上最详细教程(详细图解)
  • Python科学计算库练习题
  • 高性能MySQL实战第10讲:搭建稳固的MySQL运维体系
  • java毕业设计茶叶企业管理系统Mybatis+系统+数据库+调试部署
  • JAVA安装教程 (windows)
  • 6.hadoop文件数据库系列讲解
  • Day11OSI与TCP/IP协议簇以及物理层
  • Javaweb学生信息管理系统(Mysql+JSP+MVC+CSS)
  • ubuntu-hadoop伪分布
  • .pyc 想到的一些问题
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • 【5+】跨webview多页面 触发事件(二)
  • 【跃迁之路】【477天】刻意练习系列236(2018.05.28)
  • node入门
  • Object.assign方法不能实现深复制
  • PAT A1050
  • PAT A1092
  • VuePress 静态网站生成
  • 百度小程序遇到的问题
  • 从0到1:PostCSS 插件开发最佳实践
  • 服务器从安装到部署全过程(二)
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 极限编程 (Extreme Programming) - 发布计划 (Release Planning)
  • 深度解析利用ES6进行Promise封装总结
  • 算法-图和图算法
  • 一些基于React、Vue、Node.js、MongoDB技术栈的实践项目
  • No resource identifier found for attribute,RxJava之zip操作符
  • ​DB-Engines 11月数据库排名:PostgreSQL坐稳同期涨幅榜冠军宝座
  • ​总结MySQL 的一些知识点:MySQL 选择数据库​
  • #微信小程序(布局、渲染层基础知识)
  • (五)MySQL的备份及恢复
  • (转)Mysql的优化设置
  • (转)VC++中ondraw在什么时候调用的
  • (最简单,详细,直接上手)uniapp/vue中英文多语言切换
  • .NET 8.0 发布到 IIS
  • .NET Core 网络数据采集 -- 使用AngleSharp做html解析
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .net(C#)中String.Format如何使用
  • [Android 数据通信] android cmwap接入点
  • [AutoSar]状态管理(五)Dcm与BswM、EcuM的复位实现
  • [AutoSAR系列] 1.3 AutoSar 架构
  • [BZOJ3211]:花神游历各国(小清新线段树)
  • [docker] Docker的私有仓库部署——Harbor
  • [HackMyVM]靶场 Quick3
  • [IE技巧] 如何让IE 启动的时候不加载任何插件
  • [JS入门到进阶] 哎,被vite小坑了一波,大家记得配置build.cssTarget为‘chrome61‘
  • [Linux]进程间通信(system V共享内存 | system V信号量)
  • [Markdown] 02 简单应用 第二弹
  • [MFC] MFC消息机制的补充