一、VR交互的类型

  1. Hover(悬停)
    • 定义:发起交互的对象停留在可交互对象的交互区域。例如,当手触摸到物品表面(可交互区域)时,视为触发了Hover。
  2. Grab(抓取)
    • 概念:把物品抓起来。
  3. Use(使用)
    • 基于“抓取”。例如抓取一把枪后,按下枪的扳机发射子弹则视为Use。

三、发起交互的对象(Interactor)

  1. XR Direct Interactor脚本

    • 为了让双手成为发起交互的对象,在LeftHand Controller和RightHand Controller下分别创建Direct Interactor子物体,并添加XR Direct Interactor脚本。
  2. 添加可交互区域

    • XR Direct Interactor需要一个Trigger类型的碰撞体作为可交互区域,且需和它挂载在同一游戏物体上。如在左手和右手的Direct Interactor上分别添加Sphere Collider,调整Radius并勾选Is Trigger。当可交互对象进入该区域后,可进行交互。 ,并设置合适的参数。

在这里插入图片描述

四、可交互的对象(Interactable)创建一个3d对象,比如立方体

  1. 添加刚体

    • 因为手与物品的交互基于物理效果,所以要为可交互物体添加刚体。
    • 在这里插入图片描述
  2. XR Simple Interactable脚本

    • 为方块添加XR Simple Interactable脚本,但是它没有自带抓取的功能。
    • 物体有刚体后该脚本才生效,且挂载脚本的物体还需一个碰撞体。如果是创建的3d对象不需要额外加,如果是自定义模型的话,别忘了手动添加碰撞体
      在这里插入图片描述

2.1、可交互事件

  • 实现功能:
    • 手触碰到方块时,方块颜色改变(模拟Hover,即悬停在可交互区域)。
    • 触碰方块后按下手柄Grip键,方块颜色变成蓝色。
    • 触碰方块后按住Grip键再按Trigger键,方块颜色变回红色。
  • 实现方法:
    • 使用XR Simple Interactable脚本中的Interactable Events
    • 其中Hover Entered对应开始悬停在可交互区域触发的事件,可手动设置更改方块材质。
    • Select EnteredActivated事件分别对应上述第二、三个功能
    • 在Inspector面板中手动绑定事件,Select动作绑定“按下Grip键抓取键”,Activate动作绑定“按下Trigger键扳机键”。 Activate动作以Select动作为前提,Interactable Events中的所有事件以“与可交互对象发生交互”为前提。
      在这里插入图片描述
  1. XR Grab Interactable脚本、和XR Simple Interactable脚本脚本类似,但是有抓取效果;所以需要移除刚刚的XR Simple Interactable脚本

    • Movement Type 移动类型

      • Instantaneous:物体移动位置和姿态完全跟随手的移动,无延迟但无物理刚体效果,物体可穿过碰撞体且碰撞效果非物理。
      • Kinematic:通过Kinematic Rigidbody移动,有延迟,移动中不受力和碰撞作用,但可对其他刚体施加物理效果。
      • Velocity Tracking:通过设置刚体速度和角速度移动,有延迟,有刚体物理效果,可与碰撞体碰撞并对其他刚体产生力的效果。
        在这里插入图片描述
    • Attach Transform 抓取点

      • XR Grab Interactable脚本中有Attach Transform变量可赋值作为抓取点,若未赋值,默认以物体position为抓取点。如抓取枪时,抓取点应在枪柄;
      • 实现功能:可创建枪的子物体Attach Point并赋值给Attach Transform,然后调整其Position位置Rotation以优化抓取效果。但仅设置Attach Transform会有穿模现象,若要实现精细抓取,可参考相关教程。
  2. 代码实现Use功能(制作简易手枪)

    • 核心脚本

      • 创建GunController脚本挂载到枪的游戏物体上。获取XRGrabInteractable中的activated事件,通过AddListener绑定FireBullet函数。
    • 制作子弹预制体

      • 创建子弹Prefab(需刚体和碰撞体),并将子弹Rigidbody的Collision Detection设为Continous Dynamic,防止高速运动时检测不到碰撞。
        在这里插入图片描述
    • 制作子弹发射位置

      • 创建枪的子物体Spawn Point作为子弹生成位置,其z轴箭头方向对应子弹发射方向,将子弹和Spawn Point赋给Gun Controller。
        在这里插入图片描述
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

// 枪支控制器类
public class GunController : MonoBehaviour
{
    // 子弹预制体
    public GameObject bullet;
    // 子弹发射位置的变换组件
    public Transform spawnPoint;
    // 子弹发射速度
    public float fireSpeed = 40;

    void Start()
    {
        // 获取当前物体上的 XRGrabInteractable 组件
        XRGrabInteractable grabbable = GetComponent<XRGrabInteractable>();
        // 为 grabbable 的 activated 事件添加监听器,当该事件触发时调用 FireBullet 方法
        grabbable.activated.AddListener(FireBullet);
    }

    private void FireBullet(ActivateEventArgs arg)
    {
        // 在 spawnPoint 的位置和旋转处实例化子弹预制体
        GameObject spawnBullet = Instantiate(bullet, spawnPoint.position, spawnPoint.rotation);
        // 设置实例化出的子弹的刚体速度,使其沿着 spawnPoint 的前方以 fireSpeed 的速度飞行
        spawnBullet.GetComponent<Rigidbody>().velocity = spawnPoint.forward * fireSpeed;
        // 在 5 秒后销毁这个子弹对象
        Destroy(spawnBullet, 5);
    }
}
  1. 优化一:左右手抓取(判断哪只手与物体交互)
  • 方法一(不推荐)
    • GunController脚本中,给XRGrabInteractableSelectEntered事件绑定ChangeAttachTransform函数,通过判断Interactor是左手还是右手控制器来切换Attach Transform。
      也就是创建两个抓取点,并进行监听ChangeAttachTransform判断左右手、来重新设置抓取点
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

// 枪支控制器类
public class GunController : MonoBehaviour
{
    // 子弹游戏对象
    public GameObject bullet;
    // 子弹发射位置的变换组件
    public Transform spawnPoint;
    // 子弹发射速度
    public float fireSpeed = 40;

    // 左手抓取点的变换组件
    private Transform leftHandAttachPoint;
    // 右手抓取点的变换组件
    private Transform rightHandAttachPoint;
    // 当前物体上的可抓取交互组件
    private XRGrabInteractable grabbable;

    void Start()
    {
        // 在当前物体下查找名为"LeftHand Attach Point"的子物体,获取其变换组件
        leftHandAttachPoint = transform.Find("LeftHand Attach Point");
        // 在当前物体下查找名为"RightHand Attach Point"的子物体,获取其变换组件
        rightHandAttachPoint = transform.Find("RightHand Attach Point");
        // 获取当前物体上的 XRGrabInteractable 组件
        grabbable = GetComponent<XRGrabInteractable>();
        // 为可抓取交互组件的 selectEntered 事件添加监听器,当该事件触发时调用 ChangeAttachTransform 方法
        grabbable.selectEntered.AddListener(ChangeAttachTransform);
        // 为可抓取交互组件的 activated 事件添加监听器,当该事件触发时调用 FireBullet 方法
        grabbable.activated.AddListener(FireBullet);
    }

    private void ChangeAttachTransform(SelectEnterEventArgs arg)
    {
        // 获取触发 selectEntered 事件的交互器的变换组件
        Transform interactor = arg.interactorObject.transform;
        // 如果交互器的父物体名称为"Left Controller",表示是左手控制器
        if (interactor.transform.parent.name == "Left Controller")
        {
            // 将当前物体的抓取点设置为左手抓取点
            grabbable.attachTransform = leftHandAttachPoint;
        }
        else if (interactor.transform.parent.name == "Right Controller")
        {
            // 如果交互器的父物体名称为"Right Controller",表示是右手控制器
            // 将当前物体的抓取点设置为右手抓取点
            grabbable.attachTransform = rightHandAttachPoint;
        }
    }

    private void FireBullet(ActivateEventArgs arg)
    {
        // 在 spawnPoint 的位置和旋转处实例化子弹游戏对象
        GameObject spawnBullet = Instantiate(bullet, spawnPoint.position, spawnPoint.rotation);
        // 获取实例化出的子弹的刚体组件
        Rigidbody bulletRigidbody = spawnBullet.GetComponent<Rigidbody>();
        // 设置子弹的速度,使其沿着 spawnPoint 的前方以 fireSpeed 的速度飞行
        bulletRigidbody.velocity = spawnPoint.forward * fireSpeed;
        // 在 5 秒后销毁这个子弹对象
        Destroy(spawnBullet, 5);
    }
}
  • 方法二减少耦合性
  • 新建脚本XRGrabInteractableTwoAttach继承XRGrabInteractable,重写OnSelectEntered方法,根据Interactor的Tag判断是左手还是右手,切换相应的Attach Transform
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

// 自定义的可抓取交互类,继承自 XRGrabInteractable
public class XRGrabInteractableTwoAttach : XRGrabInteractable
{
    // 左手的抓取点变换
    public Transform leftAttachTransform;
    // 右手的抓取点变换
    public Transform rightAttachTransform;

    // 重写 OnSelectEntered 方法,在选择进入时触发
    protected override void OnSelectEntered(SelectEnterEventArgs args)
    {
        // 如果交互对象的标签是“Left Hand”(左手)
        if (args.interactorObject.transform.CompareTag("Left Hand"))
        {
            // 将当前物体的抓取点设置为左手抓取点
            attachTransform = leftAttachTransform;
        }
        // 如果交互对象的标签是“Right Hand”(右手)
        else if (args.interactorObject.transform.CompareTag("Right Hand"))
        {
            // 将当前物体的抓取点设置为右手抓取点
            attachTransform = rightAttachTransform;
        }
        // 调用基类的 OnSelectEntered 方法
        base.OnSelectEntered(args);
    }
}

在这里插入图片描述

  • 将枪上的XRGrabInteractable抓取脚本替换为XRGrabInteractableTwoAttach,并在编辑器中为左右手Attach Transform赋值并给Direct Interactor添加Tag

在这里插入图片描述
在这里插入图片描述

  • 第一次抓取或第一次切换抓取位置错误解决方法
    - 方法一
    - 在可抓取物体Gun被抓取物体上添加XRSingleGrabFreeTransformer脚本,游戏运行时会自动添加到XRGrabInteractableTwoAttach脚本。
    在这里插入图片描述

方法二
- 重写刚刚新建的XRGrabInteractableTwoAttach脚本的GetAttachTransform方法,而不是OnSelectEntered方法。根据InteractorTag返回相应的Attach Transform,若Tag不匹配则返回基类的GetAttachTransform结果。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class XRGrabInteractableTwoAttach : XRGrabInteractable
{
    public Transform leftAttachTransform;
    public Transform rightAttachTransform;
    public override Transform GetAttachTransform(IXRInteractor interactor)
    {

        Transform i_attachTransform = null;
        if (interactor.transform.CompareTag("Left Hand"))
        {
            i_attachTransform = leftAttachTransform;
        }
        if (interactor.transform.CompareTag("Right Hand"))
        {
            i_attachTransform = rightAttachTransform;
        }
        return i_attachTransform != null ? i_attachTransform : base.GetAttachTransform(interactor);

    }
}

因为之前学的射线交互层级是Everything层级比较高和物体交互层级有重复,所以也能实现射线抓取;然后将与传送有关的 XR Ray Interactor 和 Teleport Area 的 Interaction Layer Mask 改成 Teleport,就可以避免出现这类问题
在这里插入图片描述
在这里插入图片描述

  1. 其他功能一:将与物体接触的地方作为抓取点(Dynamic Attach),也就是碰哪里抓哪里,没有固定抓取点

    • 创建细长Cube作为测试物体,添加碰撞体、刚体和XR Grab Interactable脚本,勾选Use Dynamic Attach后,可根据需求设置Match Position、Match Rotation、Snap To Collider等选项,实现手抓在物体接触部位的效果。

Match Position(匹配位置):当启用相关功能时,确保被抓取物体的位置与抓取点的位置相匹配。例如在使用特定的抓取方式时,可使被抓取物体在被抓取瞬间其位置与抓取点重合,以实现更真实的抓取效果。
Match Rotation(匹配旋转):类似 “匹配位置”,当启用此选项时,被抓取物体的旋转会与抓取点的旋转相匹配。这样可以确保被抓取物体在抓取过程中以合适的角度呈现,增强真实感和交互性。
Snap To Collider(吸附到碰撞体):当开启这个功能后,抓取操作会使被抓取物体吸附到抓取点所在的碰撞体上。这可以使抓取更加精准,并且在抓取过程中物体与抓取点的连接更加紧密,避免出现不合理的位置偏差。

在这里插入图片描述

五、XR Tint Interactable Visual脚本

  1. 功能:可挂载到可交互对象上,当Interactor悬停(Hover)或选中(Select动作触发)可交互对象时,能暂时改变其颜色。
    .在这里插入图片描述2. 设置:调整Tint Color设置颜色,勾选Tint On Hover在Hover时改变颜色,勾选Tint On Selection在Select时改变颜色。注意要把使用的XR Grab Interactable放置到最上层。
    在这里插入图片描述

六、取消身体和可抓取物体的物理碰撞

  1. 设置Layer:将XR Origin的Layer设为Player(仅设置Character Controller所在物体Layer,子物体选No),将所有可抓取物体Layer设为Interactable。
    在这里插入图片描述

  2. 配置Physics:打开Unity编辑器上方菜单栏的Edit编辑 -> Project Settings项目设置-> Physics物理,找到Layer Collision Matrix图层碰撞器,将Player和Interactable的交叉点取消勾选,避免身体与可抓取物体发生物理碰撞。
    在这里插入图片描述

七、XR Interaction Group

  1. 功能:XR Interaction Toolkit 2.3新组件,可管理多个Interactor。当其中一个Interactor生效时,Group内其他Interactor会暂时失效。
    在这里插入图片描述

  2. 应用示例:如在Left/RightHand Controller物体上添加XR Interaction Group组件,将Direct Interactor物体和UI Ray Interactor物体拖到Group中,可实现在抓取物体时让UI射线暂时失效。

八、XR Direct Interactor脚本中的Select Action Trigger

在这里插入图片描述

  1. 参数说明
    • Toggle:以抓取为例,选择Toggle后,靠近物体按下手柄抓取键,物体会被抓在手上,松开抓取键物体仍在手上,下次按下抓取键才会释放。
    • Sticky:按下手柄抓取键物体被抓手上,松开抓取键物体仍在手上,下次按下并松开抓取键物体才会释放。
    • State和State Change的区别(在可抓取物体Select Mode选择Single时)
      • State:可能出现一只手无法接管另一只手抓取权的情况,只有先松开当前抓取手的抓取键才会进行切换抓取。
      • State Change:可以随意切换抓取,推荐在抓取功能上使用State Change。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐