方法一:通过添加相机,添加画布,切换相机实现轮转效果。
缺点:耗费性能,不灵活
方法二:通过代码控制图片的位置和缩放比例来实现2D模拟3D轮转播放的效果
1.RotationDiagramItem.cs
using DG.Tweening;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using static RotationDiagram2D;
public class RotationDiagramItem : MonoBehaviour,IDragHandler,IEndDragHandler
{
public int posID;
//_image 是一个私有字段,存储实际的 Image 组件。
private Image _image;
//image 是一个属性,用于访问 _image,并确保在访问之前 _image 已被初始化。
private Image image
{
get
{
if (_image == null)
{
_image = GetComponent<Image>();
}
return _image;
}
}
public float dragOffSetX;
//Action<float> 是一个内置的委托类型,表示可以指向一个接收一个 float 参数并且不返回任何值的方法。
private Action<float> moveAction;
private float aniTime = 1;
private RectTransform _rect;
private RectTransform rect
{
get
{
if (_rect == null)
{
_rect = GetComponent<RectTransform>();
}
return _rect;
}
}
public void SetParent(Transform parent)
{
transform.SetParent(parent);
}
public void SetSprite(Sprite sprite)
{
//通过属性获得sprite
image.sprite = sprite;
}
public void SetPosData(ItemPosData data)
{
rect.DOAnchorPos(Vector2.right * data.X, aniTime);
rect.DOScale(Vector3.one * data.scaletimes, aniTime);
//这里设置一个协程的原因是,我们先设置了前面的层级,生成后面的层级的时候,可能会比前面高,造成前后图形的层级穿插
StartCoroutine(Wait(data));
}
private IEnumerator Wait(ItemPosData data)
{
yield return new WaitForSeconds(aniTime*0.5f);
rect.SetSiblingIndex(data.order);
}
public void OnDrag(PointerEventData eventData)
{
//eventData.delta=与上次事件相比该鼠标的相对移动。
dragOffSetX += eventData.delta.x;
}
public void OnEndDrag(PointerEventData eventData)
{
//该方法中调用了 moveAction(dragOffSetX),这将触发 Change 方法,并传递当前的 dragOffSetX 作为参数 offSetX。
//在 OnEndDrag 方法中,当 moveAction(dragOffSetX); 被调用时,实际上是在调用 Change(dragOffSetX);
moveAction(dragOffSetX);
dragOffSetX = 0;
}
//AddMoveListener 是一个公共方法,接受一个 Action<float> 类型的参数 onmove。这个方法的功能是将传入的 onmove 方法赋值给 moveAction。
//通过这个方法,外部代码可以注册自己的方法来处理移动事件。
public void AddMoveListener(Action<float> onmove)
{
//Action<float> moveAction是一个内置的委托类型,表示可以指向一个接收一个 float 参数并且不返回任何值的方法。
//任何接收 float 类型参数并返回 void 的方法都可以赋给 moveAction
moveAction = onmove;
}
//改变图片的位置posid
public void ChangeId(int totalItemNum,int symbol)
{
int id = posID;
id += symbol;
if (id<0)
{
id += totalItemNum;
}
posID = id % totalItemNum;
}
}
2.RotationDiagram2D.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class RotationDiagram2D : MonoBehaviour
{
public Sprite[] itemSprites;
//此 RectTransform 相对于锚点之间距离的大小。
//如果锚点在一起,则 sizeDelta 与大小相同。如果锚点处于父项四个角的各个角中,则 sizeDelta 是矩形与其父项相比更大或更小的程度。
public Vector2 itemSize;
//存储items图片的列表,存图片
private List<RotationDiagramItem> itemList;
//存储轮转图结构体数据的列表,存图片位置数据
private List<ItemPosData> posDataList;
//辅助列表
private List<ItemData> itemDataList;
//缩放比例
public float ScaleTimesMin;
public float ScaleTimesMax;
//图片之间的间隔
public float offset;
private void Start()
{
itemList = new List<RotationDiagramItem>();
posDataList = new List<ItemPosData>();
itemDataList = new List<ItemData>();
CreateItem();
Calculate();
SetItemData();
}
//创建模板
public GameObject CreateTemplate()
{
GameObject item = new GameObject("Template");
//AddComponent 用于在运行时向一个 GameObject 添加一个新的组件。这通常用于需要动态添加功能的场景,例如当角色获得一个新的能力时。
//GetComponent 用于获取一个已经存在于 GameObject 上的组件。如果该组件不存在,返回值将为 null。这个方法通常用于访问和操作游戏对象
item.AddComponent<RectTransform>().sizeDelta = itemSize;
item.AddComponent<Image>();
item.AddComponent<RotationDiagramItem>();
return item;
}
//创建物体过程:resource-prefab-实例化-gameobj
public void CreateItem()
{
GameObject template = CreateTemplate();
RotationDiagramItem itemTemp = null;
foreach (Sprite sprite in itemSprites)
{
//生成物体,把template实例化,并未获得组件
itemTemp = Instantiate(template).GetComponent<RotationDiagramItem>();
itemTemp.SetParent(transform);
itemTemp.SetSprite(sprite);
//这个方法允许你添加一个监听器(如 Change 方法),该监听器会在调用 moveAction 时被触发。
//itemTemp.AddMoveListener(Change); 这一行将 Change 方法注册到 moveAction 委托中。此时,moveAction 被设置为指向 Change 方法。
itemTemp.AddMoveListener(Change);
itemList.Add(itemTemp);
}
Destroy(template);
}
//offSetX的正负代表了图片移动的方向,正的代表向x正方向移动,负的代表向x的负方向移动
//这是一个监听器,该监听器会在调用 moveAction 时被触发。
//Change 方法接收一个 float 类型的参数offSetX。这个参数就是传递给它的 dragOffSetX 值。
private void Change(float dragOffSetX)
{
//假设 dragOffSetX 的值为 5.0f,当用户结束拖动时:OnEndDrag 被调用。
//moveAction(5.0f); 实际上调用了 Change(5.0f);。在 Change 方法中,offSetX 将等于 5.0f。
int symbol = dragOffSetX > 0 ? 1 : -1;
Change(symbol);
}
//重载指的是在同一个类中,允许定义多个同名但参数不同(参数类型、数量或顺序不同)的方法。这些方法可以执行不同的操作,具体取决于传入的参数
//重写指的是在派生类中重新定义从基类继承的方法。通过重写,派生类可以提供对基类方法的特定实现
private void Change(int symbol)
{
foreach (RotationDiagramItem item in itemList)
{
item.ChangeId(itemList.Count, symbol);
}
for (int i = 0; i < itemList.Count; i++)
{
//把posdatalist的数据按照itemlist的posid的顺序给到setposdata,修改好数据,并且更换显示顺序,给到itemlist进行显示
//这里为什么不需要再次进行排序呢,因为主函数里面已经执行过了排序函数,把排序号的itemlist的posid与posdatalist的id对应起来,
//并且把order顺序给到了posdatalistt,只需要调用setposdata对posdatalist按照order顺序进行排序就好了
//说白了,其实itemlist就是一个辅助的列表,这个辅助列表通过scaletimes来把混乱的posdatalist正确赋予order顺序
//posDataList[itemList[i].posID]这个的作用就是取posdatalist的一个元素,这个元素的下标和itemlist的posid一样,我认为posdatalist的下标就是posid,因为还没有排序
itemList[i].SetPosData(posDataList[itemList[i].posID]);
}
}
//存储轮转图的位置和缩放,结构体
public class ItemPosData
{
public float X;
public float scaletimes;
public int order;
}
public struct ItemData
{
public int posID;
public int orderID;
}
//获取图片的x坐标,radio是比例,length是椭圆总长度,radio是在椭圆上的范围0-1
public float GetX(float radio, float length)
{
if (radio > 1 || radio < 0)
{
Debug.LogError("比例必须在0-1");
return 0;
}
if (radio >= 0 && radio < 0.25)
{
return length * radio;
}
else if (radio >= 0.25 && radio < 0.75)
{
return length * (0.5f - radio);
}
else
{
return length * (radio - 1f);
}
}
//计算对应radio上的对应的缩放比例
public float GetScaleTimes(float radio, float max, float min)
{
if (radio > 1 || radio < 0)
{
Debug.LogError("比例必须在0-1");
return 0;
}
float scaleoffset = (max - min) / 0.5f;
if (radio < 0.5)
{
return max - scaleoffset * radio;
}
else
{
return max - scaleoffset * (1 - radio);
}
}
//计算椭圆总长度
public void Calculate()
{
float length = (offset + itemSize.x) * itemList.Count;
//平均分配图片的位置,每个图片应该的比例位置
float radioOffset = 1 / (float)itemList.Count;
float radio = 0;
for (int i = 0; i < itemList.Count; i++)
{
//记录位置顺序
ItemData itemData = new ItemData();
itemData.posID = i;
itemDataList.Add(itemData);
itemList[i].posID = i;
ItemPosData data = new ItemPosData();
data.X = GetX(radio, length);
data.scaletimes = GetScaleTimes(radio, ScaleTimesMax, ScaleTimesMin);
radio += radioOffset;
posDataList.Add(data);
}
//对itemdatalist进行排序,这里的u是一个引用,代表着itemdatalist里面的每一个元素,
//这里的u.posID其实就是下标,因为u代表着每一个元素,所以posDatas[u.posID]代表着和itemdatalist表中有一样posID的元素
//如果这里没有u.posID,那么就需要通过for循环来遍历所有的posDatas中的元素,说白了就是一个下标的作用
itemDataList = itemDataList.OrderBy(u => posDataList[u.posID].scaletimes).ToList();
for (int i = 0; i < itemDataList.Count; i++)
{
///在 C# 中,结构体(struct)是值类型,当你通过索引访问结构体的成员时,你会得到一个结构体的副本。
///因为结构体是值类型的特性,当你试图修改这个副本的属性时,实际上你并没有改变原始结构体的内容,
///而是修改了一个临时的副本。因此,直接对结构体列表中的某个元素的属性进行赋值会导致编译错误。
///解决方法就是把ItemPosData结构体类型改成类
posDataList[itemDataList[i].posID].order = i;
}
}
public void SetItemData()
{
for (int i = 0; i < posDataList.Count; i++)
{
itemList[i].SetPosData(posDataList[i]);
}
}
}