不使用3D摄像机和Render Texture,而是直接自定义一个可以实现3D轮转的组件。

新建Scene命名为RotationDiagram,在Scripts文件夹内新建同名文件夹,并在其中新建RotationDiagram2D脚本。

我们的思路是,在代码中创建item的模板(而不是使用prefab加载),控制item模板的轮转来实现效果,所以还要再在RotationDiagram文件夹内新建RotationDiagramItem脚本。

创建Item模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEngine;
using UnityEngine.UI;

public class RotationDiagramItem : MonoBehaviour
{
private Image mImage;
private Image MyImage//如果在Start里面GetComponent<Image>();可能会出现加载不出来的情况,所以写在属性里。
{
get
{
if(mImage == null)
mImage = GetComponent<Image>();
return mImage;
}
}
public void SetParent(Transform parent)
{
gameObject.transform.SetParent(parent);
}
public void SetSprite(Sprite sprite)
{
MyImage.sprite = sprite;
}
}

修改RotationDiagram2D脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class RotationDiagram2D : MonoBehaviour
{
public Vector2 ItemSize;
public Sprite[] ItemSprites;

private List<RotationDiagramItem> mItems;

void Start()
{
mItems = new List<RotationDiagramItem>();
CreateItem();
}

private GameObject CreateTemplate()
{
GameObject item = new GameObject("Template");
item.AddComponent<RectTransform>().sizeDelta = ItemSize;
item.AddComponent<Image>();
item.AddComponent<RotationDiagramItem>();
return item;
}
private void CreateItem()
{
GameObject template = CreateTemplate();
RotationDiagramItem itemTemp = null;
foreach (Sprite sprite in ItemSprites)
{
itemTemp = Instantiate(template).GetComponent<RotationDiagramItem>();
itemTemp.SetParent(transform);
itemTemp.SetSprite(sprite);
mItems.Add(itemTemp);
}
Destroy(template);
}
}

实现伪3D旋转

数据结构

我们并不使用真实的3D排列,而是通过不同缩放比例的方式实现伪3D的效果。每个Item都只需要自己x方向的坐标信息和Scale信息就能实现横向的伪3D效果。

RotationDiagram2D添加ItemPosData结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
private List<ItemPosData> mPosData;
//...
void Start()
{
mItems = new List<RotationDiagramItem>();
mPosData = new List<ItemPosData>();//+++
CreateItem();
}
}
public struct ItemPosData
{
public float X;
public float Scale;
}

一维数据模仿椭圆的原理

原理

我们假设椭圆的周长为L,某个Item在椭圆上的位置比例(xL)和Item在X轴向上的位置比例(xX)的关系为:

  1. 在0L~0.25L范围内:(xL)的系数直接表示X轴向上的位置比例(xX)
  2. 在0.25L~0.5L范围内:(0.5L - xL)的系数表示X轴向上的位置比例(xX)
  3. 在0.5L~0.75L范围内:(0.5L - xL)的系数表示X轴向上的位置比例(-xX)
  4. 在0.75L~1L范围内:(xL - 1L)的系数表示X轴向上的位置比例(-xX)

椭圆的周长L = (item的宽度 + item的间隔)* item的总数

获取X轴的位置

RotationDiagram2D添加GetX方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// <summary>
/// 返回Item在X轴向上的位置
/// </summary>
/// <param name="ratio">Item在椭圆周长上的比例</param>
/// <param name="length">X轴的长度</param>
/// <returns>X轴向的坐标</returns>
private float GetX(float ratio, float length)
{
if(ratio > 1 || ratio < 0)
{
Debug.LogError("占用椭圆周长的比例必须是0~1的值");
return 0f;
}
if(ratio >= 0f && ratio < 0.25f)
{
return length * ratio;
}
else if (ratio >= 0.25f && ratio < 0.75f)
{
return length * (0.5f - ratio);
}
else
{
return length * (ratio - 1f);
}
}

获取对应位置的缩放

我们求缩放时,先定义缩放最小值和缩放最大值,设缩放最大值和缩放最小值的差值为H,由于椭圆在前方最大在后方最小,设椭圆的周长为L,那么我们可以求出每个“单位”缩放变换值:H/0.5L。由于这里设L为1,所以“单位”缩放值为H/0.5

这样在0L~0.5L范围内,Item按照在椭圆周长上的比例r,缩放值就是(1-r)* H/0.5

在0.5L~1L范围内,Item按照在椭圆周长上的比例r,缩放值就是r * H / 0.5

RotationDiagram2D添加GetScaleTimes方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public float ScaleTimesMin;
public float ScaleTimesMax;

/// <summary>
/// 返回Item在当前位置的缩放
/// </summary>
/// <param name="ratio">Item在椭圆周长上的比例</param>
/// <param name="max">最大缩放值</param>
/// <param name="min">最小缩放值</param>
/// <returns>Item的缩放</returns>
private float GetScaleTimes(float ratio,float max,float min)
{
if (ratio > 1 || ratio < 0)
{
Debug.LogError("占用椭圆周长的比例必须是0~1的值");
return 0f;
}
float scaleOffset = (max - min) / 0.5f;
if (ratio < 0.5f)
{
return max - (scaleOffset * ratio);
}
else
{
return max - scaleOffset * (1- ratio);
}
}

最终计算

RotationDiagram2D添加GetScaleTimesSetItemData方法,用来计算椭圆总长度和设置Item等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public float Offset;//每个item的距离
void Start()
{
//...
CalculateData();//+++
SetItemData();//+++
}
private void CalculateData()
{
float len = (ItemSize.x + Offset) * mItems.Count;//总长度
float ratioOffet = 1f/mItems.Count;//比率差值
float ratio = 0;
for (int i = 0; i < mItems.Count; i++)
{
ItemPosData data = new ItemPosData();
data.X = GetX(ratio, len);
data.Scale = GetScaleTimes(ratio, ScaleTimesMax, ScaleTimesMin);

ratio += ratioOffet;
mPosData.Add(data);
}
}
private void SetItemData()
{
for (int i = 0; i < mPosData.Count; i++)
{
mItems[i].SetPosData(mPosData[i]);
}
}

修改RotationDiagramItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEngine;
using UnityEngine.UI;

public class RotationDiagramItem : MonoBehaviour
{
//...
private RectTransform mRect;
private RectTransform MyRect
{
get
{
if (mRect == null)
mRect = GetComponent<RectTransform>();
return mRect;
}
}
//...
}
public void SetPosData(ItemPosData itemPosData)
{
MyRect.anchoredPosition = Vector2.right * itemPosData.X;//Y肯定是零
MyRect.localScale = Vector3.one * itemPosData.Scale;
}
}

实现层级排列

我们创建的Item,需要改变层级来实现中间的最前面的遮挡最后面的效果。

我们可以给Item挂载Canvas组件并且勾选“Override Order”来设定每个Item的渲染层级,但是添加Canvas组件会影响合批和DrawCall,所以我们需要在代码中修改Item的自然层级。

首先我们修改ItemPosData,把它从struct改为class,然后再添加Order字段

1
2
3
4
5
6
public class ItemPosData
{
public float X;
public float Scale;
public int Order;
}

改为class是因为Order字段在后面我们会在mPosData这个列表里使用索引给它赋值,如果是struct,值类型不能修改。

注意ItemPosData这个中间类代表的意义,它缓存了我们“模拟”的椭圆图形的每个Item的X坐标、缩放值和排序位置

然后我们再添加ItemData结构体

1
2
3
4
5
6
7
8
/// <summary>
/// 记录Item位置和层级的中间结构体
/// </summary>
public struct ItemData
{
public int PosId;
public int OrderId;
}

PosId指的是在“椭圆”上每一个Item的Id,它按照我们生成的顺序赋值,也就是椭圆上逆时针方向从前到后。

OrderId暂时用不到

修改RotationDiagramItem,添加PosId字段,并修改SetPosData方法

1
2
3
4
5
6
7
8
9
10
11
public class RotationDiagramItem : MonoBehaviour
{
public int PosId;
//...
public void SetPosData(ItemPosData itemPosData)
{
MyRect.anchoredPosition = Vector2.right * itemPosData.X;//Y肯定是零
MyRect.localScale = Vector3.one * itemPosData.Scale;
transform.SetSiblingIndex(itemPosData.Order);//+++
}
}

修改CalculateData方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void CalculateData()
{
List<ItemData> itemDatas = new List<ItemData>();//是一个临时列表

float len = (ItemSize.x + Offset) * mItems.Count;
float ratioOffet = 1f/mItems.Count;
float ratio = 0;
for (int i = 0; i < mItems.Count; i++)
{
ItemData itemData = new ItemData();//+++
itemData.PosId = i;//+++
itemDatas.Add(itemData);//+++

mItems[i].PosId = i;//itemDatas列表内保存的PosId和mItem自己的PosId是一一对应的

ItemPosData data = new ItemPosData();
data.X = GetX(ratio, len);
data.Scale = GetScaleTimes(ratio, ScaleTimesMax, ScaleTimesMin);

ratio += ratioOffet;
mPosData.Add(data);
}

itemDatas.Sort((item1, item2) =>
{
//按照每一个item对应的mPosData里记录的Scale信息来给itemData列表排序
//排序后Scale较小的排在最前面
return mPosData[item1.PosId].Scale.CompareTo(mPosData[item2.PosId].Scale);
});
for (int i = 0; i < itemDatas.Count; i++)
{
//排序后的itemDatas的Index就是每个item的Order
mPosData[itemDatas[i].PosId].Order = i;
}
}

实现卡片拖动

实现卡片拖动的逻辑是:当指针点击item并左右移动时,卡片不做出反应,我们计算指针的偏移,确定是往左还是往右移动了,只有当指针松开时,才进行卡片的转换。

修改RotationDiagramItem,添加mOffsetX变量,用来记录指针偏移量的总和,从而确定是向左还是向右,添加mMoveAction变量,用来发布指针松开的事件。并添加几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using UnityEngine.EventSystems;

public class RotationDiagramItem : MonoBehaviour, IDragHandler, IEndDragHandler
{
public int PosId;
private Image mImage;
private float mOffsetX;//+++
private System.Action<float> mMoveAction;//+++
//...
public void OnDrag(PointerEventData eventData)
{
mOffsetX += eventData.delta.x;
}

public void OnEndDrag(PointerEventData eventData)
{
mMoveAction.Invoke(mOffsetX);
mOffsetX = 0f;
}
public void AddMoveListener(System.Action<float> onMove)
{
mMoveAction = onMove;
}
public void ChangePosId(int symbol, int totalItemNum)
{
int id = PosId;
id += symbol;
if (id < 0)
{
id += totalItemNum;
}
PosId = id % totalItemNum;
}
}

重点使用eventData.delta来记录指针的偏移量。

使用ChangePosId方法来改变当前Item的PosId。其中symbol形参代表偏移量,因为一次只能移动一个item,所以symbol传过来的值只有1或-1;totalItemNum指的就是当前的Item总数。当当前的PosId是0时,如果传进来的symbol是-1就会超出数组下标,所以我们做个判断。当当前的PosId是最大时,如果传进来的symbol是1也会超出数组下标,所以我们最终取余。

修改RotationDiagram2D

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void CreateItem()
{
GameObject template = CreateTemplate();
RotationDiagramItem itemTemp = null;
foreach (Sprite sprite in ItemSprites)
{
itemTemp = Instantiate(template).GetComponent<RotationDiagramItem>();
itemTemp.SetParent(transform);
itemTemp.SetSprite(sprite);
itemTemp.AddMoveListener(Change);//+++
mItems.Add(itemTemp);
}
Destroy(template);
}
private void Change(float offsetX)//+++
{
int symbol = offsetX > 0 ? 1 : -1;
Change(symbol);
}
private void Change(int sym)//+++
{
//更新所有Item的PosId
foreach (var item in mItems)
{
item.ChangePosId(sym,mItems.Count);
}
//更新所有item的order、X和Scale
for (int i = 0; i < mPosData.Count; i++)
{
var item = mItems[i];
item.SetPosData(mPosData[item.PosId]);
}
}

由于每个item应该的X、Scale、Order都缓存在了mPosData列表里,只要我们更新了每个item的PosId,之后按照PosId更新每个Item的属性即可。

实现转换动画

使用Dotween,改变RotationDiagramItemSetPosData方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using DG.Tweening;

public class RotationDiagramItem : MonoBehaviour, IDragHandler, IEndDragHandler
{
//...
private float mAniTime = 1;
//...
public void SetPosData(ItemPosData itemPosData)
{
MyRect.DOAnchorPos(Vector2.right * itemPosData.X,mAniTime);
//MyRect.anchoredPosition = Vector2.right * itemPosData.X;//Y肯定是零
MyRect.DOScale(Vector3.one * itemPosData.Scale, mAniTime);
//MyRect.localScale = Vector3.one * itemPosData.Scale;
transform.SetSiblingIndex(itemPosData.Order);
}
//...
}

此时拖动Item,向右拖时会发生层级问题,这是因为我们的Item层级会在松开指针的一瞬间更新,所以我们添加一个协程,让层级在动画中途更新,来解决穿帮问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void SetPosData(ItemPosData itemPosData)
{
MyRect.DOAnchorPos(Vector2.right * itemPosData.X,mAniTime);
//MyRect.anchoredPosition = Vector2.right * itemPosData.X;//Y肯定是零
MyRect.DOScale(Vector3.one * itemPosData.Scale, mAniTime);
//MyRect.localScale = Vector3.one * itemPosData.Scale;
StartCoroutine(Wait(itemPosData));
}
private IEnumerator Wait(ItemPosData itemPosData)
{
yield return new WaitForSeconds(mAniTime * 0.5f);
transform.SetSiblingIndex(itemPosData.Order);
}