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

一、效果展示

二、实现过程

2.1 准备工作

首先在Canvas下创建一个空物体,将其命名为「SlideShow」,并设置好其大小。它将作为轮播图的父容器。
UGUI学习笔记(九)自制3D轮播图_第1张图片

在「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();
}
/// 
/// 创建子物体
/// 
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)位置。
UGUI学习笔记(九)自制3D轮播图_第2张图片

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

首先分析一下图片的移动轨迹。从俯视角来看,图片是在一个椭圆形的轨迹上移动
UGUI学习笔记(九)自制3D轮播图_第3张图片

但摄像机是位于轮播图的正前方,也就是说在摄像机的角度来看,图片是在一条数轴上往复移动。图片的远近通过缩放来表示。
UGUI学习笔记(九)自制3D轮播图_第4张图片

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

///   
/// 获取图片在x轴上的位置  
///   
/// 图片在椭圆上的位置[0,1]  
/// 椭圆的周长  
/// 
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);
	}
}

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

/// 
/// 获取放大系数
/// 
/// 图片在椭圆上的位置[0,1]
/// 最大放大系数
/// 最小放大系数
/// 
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;  
}
/// 
/// 计算子物体的位置数据
/// 
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();
}
/// 
/// 设置子物体的位置信息
/// 
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;
}

运行游戏,可以看到生成出来的图片已经有了位移和缩放变化,只不过还存在一些层级问题
UGUI学习笔记(九)自制3D轮播图_第5张图片

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);
}

运行游戏,可以发现层级已显示正常
UGUI学习笔记(九)自制3D轮播图_第6张图片

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属性暴露出去并不安全,因此可以定义一个设置监听事件的方法

/// 
/// 添加拖动监听事件
/// 
/// 
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属性

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

运行游戏,可以看到现在旋转功能基本实现,只是还缺少转动时的动画
UGUI学习笔记(九)自制3D轮播图_第7张图片

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);
}

最后的效果如下

你可能感兴趣的:(Unity,unity,ui)