不使用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 { 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)的关系为:
- 在0L~0.25L范围内:(xL)的系数直接表示X轴向上的位置比例(xX)
- 在0.25L~0.5L范围内:(0.5L - xL)的系数表示X轴向上的位置比例(xX)
- 在0.5L~0.75L范围内:(0.5L - xL)的系数表示X轴向上的位置比例(-xX)
- 在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
|
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;
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
添加GetScaleTimes
和SetItemData
方法,用来计算椭圆总长度和设置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; 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; 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
|
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; 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;
ItemPosData data = new ItemPosData(); data.X = GetX(ratio, len); data.Scale = GetScaleTimes(ratio, ScaleTimesMax, ScaleTimesMin);
ratio += ratioOffet; mPosData.Add(data); }
itemDatas.Sort((item1, item2) => { return mPosData[item1.PosId].Scale.CompareTo(mPosData[item2.PosId].Scale); }); for (int i = 0; i < itemDatas.Count; i++) { 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) { foreach (var item in mItems) { item.ChangePosId(sym,mItems.Count); } 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,改变RotationDiagramItem
的SetPosData
方法
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.DOScale(Vector3.one * itemPosData.Scale, mAniTime); 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.DOScale(Vector3.one * itemPosData.Scale, mAniTime); StartCoroutine(Wait(itemPosData)); } private IEnumerator Wait(ItemPosData itemPosData) { yield return new WaitForSeconds(mAniTime * 0.5f); transform.SetSiblingIndex(itemPosData.Order); }
|