Unity之卡牌选中3D效果

一、效果

二、脚本及原理

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

相关推荐
天人合一peng1 小时前
unity 生成标记根据背景色标记变色
unity·游戏引擎
cy_cy0022 小时前
互动滑轨屏如何优化参观动线?
科技·3d·人机交互·交互·软件构建
Coovally AI模型快速验证3 小时前
CVPR 2026|PanDA:首个多模态3D全景分割的无监督域适应框架
人工智能·3d·视觉检测·工业质检
天人合一peng5 小时前
unity 生成标记根据背景色变色为明显的颜色
unity·游戏引擎
魔士于安5 小时前
Unity 超市总动员 超市收银台 超市货架 超市购物手推车 超市常见商品
游戏·unity·游戏引擎·贴图·模型
CandyU25 小时前
Unity —— 数据持久化
unity·游戏引擎
zh路西法6 小时前
【Unity实现Oneshot胶卷显形】游戏窗口化与Win32API的使用
游戏·unity·游戏引擎
迪捷软件7 小时前
显控系统虚拟仿真的工程化路径
游戏引擎·cocos2d
AGV算法笔记7 小时前
CVPR 2024顶级SLAM论文精读:SplaTAM如何用3D高斯实现稠密RGB-D SLAM?
深度学习·3d·机器人视觉·slam·三维重建
hhhhhh_we8 小时前
皮肤人格的工程化实现:预颜美历如何用3D点云与循环神经网络构建数字孪生人格
图像处理·人工智能·rnn·深度学习·神经网络·3d·产品运营