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

相关推荐
starsongda6 小时前
科技成果跃然“屏”上,虚拟展厅引领科技展示新风尚
科技·3d·虚拟现实
梦想的理由7 小时前
3D人体建模的前沿探索(二):深入解析SMPL-IK与多视角人体网格重建
3d
charon877810 小时前
UE ARPG | 虚幻引擎战斗系统
游戏引擎
道可云11 小时前
道可云人工智能&元宇宙每日资讯|2024国际虚拟现实创新大会将在青岛举办
大数据·人工智能·3d·机器人·ar·vr
小春熙子11 小时前
Unity图形学之Shader结构
unity·游戏引擎·技术美术
Sitarrrr14 小时前
【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中
3d·unity
极梦网络无忧14 小时前
Unity中IK动画与布偶死亡动画切换的实现
unity·游戏引擎·lucene
逐·風1 天前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i1 天前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
代码盗圣1 天前
GODOT 4 不用scons编译cpp扩展的方法
游戏引擎·godot