一、效果
二、脚本及原理
1.卡牌控制脚本
主要负责射线检测卡牌,调用执行当前选择卡牌的进入、持续、离开后的一些功能操作
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.EventSystems;
public class CardUISelectController : MonoBehaviour
{
CardUISelect currentCard; //当前选择的Card
[Header("UI信息提示")]
[SerializeField] Text text;
[SerializeField] Text state;
[Header("检测物体、层级")]
public LayerMask layer;
#region 卡牌参数
[Header("卡牌动画时间")]
public float aniTimer = 0.3f; //dotween动画时间
[Header("卡牌旋转灵敏度、平滑度")]
public float rotSpeed = 0.2f; //旋转灵敏度
[Range(0, 20)] public float damper = 20; //平滑度
[Header("卡牌位置偏移参数")]
public float zAxisOffset = -0.5f; //Z轴偏移位置
public float posOffsetRate = 0.3f; //移动偏移倍率(解决在透视下移动卡牌z轴导致卡牌偏移问题)
[Header("卡牌位置、放大、旋转参数")]
public float scaleRate = 1.5f; //Scale放大倍率
public Vector2 angleAstrict = new Vector2(20, 15);//旋转角度限制
#endregion
public static CardUISelectController instance;
private void Awake()
{
instance = this;
}
void Start()
{
}
void Update()
{
text.text = Input.mousePosition.ToString();
SwitchCurrentCard(RayDetectionTouchItem<CardUISelect>(Input.mousePosition));
if (currentCard)
currentCard.AngleFollowMouse(); //旋转角度跟随鼠标变换
}
#region Card选择切换
public void SwitchCurrentCard(CardUISelect _card)
{
if (currentCard == null) /* currentCard为空 */
{
//当前card为空 射线card的不为空
if (_card != null && _card.isDtEnd)
{
Debug.Log("1.进入");
state.text = "1.进入";
currentCard = _card;
currentCard.OnPointerEnter();//进入
}
}
else /* currentCard不为空 */
{
if (_card != null)
{
//当前card与射线card不相同并且两个card的动画都是结束状态
if (currentCard != _card && _card.isDtEnd && currentCard.isDtEnd)
{
Debug.Log("2.离开在进入");
state.text = "2.离开在进入";
currentCard.OnPointerExit();//离开
currentCard = _card;
currentCard.OnPointerEnter();//进入
}
}
else
{
if (currentCard.isDtEnd)
{
Debug.Log("3.离开");
state.text = "3.离开";
currentCard.OnPointerExit();//离开
currentCard = null;
}
}
}
}
#endregion
#region 射线检测获取卡牌
T RayDetectionTouchItem<T>(Vector2 pos)
{
return RayDetectionUI<T>(pos) != null ? RayDetectionUI<T>(pos) :
RayDetection2D<T>(pos) != null ? RayDetection2D<T>(pos) :
RayDetection3D<T>(pos);
}
#endregion
#region UI射线检测
/// <summary>
/// 获取点击的UI类型Item
/// </summary>
/// <param name="position">点击屏幕坐标</param>
/// <returns></returns>
T RayDetectionUI<T>(Vector2 position)
{
EventSystem eventSystem = EventSystem.current;
PointerEventData pointerEventData = new PointerEventData(eventSystem);
pointerEventData.position = position;
//射线检测ui
List<RaycastResult> uiRaycastResultCache = new List<RaycastResult>();
eventSystem.RaycastAll(pointerEventData, uiRaycastResultCache);
if (uiRaycastResultCache.Count > 0 && uiRaycastResultCache[0].gameObject.GetComponent<T>() != null)
return uiRaycastResultCache[0].gameObject.GetComponent<T>();
return default(T);
}
#endregion
#region 2D BoxClider物体射线检测
T RayDetection2D<T>(Vector3 pos)
{
pos.z = 10;
Vector3 screenPos = Camera.main.ScreenToWorldPoint(pos);
RaycastHit2D hit = Physics2D.Raycast(screenPos, Vector2.zero, int.MaxValue, layer);
if (hit)
Debug.DrawLine(transform.position, hit.point, Color.green);//射线绘制
if (hit) return hit.transform.GetComponent<T>();
return default(T);
}
#endregion
#region 3D物体射线检测
T RayDetection3D<T>(Vector2 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, int.MaxValue, layer) && hit.transform.GetComponent<CardUISelect>().enabled)
{
Debug.DrawLine(transform.position, hit.point, Color.green);//射线绘制
return hit.transform.GetComponent<T>();
}
return default(T);
}
#endregion
}
2.卡牌脚本
主要负责卡牌进入、持续、离开功能操作的具体实现方式
cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening;
public class CardUISelect : MonoBehaviour
{
public bool isDtEnd = true; //dt动画是否结束
Vector3 centerScenePos; //中心点位在屏幕的坐标
Vector3 originPos, originRot, //原始位置、旋转、大小
originSize;
Vector2 angleAstrict; //旋转限制
//DT动画
Tweener posMoveDt, posRecoverDt;//位移和位置复原动画
Tweener bigSizeDt, smallSizeDt;//放大缩小动画
[Header("目标物体")]
[SerializeField] Transform targetObj;
void Start()
{
Debug.Log("初始化中心点位:" + centerScenePos);
//记录初始大小和旋转
originPos = transform.position;
originRot = transform.eulerAngles;
originSize = transform.localScale;
//移动的目标位置(zDistance*0.3f 是为了解决在透视下移动卡牌z轴导致卡牌偏移问题)
float zAxisOffset, posOffsetRate;
zAxisOffset = CardUISelectController.instance.zAxisOffset;
posOffsetRate = CardUISelectController.instance.posOffsetRate;
Vector3 targetPos = new Vector3(originPos.x, originPos.y, originPos.z + zAxisOffset);
if (targetPos.x < 0)
targetPos.x -= zAxisOffset * posOffsetRate;
else if ((targetPos.x > 0))
targetPos.x += zAxisOffset * posOffsetRate;
if (targetPos.y < 0)
targetPos.y -= zAxisOffset * posOffsetRate;
else if ((targetPos.y > 0))
targetPos.y += zAxisOffset * posOffsetRate;
#region 放大缩小Dotween
bigSizeDt = transform.DOScale(originSize * CardUISelectController.instance.scaleRate, CardUISelectController.instance.aniTimer);
bigSizeDt.SetAutoKill(false);
bigSizeDt.Pause();
smallSizeDt = transform.DOScale(originSize, CardUISelectController.instance.aniTimer);
smallSizeDt.SetAutoKill(false);
smallSizeDt.Pause();
#endregion
#region 位置移动
//位置移动
posMoveDt = transform.DOMove(targetPos, CardUISelectController.instance.aniTimer);
posMoveDt.SetAutoKill(false);
posMoveDt.Pause();
posMoveDt.OnComplete(() =>
{
isDtEnd = true; //dotween动画播放结束
});
posRecoverDt = transform.DOMove(originPos, CardUISelectController.instance.aniTimer);
posRecoverDt.SetAutoKill(false);
posRecoverDt.Pause();
posRecoverDt.OnComplete(() =>
{
isDtEnd = true;//dotween动画播放结束
});
#endregion
}
#region 进入&离开卡牌
public void OnPointerEnter()
{
//因为透视效果导致 位置不在中心点时需要对角度进行添加
angleAstrict = CardUISelectController.instance.angleAstrict;
angleAstrict += new Vector2(Mathf.Abs(transform.position.x), Mathf.Abs(transform.position.y));
isDtEnd = false;//min未结束
bigSizeDt.Restart();//重新播放 放大动画
posMoveDt.Restart();//重新播放 位移动画
}
//离开
public void OnPointerExit()
{
isDtEnd = false; //max未结束
targetObj.eulerAngles = originRot;
smallSizeDt.Restart(); //重新播放 缩小动画
posRecoverDt.Restart(); //重新播放 位移复原
}
#endregion
#region 旋转角度跟随鼠标变换
Vector3 targetAngles; //目标旋转位置
Vector3 currentAngles; //当前旋转位置
public void AngleFollowMouse()
{
centerScenePos = CenterPos();//记录ui中心点在屏幕坐标的位置
//方向*距离*速度
targetAngles = (Input.mousePosition - centerScenePos).normalized * Vector3.Distance(Input.mousePosition, centerScenePos) * CardUISelectController.instance.rotSpeed;
targetAngles = new Vector3(targetAngles.y, -targetAngles.x);
//限制范围
targetAngles.x = Mathf.Clamp(targetAngles.x, -angleAstrict.x, angleAstrict.x);
targetAngles.y = Mathf.Clamp(targetAngles.y, -angleAstrict.y, angleAstrict.y);
if (currentAngles != targetAngles)
{
//使用bool判断如果不相同继续旋转执行,如果写在鼠标抬起方法中会导致currentAngles与targetAngles不相同,下次旋转发生错误
currentAngles = Vector3.Lerp(currentAngles, targetAngles, CardUISelectController.instance.damper * Time.deltaTime);
targetObj.rotation = Quaternion.Euler(new Vector3(currentAngles.x, currentAngles.y, 0));
}
}
#endregion
#region 计算物体在屏幕坐标中的位置
Vector3 CenterPos()
{
return Camera.main.WorldToScreenPoint(transform.position);
}
#endregion
}
3.卡牌布局
卡牌布局由一个空父物体和子物体组成。之所以做成两层是因为如果碰撞盒和旋转物体是同一个时,物体旋转的同时会带动碰撞盒旋转导致射线无法检测到卡牌 "边缘"。
父物体层级设置为layer,并且添加一个BoxCollider用于射线检测。并且添加CardUISelect脚本
子物体是一个SpriteRenderer组件,主要用于显示卡牌。
三、包下载
链接:https://pan.baidu.com/s/1ioNazUTupp0jdtF76iEpUw?pwd=syq1
提取码:syq1